From 6083a895bc6c7b2751251a2d0294d65f87c47db7 Mon Sep 17 00:00:00 2001 From: MisterJulsen Date: Thu, 27 Jun 2024 14:15:50 +0200 Subject: [PATCH 01/10] Updated DragonLib to fix crash --- fabric/src/main/resources/fabric.mod.json | 2 +- forge/src/main/resources/META-INF/mods.toml | 2 +- gradle.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 24a00cc7..b6c4f67a 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -27,7 +27,7 @@ "fabric": "*", "minecraft": ">=1.19.2", "architectury": ">=6.5.85", - "dragonlib": ">=1.19.2-2.1.12", + "dragonlib": ">=1.19.2-2.1.13", "create": "*" }, "accessWidener": "createrailwaysnavigator.accesswidener" diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index ea76800f..633a5c4d 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -40,7 +40,7 @@ side = "BOTH" [[dependencies.createrailwaysnavigator]] modId = "dragonlib" mandatory = true -versionRange = "[1.19.2-2.1.12,)" +versionRange = "[1.19.2-2.1.13,)" ordering = "AFTER" side = "BOTH" diff --git a/gradle.properties b/gradle.properties index 0a45adb9..83ffdc0c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ fabric_loader_version = 0.15.11 fabric_api_version = 0.77.0+1.19.2 forge_version = 1.19.2-43.3.13 -dragonlib_version = 2.1.12 +dragonlib_version = 2.1.13 modmenu_version=4.1.2 forge_config_api_port_version=4.2.11 create_fabric_version = 0.5.1-f-build.1416+mc1.19.2 From 78ce18135b28b61d2d77383ed97d8254183ea387 Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Sat, 12 Oct 2024 12:48:23 +0200 Subject: [PATCH 02/10] 1.19.2 Port --- README.md | 34 +- .../de/mrjulsen/crn/CRNPlatformSpecific.java | 2 + .../main/java/de/mrjulsen/crn/Constants.java | 23 +- .../mrjulsen/crn/CreateRailwaysNavigator.java | 80 +- .../block/AbstractAdvancedDisplayBlock.java | 13 +- .../AbstractAdvancedSidedDisplayBlock.java | 2 +- .../crn/block/AdvancedDisplayBlock.java | 1 + .../crn/block/AdvancedDisplayBoardBlock.java | 2 +- .../block/AdvancedDisplayHalfPanelBlock.java | 4 +- .../crn/block/AdvancedDisplayPanelBlock.java | 2 +- .../crn/block/AdvancedDisplaySlabBlock.java | 166 +++ .../crn/block/AdvancedDisplaySmallBlock.java | 2 +- .../crn/block/TrainStationClockBlock.java | 7 +- .../AdvancedDisplayBlockEntity.java | 226 ++-- .../IColorableBlockEntity.java | 2 +- .../IContraptionBlockEntity.java | 2 +- .../IMultiblockBlockEntity.java | 2 +- .../TrainStationClockBlockEntity.java | 2 +- .../connected/AdvancedDisplayCTBehaviour.java | 2 +- .../AdvancedDisplaySmallCTBehaviour.java | 2 +- .../block/display/AdvancedDisplaySource.java | 1 - .../block/display/AdvancedDisplayTarget.java | 139 +-- .../properties}/EBlockAlignment.java | 2 +- .../properties}/EDisplayInfo.java | 3 +- .../properties}/EDisplayType.java | 16 +- .../crn/{data => block/properties}/ESide.java | 2 +- .../crn/client/AdvancedDisplaysRegistry.java | 101 ++ .../java/de/mrjulsen/crn/client/CRNGui.java | 10 + .../de/mrjulsen/crn/client/ClientWrapper.java | 54 +- .../de/mrjulsen/crn/client/ModGuiUtils.java | 8 + .../ber/AdvancedDisplayRenderInstance.java | 171 +-- .../{variants => }/BERRenderSubtypeBase.java | 9 +- .../crn/client/ber/IBERRenderSubtype.java | 16 + .../client/ber/TrainStationClockRenderer.java | 71 +- .../mrjulsen/crn/client/ber/base/BERText.java | 350 ------ .../crn/client/ber/variants/BERError.java | 41 + .../variants/BERPassengerInfoDetailed.java | 234 ----- .../variants/BERPassengerInfoInformative.java | 994 ++++++++++-------- .../ber/variants/BERPassengerInfoSimple.java | 283 ++--- .../ber/variants/BERPlatformDetailed.java | 379 +++---- .../ber/variants/BERPlatformInformative.java | 625 ++++++----- .../ber/variants/BERPlatformSimple.java | 123 +-- .../variants/BERTrainDestinationDetailed.java | 161 +-- .../BERTrainDestinationInformative.java | 233 ++-- .../variants/BERTrainDestinationSimple.java | 108 +- .../ber/variants/IBERRenderSubtype.java | 19 - .../de/mrjulsen/crn/client/gui/Animator.java | 93 ++ .../crn/client/gui/CreateDynamicWidgets.java | 146 ++- .../{screen => }/CustomIconScreenElement.java | 2 +- .../mrjulsen/crn/client/gui/ModGuiIcons.java | 25 +- .../gui/overlay/RouteDetailsOverlay.java | 296 ++++++ .../overlay/RouteDetailsOverlayScreen.java | 787 -------------- .../pages/AbstractRouteDetailsPage.java | 20 + .../overlay/pages/ConnectionMissedPage.java | 42 + .../overlay/pages/JourneyCompletedPage.java | 58 + .../overlay/pages/NextConnectionsPage.java | 108 ++ .../gui/overlay/pages/RouteOverviewPage.java | 122 +++ .../gui/overlay/pages/TrainCancelledInfo.java | 42 + .../gui/overlay/pages/TransferPage.java | 69 ++ .../client/gui/overlay/pages/WelcomePage.java | 69 ++ .../gui/screen/AbstractBlacklistScreen.java | 357 ------- .../AbstractEntryListSettingsScreen.java | 355 ------- .../gui/screen/AbstractNavigatorScreen.java | 68 ++ .../screen/AdvancedDisplaySettingsScreen.java | 86 +- .../gui/screen/AliasSettingsScreen.java | 34 - .../gui/screen/GlobalSettingsScreen.java | 520 ++++++--- .../crn/client/gui/screen/LoadingScreen.java | 45 - .../client/gui/screen/NavigatorScreen.java | 496 ++++----- .../client/gui/screen/RouteDetailsScreen.java | 515 +++------ .../screen/RouteOverlaySettingsScreen.java | 68 +- .../client/gui/screen/SavedRoutesScreen.java | 58 + .../gui/screen/ScheduleBoardScreen.java | 268 +++++ .../gui/screen/SearchSettingsScreen.java | 432 -------- .../gui/screen/StationBlacklistScreen.java | 47 - .../gui/screen/StationTagSettingsScreen.java | 289 +++++ .../gui/screen/TrainBlacklistScreen.java | 47 - .../client/gui/screen/TrainDebugScreen.java | 66 ++ .../client/gui/screen/TrainGroupScreen.java | 34 - .../client/gui/screen/TrainJourneySreen.java | 76 ++ .../screen/TrainSectionSettingsScreen.java | 225 ++++ .../AbstractEntryListOptionWidget.java | 16 - .../gui/widgets/AbstractFlyoutWidget.java | 223 ++++ .../widgets/AbstractNotificationPopup.java | 115 ++ .../client/gui/widgets/AliasEntryWidget.java | 501 --------- .../crn/client/gui/widgets/CRNListBox.java | 87 ++ .../client/gui/widgets/ColorPickerWidget.java | 109 ++ .../gui/widgets/DLCreateIconButton.java | 19 + .../crn/client/gui/widgets/DLCreateLabel.java | 2 +- .../widgets/DLCreateSelectionScrollInput.java | 2 +- .../crn/client/gui/widgets/ExpandButton.java | 59 -- .../crn/client/gui/widgets/FlatCheckBox.java | 34 + .../gui/widgets/IEntryListSettingsOption.java | 29 - .../widgets/ModDestinationSuggestions.java | 10 +- .../gui/widgets/ModernVerticalScrollBar.java | 46 + .../client/gui/widgets/ResizableButton.java | 35 + .../gui/widgets/RouteDetailsViewer.java | 146 +++ .../gui/widgets/RouteEntryOverviewWidget.java | 141 --- .../crn/client/gui/widgets/RouteViewer.java | 72 ++ .../crn/client/gui/widgets/RouteWidget.java | 146 +++ .../client/gui/widgets/SavedRouteWidget.java | 105 ++ .../client/gui/widgets/SavedRoutesViewer.java | 132 +++ .../gui/widgets/SearchOptionButton.java | 47 + .../gui/widgets/SettingsOptionWidget.java | 65 -- .../gui/widgets/StationDeparturesViewer.java | 86 ++ .../gui/widgets/StationDeparturesWidget.java | 108 ++ .../client/gui/widgets/TrainDebugViewer.java | 83 ++ .../client/gui/widgets/TrainDebugWidget.java | 74 ++ .../gui/widgets/TrainGroupEntryWidget.java | 408 ------- .../widgets/WidgetContainerCollection.java | 67 -- .../create/CreateTimeSelectionWidget.java | 82 ++ .../FlyoutAdvancedSearchsettingsWidget.java | 63 ++ .../widgets/flyouts/FlyoutColorPicker.java | 30 + .../flyouts/FlyoutDepartureInWidget.java | 76 ++ .../flyouts/FlyoutTrainGroupsWidget.java | 96 ++ .../flyouts/FlyoutTransferTimeWidget.java | 75 ++ .../NotificationTrainInitialization.java | 54 + .../options/AbstractDataListEntry.java | 166 +++ .../gui/widgets/options/DLOptionsList.java | 114 ++ .../widgets/options/DataListContainer.java | 247 +++++ .../gui/widgets/options/NewEntryWidget.java | 132 +++ .../gui/widgets/options/OptionEntry.java | 245 +++++ .../widgets/options/OptionEntryHeader.java | 76 ++ .../widgets/options/SimpleDataListEntry.java | 113 ++ .../options/SimpleDataListNewEntry.java | 128 +++ .../RouteDetailsTransferWidget.java | 55 + .../routedetails/RoutePartEntryWidget.java | 114 ++ .../RoutePartTrainDetailsWidget.java | 192 ++++ .../widgets/routedetails/RoutePartWidget.java | 163 +++ .../mrjulsen/crn/client/lang/ELanguage.java | 6 +- .../de/mrjulsen/crn/cmd/DebugCommand.java | 105 ++ .../mrjulsen/crn/config/ModClientConfig.java | 31 +- .../mrjulsen/crn/config/ModCommonConfig.java | 34 +- .../de/mrjulsen/crn/core/navigation/Edge.java | 70 -- .../mrjulsen/crn/core/navigation/Graph.java | 423 -------- .../de/mrjulsen/crn/core/navigation/Node.java | 101 -- .../crn/core/navigation/TrainData.java | 25 - .../crn/core/navigation/TrainSchedule.java | 105 -- .../crn/data/ClientTrainStationSnapshot.java | 66 -- .../crn/data/DeparturePrediction.java | 185 ---- .../de/mrjulsen/crn/data/EFilterCriteria.java | 62 -- .../de/mrjulsen/crn/data/EResultCount.java | 50 - .../de/mrjulsen/crn/data/GlobalSettings.java | 358 ------- .../crn/data/GlobalSettingsManager.java | 82 -- .../de/mrjulsen/crn/data/GlobalTrainData.java | 268 ----- .../crn/data/ISaveableNavigatorData.java | 28 + .../mrjulsen/crn/data/ITranslatableEnum.java | 18 - .../crn/data/NearestTrackStationResult.java | 40 +- .../main/java/de/mrjulsen/crn/data/Route.java | 101 -- .../java/de/mrjulsen/crn/data/RoutePart.java | 126 --- .../mrjulsen/crn/data/SavedRoutesManager.java | 76 ++ .../de/mrjulsen/crn/data/SimpleRoute.java | 508 --------- .../crn/data/SimpleTrainSchedule.java | 265 ----- .../crn/data/SimulatedTrainSchedule.java | 122 --- .../java/de/mrjulsen/crn/data/StationTag.java | 295 ++++++ .../crn/data/{AliasName.java => TagName.java} | 22 +- ...inConnection.java => TrainConnection.java} | 8 +- .../java/de/mrjulsen/crn/data/TrainData.java | 49 - .../de/mrjulsen/crn/data/TrainExitSide.java | 27 + .../java/de/mrjulsen/crn/data/TrainGroup.java | 115 +- .../java/de/mrjulsen/crn/data/TrainInfo.java | 28 + .../java/de/mrjulsen/crn/data/TrainLine.java | 47 + .../mrjulsen/crn/data/TrainStationAlias.java | 217 ---- .../java/de/mrjulsen/crn/data/TrainStop.java | 58 - .../de/mrjulsen/crn/data/UserSettings.java | 235 ++++- .../crn/data/navigation/ClientRoute.java | 673 ++++++++++++ .../crn/data/navigation/ClientRoutePart.java | 323 ++++++ .../data/navigation/ClientTrainListener.java | 67 ++ .../crn/data/navigation/EdgeData.java | 63 ++ .../data/navigation/ITrainListenerClient.java | 5 + .../crn/data/navigation/NavigatableGraph.java | 430 ++++++++ .../de/mrjulsen/crn/data/navigation/Node.java | 185 ++++ .../mrjulsen/crn/data/navigation/Route.java | 146 +++ .../crn/data/navigation/RoutePart.java | 217 ++++ .../crn/data/navigation/TrainSchedule.java | 89 ++ .../data/navigation/TransferConnection.java | 110 ++ .../condition/DynamicDelayCondition.java | 147 +++ .../ICustomSuggestionsInstruction.java | 10 + .../instruction/IPredictableInstruction.java | 11 + .../IStationPredictableInstruction.java | 12 + .../instruction/IStationTagInstruction.java | 3 + .../instruction/ITrainNameInstruction.java | 3 + .../instruction/ResetTimingsInstruction.java | 88 ++ .../instruction/TravelSectionInstruction.java | 140 +++ .../crn/data/storage/GlobalSettings.java | 496 +++++++++ .../data/storage/GlobalSettingsClient.java | 124 +++ .../crn/data/train/ClientTrainStop.java | 215 ++++ .../data/train/RoutePartProgressState.java | 40 + .../crn/data/train/RouteProgressState.java | 52 + .../de/mrjulsen/crn/data/train/TrainData.java | 672 ++++++++++++ .../crn/data/train/TrainListener.java | 225 ++++ .../crn/data/train/TrainPrediction.java | 509 +++++++++ .../mrjulsen/crn/data/train/TrainState.java | 35 + .../mrjulsen/crn/data/train/TrainStatus.java | 226 ++++ .../de/mrjulsen/crn/data/train/TrainStop.java | 417 ++++++++ .../crn/data/train/TrainTravelSection.java | 264 +++++ .../mrjulsen/crn/data/train/TrainUtils.java | 253 +++++ .../train/portable/BasicTrainDisplayData.java | 144 +++ .../portable/NextConnectionsDisplayData.java | 60 ++ .../train/portable/StationDisplayData.java | 149 +++ .../data/train/portable/TrainDisplayData.java | 208 ++++ .../train/portable/TrainStopDisplayData.java | 170 +++ .../de/mrjulsen/crn/debug/DebugOverlay.java | 127 +++ .../de/mrjulsen/crn/debug/TrainDebugData.java | 61 ++ .../mrjulsen/crn/debug/TrainDebugState.java | 44 + .../event/CRNClientEventsRegistryEvent.java | 12 + .../event/CRNCommonEventsRegistryEvent.java | 12 + .../mrjulsen/crn/event/CRNEventsManager.java | 111 ++ .../java/de/mrjulsen/crn/event/IEvent.java | 16 + ...ClientEvents.java => ModClientEvents.java} | 66 +- .../mrjulsen/crn/event/ModCommonEvents.java | 121 +++ .../java/de/mrjulsen/crn/event/ModEvents.java | 53 - .../events/CreateTrainPredictionEvent.java | 22 + .../events/DefaultTrainDataRefreshEvent.java | 11 + ...lobalTrainDisplayDataRefreshEventPost.java | 10 + ...GlobalTrainDisplayDataRefreshEventPre.java | 10 + .../events/RouteDetailsActionsEvent.java | 23 + .../crn/event/events/ScheduleResetEvent.java | 17 + .../events/SubmitTrainPredictionsEvent.java | 20 + .../events/TotalDurationTimeChangedEvent.java | 16 + .../events/TrainArrivalAndDepartureEvent.java | 23 + .../events/TrainDestinationChangedEvent.java | 18 + .../listeners/IJourneyListenerClient.java | 7 - .../crn/event/listeners/JourneyListener.java | 881 ---------------- .../listeners/JourneyListenerManager.java | 125 --- .../crn/event/listeners/TrainListener.java | 136 --- .../crn/exceptions/RuntimeSideException.java | 7 + .../de/mrjulsen/crn/item/NavigatorItem.java | 3 +- .../crn/mixin/ControlsHandlerMixin.java | 43 + .../mixin/GlobalTrainDisplayDataMixin.java | 34 + .../mixin/ModularGuiLineBuilderAccessor.java | 21 + .../crn/mixin/MountedStorageManagerMixin.java | 4 +- .../mixin/ReloadableServerResourcesMixin.java | 36 + ...ssor.java => ScheduleRuntimeAccessor.java} | 17 +- .../crn/mixin/ScheduleRuntimeMixin.java | 153 +++ .../crn/mixin/ScheduleScreenAccessor.java | 37 + .../crn/mixin/ScheduleScreenMixin.java | 71 ++ .../de/mrjulsen/crn/mixin/TrainMixin.java | 35 + .../crn/mixin/TrainStatusAccessor.java | 16 + .../mrjulsen/crn/network/InstanceManager.java | 132 --- .../cts/AdvancedDisplayUpdatePacket.java | 31 +- .../cts/GlobalSettingsRequestPacket.java | 40 - .../cts/GlobalSettingsUpdatePacket.java | 152 --- .../packets/cts/NavigationRequestPacket.java | 92 -- .../cts/NearestStationRequestPacket.java | 65 -- .../cts/NextConnectionsRequestPacket.java | 57 - .../packets/cts/RealtimeRequestPacket.java | 72 -- .../cts/TrackStationsRequestPacket.java | 47 - .../packets/cts/TrainDataRequestPacket.java | 196 ---- .../stc/GlobalSettingsResponsePacket.java | 49 - .../packets/stc/NavigationResponsePacket.java | 65 -- .../stc/NearestStationResponsePacket.java | 46 - .../stc/NextConnectionsResponsePacket.java | 60 -- .../packets/stc/RealtimeResponsePacket.java | 62 -- .../packets/stc/ServerErrorPacket.java | 4 +- .../packets/stc/TimeCorrectionPacket.java | 40 - .../stc/TrackStationResponsePacket.java | 83 -- .../packets/stc/TrainDataResponsePacket.java | 50 - .../crn/registry/ModAccessorTypes.java | 932 ++++++++++++++++ .../crn/registry/ModBlockEntities.java | 9 +- .../de/mrjulsen/crn/registry/ModBlocks.java | 17 +- .../mrjulsen/crn/registry/ModDisplayTags.java | 1 + .../crn/registry/ModDisplayTypes.java | 81 ++ .../de/mrjulsen/crn/registry/ModExtras.java | 2 +- .../de/mrjulsen/crn/registry/ModItems.java | 2 +- .../de/mrjulsen/crn/registry/ModSchedule.java | 34 + .../crn/registry/ModTrainStatusInfos.java | 76 ++ .../data/NextConnectionsRequestData.java | 24 + .../java/de/mrjulsen/crn/util/BERUtils.java | 66 -- .../de/mrjulsen/crn/util/DLListUtils.java | 48 + .../de/mrjulsen/crn/util/IListenable.java | 76 ++ .../java/de/mrjulsen/crn/util/LockedList.java | 73 ++ .../java/de/mrjulsen/crn/util/ModUtils.java | 82 +- .../java/de/mrjulsen/crn/util/TrainUtils.java | 297 ------ .../de/mrjulsen/crn/web/SimpleWebServer.java | 274 +++++ .../web/WebsitePreparableReloadListener.java | 57 + .../blockstates/advanced_display_slab.json | 101 ++ .../createrailwaysnavigator/lang/bar.json | 37 +- .../createrailwaysnavigator/lang/de_de.json | 147 ++- .../createrailwaysnavigator/lang/en_us.json | 151 ++- .../createrailwaysnavigator/lang/es_es.json | 35 +- .../createrailwaysnavigator/lang/fr_fr.json | 250 +++++ .../createrailwaysnavigator/lang/ko_kr.json | 250 +++++ .../createrailwaysnavigator/lang/nl_nl.json | 35 +- .../createrailwaysnavigator/lang/pl_pl.json | 35 +- .../createrailwaysnavigator/lang/pt_pt.json | 250 +++++ .../createrailwaysnavigator/lang/ru_ru.json | 35 +- .../createrailwaysnavigator/lang/sv_se.json | 250 +++++ .../createrailwaysnavigator/lang/sxu.json | 37 +- .../createrailwaysnavigator/lang/zh_cn.json | 35 +- .../block/advanced_display_slab_cen.json | 62 ++ .../advanced_display_slab_double_cen.json | 62 ++ .../advanced_display_slab_double_neg.json | 62 ++ .../advanced_display_slab_double_pos.json | 62 ++ .../block/advanced_display_slab_neg.json | 62 ++ .../block/advanced_display_slab_pos.json | 62 ++ .../models/item/advanced_display_slab.json | 3 + .../textures/gui/container_blue.png | Bin 0 -> 827 bytes .../textures/gui/container_gold.png | Bin 0 -> 827 bytes .../textures/gui/container_gray.png | Bin 0 -> 827 bytes .../textures/gui/container_purple.png | Bin 0 -> 827 bytes .../textures/gui/gui.png | Bin 0 -> 7821 bytes .../textures/gui/icons.png | Bin 7151 -> 9953 bytes .../textures/gui/section_settings.png | Bin 0 -> 6459 bytes .../textures/gui/shadow.png | Bin 0 -> 4514 bytes .../textures/gui/widgets.png | Bin 6374 -> 6963 bytes .../textures/mod_icon.png | Bin 961 -> 7724 bytes .../createrailwaysnavigator.accesswidener | 4 +- .../createrailwaysnavigator.mixins.json | 13 +- .../crn_website/index.html | 13 + .../crn_website/script.js | 4 + .../crn_website/style.css | 10 + .../blocks/advanced_display_slab.json | 20 + .../tags/blocks/advanced_displays.json | 3 +- .../tags/items/advanced_displays.json | 3 +- .../tags/blocks/mineable/pickaxe.json | 1 + common/src/main/resources/icon.png | Bin 45387 -> 412476 bytes .../fabric/CreateRailwaysNavigatorFabric.java | 6 +- .../recipes/advanced_display_slab.json | 15 + fabric/src/main/resources/fabric.mod.json | 2 +- forge/src/main/resources/META-INF/mods.toml | 2 +- .../recipes/advanced_display_slab.json | 15 + gradle.properties | 4 +- icon.png | Bin 84414 -> 412476 bytes icon_256px.png | Bin 45387 -> 39483 bytes reset.bat | 11 + 325 files changed, 21915 insertions(+), 13366 deletions(-) create mode 100644 common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySlabBlock.java rename common/src/main/java/de/mrjulsen/crn/block/{be => blockentity}/AdvancedDisplayBlockEntity.java (70%) rename common/src/main/java/de/mrjulsen/crn/block/{be => blockentity}/IColorableBlockEntity.java (68%) rename common/src/main/java/de/mrjulsen/crn/block/{be => blockentity}/IContraptionBlockEntity.java (92%) rename common/src/main/java/de/mrjulsen/crn/block/{be => blockentity}/IMultiblockBlockEntity.java (98%) rename common/src/main/java/de/mrjulsen/crn/block/{be => blockentity}/TrainStationClockBlockEntity.java (98%) rename common/src/main/java/de/mrjulsen/crn/{data => block/properties}/EBlockAlignment.java (95%) rename common/src/main/java/de/mrjulsen/crn/{data => block/properties}/EDisplayInfo.java (95%) rename common/src/main/java/de/mrjulsen/crn/{data => block/properties}/EDisplayType.java (66%) rename common/src/main/java/de/mrjulsen/crn/{data => block/properties}/ESide.java (95%) create mode 100644 common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/CRNGui.java rename common/src/main/java/de/mrjulsen/crn/client/ber/{variants => }/BERRenderSubtypeBase.java (72%) create mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/IBERRenderSubtype.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/base/BERText.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoDetailed.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/variants/IBERRenderSubtype.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/Animator.java rename common/src/main/java/de/mrjulsen/crn/client/gui/{screen => }/CustomIconScreenElement.java (95%) create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlay.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlayScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/AbstractRouteDetailsPage.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/ConnectionMissedPage.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/JourneyCompletedPage.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TrainCancelledInfo.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractBlacklistScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractEntryListSettingsScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/AliasSettingsScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/LoadingScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/SavedRoutesScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/ScheduleBoardScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/SearchSettingsScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationBlacklistScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationTagSettingsScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainBlacklistScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainDebugScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainGroupScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractEntryListOptionWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AliasEntryWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ExpandButton.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/FlatCheckBox.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IEntryListSettingsOption.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModernVerticalScrollBar.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ResizableButton.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteEntryOverviewWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SearchOptionButton.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SettingsOptionWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugWidget.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainGroupEntryWidget.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/WidgetContainerCollection.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/notifications/NotificationTrainInitialization.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntryHeader.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListEntry.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListNewEntry.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/core/navigation/Edge.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/core/navigation/Graph.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/core/navigation/Node.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/core/navigation/TrainData.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/core/navigation/TrainSchedule.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/ClientTrainStationSnapshot.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/DeparturePrediction.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/EFilterCriteria.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/EResultCount.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/GlobalSettings.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/GlobalSettingsManager.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/GlobalTrainData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/ITranslatableEnum.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/Route.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/RoutePart.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/SavedRoutesManager.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/SimpleRoute.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/SimpleTrainSchedule.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/SimulatedTrainSchedule.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/StationTag.java rename common/src/main/java/de/mrjulsen/crn/data/{AliasName.java => TagName.java} (61%) rename common/src/main/java/de/mrjulsen/crn/data/{SimpleTrainConnection.java => TrainConnection.java} (77%) delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/TrainData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/TrainExitSide.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/TrainLine.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/TrainStationAlias.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/data/TrainStop.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/EdgeData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/ITrainListenerClient.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/navigation/TransferConnection.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ICustomSuggestionsInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IPredictableInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationPredictableInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationTagInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ITrainNameInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettingsClient.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/ClientTrainStop.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/RoutePartProgressState.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/RouteProgressState.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainState.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java create mode 100644 common/src/main/java/de/mrjulsen/crn/debug/TrainDebugData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/debug/TrainDebugState.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/CRNClientEventsRegistryEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/CRNCommonEventsRegistryEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/CRNEventsManager.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/IEvent.java rename common/src/main/java/de/mrjulsen/crn/event/{ClientEvents.java => ModClientEvents.java} (53%) create mode 100644 common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/event/ModEvents.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/CreateTrainPredictionEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/DefaultTrainDataRefreshEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPost.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPre.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/RouteDetailsActionsEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/ScheduleResetEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/SubmitTrainPredictionsEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/TotalDurationTimeChangedEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java create mode 100644 common/src/main/java/de/mrjulsen/crn/event/events/TrainDestinationChangedEvent.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/event/listeners/IJourneyListenerClient.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListener.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListenerManager.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/event/listeners/TrainListener.java create mode 100644 common/src/main/java/de/mrjulsen/crn/exceptions/RuntimeSideException.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ControlsHandlerMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/GlobalTrainDisplayDataMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ModularGuiLineBuilderAccessor.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java rename common/src/main/java/de/mrjulsen/crn/mixin/{ScheduleDataAccessor.java => ScheduleRuntimeAccessor.java} (50%) create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenAccessor.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/TrainStatusAccessor.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsUpdatePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/NavigationRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/NearestStationRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/NextConnectionsRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/RealtimeRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrackStationsRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrainDataRequestPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/GlobalSettingsResponsePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/NavigationResponsePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/NearestStationResponsePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/NextConnectionsResponsePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/RealtimeResponsePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/TimeCorrectionPacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrackStationResponsePacket.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrainDataResponsePacket.java create mode 100644 common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java create mode 100644 common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java create mode 100644 common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java create mode 100644 common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java create mode 100644 common/src/main/java/de/mrjulsen/crn/registry/data/NextConnectionsRequestData.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/util/BERUtils.java create mode 100644 common/src/main/java/de/mrjulsen/crn/util/DLListUtils.java create mode 100644 common/src/main/java/de/mrjulsen/crn/util/IListenable.java create mode 100644 common/src/main/java/de/mrjulsen/crn/util/LockedList.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/util/TrainUtils.java create mode 100644 common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/blockstates/advanced_display_slab.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_slab.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/container_blue.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/container_gold.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/container_gray.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/container_purple.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/gui.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/section_settings.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/shadow.png create mode 100644 common/src/main/resources/data/createrailwaysnavigator/crn_website/index.html create mode 100644 common/src/main/resources/data/createrailwaysnavigator/crn_website/script.js create mode 100644 common/src/main/resources/data/createrailwaysnavigator/crn_website/style.css create mode 100644 common/src/main/resources/data/createrailwaysnavigator/loot_tables/blocks/advanced_display_slab.json create mode 100644 fabric/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json create mode 100644 forge/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json create mode 100644 reset.bat diff --git a/README.md b/README.md index 57209af7..48a5a986 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Create Railways Navigator (Minecraft Create Mod Addon) -![Logo](https://github.com/MisterJulsen/Create-Train-Navigator/blob/1.19.2/icon_256px.png) +# 🚅 Create Railways Navigator (Minecraft Create Mod Addon) +![Logo](https://github.com/user-attachments/assets/2ee05cf6-c816-4dd7-8c24-6bb05062758e) Get a list possible train connections in your world from one station to another using the Create Railways Navigator. @@ -7,22 +7,32 @@ Get a list possible train connections in your world from one station to another [![Modrinth](https://i.imgur.com/uLIB4gb.png)](https://modrinth.com/mod/create-railways-navigator) [![CurseForge](https://i.imgur.com/XZYlGVF.png)](https://www.curseforge.com/minecraft/mc-mods/create-railways-navigator) -## Dependencies +## 📚 Dependencies This mod requires the Minecraft [Create Mod v0.5.1e](https://www.curseforge.com/minecraft/mc-mods/create) or newer for [Minecraft Forge](https://files.minecraftforge.net) or later. This mod also uses [DragonLib](https://www.curseforge.com/minecraft/mc-mods/dragonlib), which is already embedded into the built jar, so you don't have to install it manually. -## Features +## ✅ Features This mod adds a new item, the Create Railways Navigator, which is inspired by the [DB Navigator](https://de.wikipedia.org/wiki/DB_Navigator) (an app in Germany where, among other things, you can get possible train connections). Like this app, the navigator in this mod is intended to suggest possible train connections for trains from the Create Mod in your Minecraft world. Various customization options allow you to specify which track stations should be treated as a single station and how your navigation results should be filtered and sorted so that you always receive the best possible route suggestions. -## Supported Languages -- English (100%) -- German (100%) -- Dutch (100%) (by TheSatanicFlame) -- Polish (100%) (by Slasherss) - -## **Please note!** +## 🗣️ Supported Languages +- English +- German +- Dutch (by TheSatanicFlame) +- Polish (by Slasherss) +- Chinese (simplified) (by Mingshuai Zhu, iaddda) +- Saxon (DE) (by PULZ418) +- Bavarian (DE) +- Spanish (by albertosaurio65) +- Russian (by VGamerGroup) +- French (by GeoffreyGx) +- Portuguese (by AlfredoProgramer) +- Korean (by queso-gato1355) +- Swedish (by Geoffrey) + + +## ⚠️ **Please note!** To protect your world from damage, you should always create a backup of your world before installing an update of this mod. Alpha versions in particular may contain critical bugs! -## **Dependencies** +## 🐉 **Dependencies** This mod uses **DragonLib** as a library mod that contains useful code shared accross all my mods. DragonLib is embedded in all builds of Create Railways Navigator since version `0.2.0-beta-1.18.2`, so you don't need to install DragonLib manually. If you are developer and are interested in this library you can find more information about it on [GitHub](https://github.com/MisterJulsen/MC-DragonLib "DragonLib on GitHub"). [![DragonLib](https://i.imgur.com/4d8BF5J.png)](https://github.com/MisterJulsen/MC-DragonLib "DragonLib on GitHub") diff --git a/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java b/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java index f125577a..5992e2f9 100644 --- a/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java +++ b/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java @@ -39,8 +39,10 @@ public static void registerConfig() { throw new AssertionError(); } + @ExpectPlatform public static GlobalStation getStationFromBlockEntity(BlockEntity be) { throw new AssertionError(); } + } diff --git a/common/src/main/java/de/mrjulsen/crn/Constants.java b/common/src/main/java/de/mrjulsen/crn/Constants.java index 6ee63c48..89951cbf 100644 --- a/common/src/main/java/de/mrjulsen/crn/Constants.java +++ b/common/src/main/java/de/mrjulsen/crn/Constants.java @@ -19,8 +19,25 @@ public class Constants { public static final Component TEXT_FALSE = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.false"); public static final Component TEXT_SERVER_ERROR = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.server_error"); public static final Component TEXT_SEARCH = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.search"); - public static final UUID ZERO_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + public static final Component TEXT_HELP = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.help"); + public static final UUID ZERO_UUID = new UUID(0, 0); + public static final int[] DEFAULT_TRAIN_TYPE_COLORS = new int[] { 0xFF393939, 0xFFf0f3f5, 0xFFafb4bb, 0xFF878c96, 0xFF2a7230, 0xFF814997, 0xFF1455c0, 0xFFa9455d, 0xFF55b9e6, 0xFFffd800 }; - public static final int COLOR_ON_TIME = 0x1AEA5F; - public static final int COLOR_DELAYED = 0xFF4242; + public static final int COLOR_ON_TIME = 0xFF1AEA5F; + public static final int COLOR_DELAYED = 0xFFFF4242; + public static final int COLOR_TRAIN_BACKGROUND = 0xFF393939; + + public static final String GITHUB_WIKI = "https://github.com/MisterJulsen/Create-Train-Navigator/wiki/"; + public static final String HELP_PAGE_ADVANCED_DISPLAYS = GITHUB_WIKI + "Advanced-Displays"; + public static final String HELP_PAGE_DYNAMIC_DELAYS = GITHUB_WIKI + "Dynamic-Delays"; + public static final String HELP_PAGE_GLOBAL_SETTINGS = GITHUB_WIKI + "Global-Settings"; + public static final String HELP_PAGE_NAVIGATION_WARNING = GITHUB_WIKI + "Navigation-Warning"; + public static final String HELP_PAGE_SCHEDULE_SECTIONS = GITHUB_WIKI + "Train-Schedule-Sections"; + public static final String HELP_PAGE_SCHEDULED_TIMES_AND_REAL_TIME = GITHUB_WIKI + "Scheduled-Time-and-Real-Time"; + public static final String HELP_PAGE_STATION_BLACKLIST = GITHUB_WIKI + "Station-Blacklist"; + public static final String HELP_PAGE_STATION_TAGS = GITHUB_WIKI + "Station-Tags"; + public static final String HELP_PAGE_TRAIN_BLACKLIST = GITHUB_WIKI + "Train-Blacklist"; + public static final String HELP_PAGE_TRAIN_GROUPS = GITHUB_WIKI + "Train-Groups"; + public static final String HELP_PAGE_TRAIN_INITIALIZATION = GITHUB_WIKI + "Train-Imnitialization"; + public static final String HELP_PAGE_TRAIN_LINES = GITHUB_WIKI + "Train-Lines"; } diff --git a/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java b/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java index 94629602..0aaca184 100644 --- a/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java +++ b/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java @@ -8,33 +8,23 @@ import com.simibubi.create.foundation.item.TooltipHelper.Palette; import de.mrjulsen.crn.block.AdvancedDisplayBlock; -import de.mrjulsen.crn.event.ClientEvents; -import de.mrjulsen.crn.event.ModEvents; +import de.mrjulsen.crn.event.CRNClientEventsRegistryEvent; +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.ModClientEvents; +import de.mrjulsen.crn.event.ModCommonEvents; import de.mrjulsen.crn.network.packets.cts.AdvancedDisplayUpdatePacket; -import de.mrjulsen.crn.network.packets.cts.GlobalSettingsRequestPacket; -import de.mrjulsen.crn.network.packets.cts.GlobalSettingsUpdatePacket; -import de.mrjulsen.crn.network.packets.cts.NavigationRequestPacket; -import de.mrjulsen.crn.network.packets.cts.NearestStationRequestPacket; -import de.mrjulsen.crn.network.packets.cts.NextConnectionsRequestPacket; -import de.mrjulsen.crn.network.packets.cts.RealtimeRequestPacket; -import de.mrjulsen.crn.network.packets.cts.TrackStationsRequestPacket; -import de.mrjulsen.crn.network.packets.cts.TrainDataRequestPacket; -import de.mrjulsen.crn.network.packets.stc.GlobalSettingsResponsePacket; -import de.mrjulsen.crn.network.packets.stc.NavigationResponsePacket; -import de.mrjulsen.crn.network.packets.stc.NearestStationResponsePacket; -import de.mrjulsen.crn.network.packets.stc.NextConnectionsResponsePacket; -import de.mrjulsen.crn.network.packets.stc.RealtimeResponsePacket; import de.mrjulsen.crn.network.packets.stc.ServerErrorPacket; -import de.mrjulsen.crn.network.packets.stc.TimeCorrectionPacket; -import de.mrjulsen.crn.network.packets.stc.TrackStationResponsePacket; -import de.mrjulsen.crn.network.packets.stc.TrainDataResponsePacket; +import de.mrjulsen.crn.registry.ModAccessorTypes; import de.mrjulsen.crn.registry.ModBlockEntities; import de.mrjulsen.crn.registry.ModBlocks; +import de.mrjulsen.crn.registry.ModDisplayTypes; import de.mrjulsen.crn.registry.ModExtras; import de.mrjulsen.crn.registry.ModItems; +import de.mrjulsen.crn.registry.ModSchedule; +import de.mrjulsen.crn.registry.ModTrainStatusInfos; import de.mrjulsen.mcdragonlib.net.NetworkManagerBase; import dev.architectury.platform.Platform; -import dev.architectury.utils.Env; +import net.fabricmc.api.EnvType; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; @@ -47,7 +37,11 @@ public final class CreateRailwaysNavigator { public static final String MOD_ID = "createrailwaysnavigator"; + public static final String SHORT_MOD_ID = "crn"; public static final Logger LOGGER = LogUtils.getLogger(); + + public static final String DISCORD = "https://discord.gg/hH7YxTrPpk"; + public static final String GITHUB = "https://github.com/MisterJulsen/Create-Train-Navigator"; public static final CreateRegistrate REGISTRATE = CreateRegistrate.create(MOD_ID); @@ -69,52 +63,48 @@ public static KineticStats create(Item item) { return null; } - - private static NetworkManagerBase crnNet; + + public static void load() {} public static void init() { - ModBlocks.register(); - ModItems.register(); - ModBlockEntities.register(); - ModExtras.register(); + ModBlocks.init(); + ModItems.init(); + ModBlockEntities.init(); + ModExtras.init(); + ModSchedule.init(); + ModAccessorTypes.init(); + ModTrainStatusInfos.init(); + ModDisplayTypes.init(); crnNet = new NetworkManagerBase(MOD_ID, "crn_network", List.of( // cts - GlobalSettingsRequestPacket.class, - GlobalSettingsUpdatePacket.class, - NavigationRequestPacket.class, - NearestStationRequestPacket.class, - NextConnectionsRequestPacket.class, - RealtimeRequestPacket.class, - TrackStationsRequestPacket.class, - TrainDataRequestPacket.class, AdvancedDisplayUpdatePacket.class, // stc - GlobalSettingsResponsePacket.class, - NavigationResponsePacket.class, - NearestStationResponsePacket.class, - NextConnectionsResponsePacket.class, - RealtimeResponsePacket.class, - ServerErrorPacket.class, - TrackStationResponsePacket.class, - TrainDataResponsePacket.class, - TimeCorrectionPacket.class + ServerErrorPacket.class )); CRNPlatformSpecific.registerConfig(); - ModEvents.init(); - if (Platform.getEnvironment() == Env.CLIENT) { - ClientEvents.init(); + ModCommonEvents.init(); + if (Platform.getEnv() == EnvType.CLIENT) { + ModClientEvents.init(); } + + CRNEventsManager.getEvent(CRNClientEventsRegistryEvent.class).register(MOD_ID, () -> { + }); + } public static NetworkManagerBase net() { return crnNet; } + + public static boolean isDebug() { + return Platform.isDevelopmentEnvironment(); + } } diff --git a/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java index 5a70faf5..3b44a902 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java @@ -8,10 +8,10 @@ import com.simibubi.create.foundation.block.IBE; import com.simibubi.create.foundation.utility.Iterate; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.registry.ModBlockEntities; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; import net.minecraft.core.BlockPos; @@ -46,6 +46,7 @@ import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.DirectionProperty; import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.material.MaterialColor; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.ticks.LevelTickAccess; @@ -57,7 +58,7 @@ public abstract class AbstractAdvancedDisplayBlock extends Block implements IWre public static final BooleanProperty DOWN = BooleanProperty.create("down"); public AbstractAdvancedDisplayBlock(Properties properties) { - super(properties); + super(properties.color(MaterialColor.METAL)); this.registerDefaultState(this.stateDefinition.any() .setValue(UP, false) @@ -157,7 +158,7 @@ public void onPlace(BlockState pState, Level pLevel, BlockPos pPos, BlockState p updateNeighbours(pState, pLevel, pPos); if (pLevel.isClientSide) { - withBlockEntityDo(pLevel, pPos, be -> be.getController().getRenderer().update(pLevel, pPos, pState, be, EUpdateReason.BLOCK_CHANGED)); + withBlockEntityDo(pLevel, pPos, be -> be.getController().getRenderer().update(pLevel, pPos, pState, be, EUpdateReason.LAYOUT_CHANGED)); } } @@ -312,7 +313,7 @@ public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Pla }); if (pLevel.isClientSide) { - blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, EUpdateReason.BLOCK_CHANGED); + blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, EUpdateReason.LAYOUT_CHANGED); } return InteractionResult.SUCCESS; @@ -327,7 +328,7 @@ public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Pla }); if (pLevel.isClientSide) { - blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, EUpdateReason.BLOCK_CHANGED); + blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, EUpdateReason.LAYOUT_CHANGED); } return InteractionResult.SUCCESS; diff --git a/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedSidedDisplayBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedSidedDisplayBlock.java index 1e2967c8..66866580 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedSidedDisplayBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedSidedDisplayBlock.java @@ -1,6 +1,6 @@ package de.mrjulsen.crn.block; -import de.mrjulsen.crn.data.ESide; +import de.mrjulsen.crn.block.properties.ESide; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.item.context.BlockPlaceContext; diff --git a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBlock.java index b7ae7966..12bc4e81 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBlock.java @@ -32,6 +32,7 @@ public Tripple getRenderRotation(Level level, BlockState bl return Tripple.of(0.0F, 0.0F, 0.0F); } + @Override public boolean isSingleLined() { return false; diff --git a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBoardBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBoardBlock.java index cd0111ad..e3b8945b 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBoardBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayBoardBlock.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import de.mrjulsen.crn.data.EBlockAlignment; +import de.mrjulsen.crn.block.properties.EBlockAlignment; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; import net.minecraft.core.BlockPos; diff --git a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayHalfPanelBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayHalfPanelBlock.java index d7ec9408..079bb8e5 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayHalfPanelBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayHalfPanelBlock.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import de.mrjulsen.crn.data.EBlockAlignment; +import de.mrjulsen.crn.block.properties.EBlockAlignment; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; import net.minecraft.core.BlockPos; @@ -82,7 +82,7 @@ public class AdvancedDisplayHalfPanelBlock extends AbstractAdvancedSidedDisplayB public AdvancedDisplayHalfPanelBlock(Properties properties) { super(properties); - registerDefaultState(defaultBlockState() + registerDefaultState(defaultBlockState() .setValue(Y_ALIGN, EBlockAlignment.CENTER) .setValue(Z_ALIGN, EBlockAlignment.CENTER) ); diff --git a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayPanelBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayPanelBlock.java index ac2094ef..db351e01 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayPanelBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplayPanelBlock.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import de.mrjulsen.crn.data.EBlockAlignment; +import de.mrjulsen.crn.block.properties.EBlockAlignment; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; import net.minecraft.core.BlockPos; diff --git a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySlabBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySlabBlock.java new file mode 100644 index 00000000..9449ad8a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySlabBlock.java @@ -0,0 +1,166 @@ +package de.mrjulsen.crn.block; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import de.mrjulsen.crn.block.properties.EBlockAlignment; +import de.mrjulsen.mcdragonlib.data.Pair; +import de.mrjulsen.mcdragonlib.data.Tripple; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition.Builder; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class AdvancedDisplaySlabBlock extends AbstractAdvancedSidedDisplayBlock { + + public static final EnumProperty Y_ALIGN = EnumProperty.create("y_alignment", EBlockAlignment.class); + + private static final Map SHAPES = Map.ofEntries( + Map.entry(new ShapeKey(Direction.SOUTH, EBlockAlignment.NEGATIVE), Block.box(0 , 0 , 0 , 16, 8 , 16)), + Map.entry(new ShapeKey(Direction.NORTH, EBlockAlignment.NEGATIVE), Block.box(0 , 0 , 0 , 16, 8 , 16)), + Map.entry(new ShapeKey(Direction.EAST, EBlockAlignment.NEGATIVE), Block.box(0 , 0 , 0 , 16, 8 , 16)), + Map.entry(new ShapeKey(Direction.WEST, EBlockAlignment.NEGATIVE), Block.box(0 , 0 , 0 , 16, 8 , 16)), + + Map.entry(new ShapeKey(Direction.SOUTH, EBlockAlignment.CENTER), Block.box(0 , 4 , 0 , 16, 12, 16)), + Map.entry(new ShapeKey(Direction.NORTH, EBlockAlignment.CENTER), Block.box(0 , 4 , 0 , 16, 12, 16)), + Map.entry(new ShapeKey(Direction.EAST, EBlockAlignment.CENTER), Block.box(0 , 4 , 0 , 16, 12, 16)), + Map.entry(new ShapeKey(Direction.WEST, EBlockAlignment.CENTER), Block.box(0 , 4 , 0 , 16, 12, 16)), + + Map.entry(new ShapeKey(Direction.SOUTH, EBlockAlignment.POSITIVE), Block.box(0 , 8 , 0 , 16, 16, 16)), + Map.entry(new ShapeKey(Direction.NORTH, EBlockAlignment.POSITIVE), Block.box(0 , 8 , 0 , 16, 16, 16)), + Map.entry(new ShapeKey(Direction.EAST, EBlockAlignment.POSITIVE), Block.box(0 , 8 , 0 , 16, 16, 16)), + Map.entry(new ShapeKey(Direction.WEST, EBlockAlignment.POSITIVE), Block.box(0 , 8 , 0 , 16, 16, 16)) + ); + + public AdvancedDisplaySlabBlock(Properties properties) { + super(properties); + registerDefaultState(defaultBlockState() + .setValue(Y_ALIGN, EBlockAlignment.CENTER) + ); + } + + @Override + public Collection> getExcludedProperties() { + return List.of(Y_ALIGN); + } + + @Override + public VoxelShape getShape(BlockState pState, BlockGetter pLevel, BlockPos pPos, CollisionContext pContext) { + return SHAPES.get(new ShapeKey(pState.getValue(FACING), pState.getValue(Y_ALIGN))); + } + + @Override + public BlockState getDefaultPlacementState(BlockPlaceContext context, BlockState state, BlockState other) { + BlockState stateForPlacement = super.getDefaultPlacementState(context, state, other); + Direction direction = context.getClickedFace(); + + EBlockAlignment yAlign = EBlockAlignment.CENTER; + + if (direction == Direction.UP || (context.getClickLocation().y - context.getClickedPos().getY() < 0.33333333D)) { + yAlign = EBlockAlignment.NEGATIVE; + } else if (direction == Direction.DOWN || (context.getClickLocation().y - context.getClickedPos().getY() > 0.66666666D)) { + yAlign = EBlockAlignment.POSITIVE; + } + + return stateForPlacement + .setValue(Y_ALIGN, yAlign) + ; + } + + @Override + public BlockState appendOnPlace(BlockPlaceContext context, BlockState state, BlockState other) { + return super.appendOnPlace(context, state, other) + .setValue(Y_ALIGN, other.getValue(Y_ALIGN)) + ; + } + + @Override + protected void createBlockStateDefinition(Builder pBuilder) { + super.createBlockStateDefinition(pBuilder.add(Y_ALIGN)); + } + + @Override + public boolean canConnectWithBlock(BlockGetter level, BlockState selfState, BlockState otherState) { + return super.canConnectWithBlock(level, selfState, otherState) && + selfState.getValue(Y_ALIGN) == otherState.getValue(Y_ALIGN) + ; + } + + @Override + protected boolean canConnect(LevelAccessor level, BlockPos pos, BlockState state, BlockState other) { + return super.canConnect(level, pos, state, other) && + state.getValue(Y_ALIGN) == other.getValue(Y_ALIGN) + ; + } + + @Override + public Pair getRenderAspectRatio(Level level, BlockState blockState, BlockPos pos) { + return Pair.of(1.0F, 0.5F); + } + + @Override + public Pair getRenderOffset(Level level, BlockState blockState, BlockPos pos) { + float y; + switch (blockState.getValue(Y_ALIGN)) { + case NEGATIVE: + y = 8.0f; + break; + case POSITIVE: + y = 0.0f; + break; + default: + y = 4.0f; + break; + } + return Pair.of(0.0f, y); + } + + @Override + public Pair getRenderZOffset(Level level, BlockState blockState, BlockPos pos) { + return Pair.of(16.05f, 16.05f); + } + + @Override + public Tripple getRenderRotation(Level level, BlockState blockState, BlockPos pos) { + return Tripple.of(0.0F, 0.0F, 0.0F); + } + + @Override + public boolean isSingleLined() { + return true; + } + + private static final class ShapeKey { + private final Direction facing; + private final EBlockAlignment yAlign; + + public ShapeKey(Direction facing, EBlockAlignment yAlign) { + this.facing = facing; + this.yAlign = yAlign; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ShapeKey other) { + return facing == other.facing && yAlign == other.yAlign; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(facing, yAlign); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySmallBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySmallBlock.java index b44d397e..33558e7f 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySmallBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AdvancedDisplaySmallBlock.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import de.mrjulsen.crn.data.EBlockAlignment; +import de.mrjulsen.crn.block.properties.EBlockAlignment; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; import net.minecraft.core.BlockPos; diff --git a/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java b/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java index d0726a36..2f2ee6e6 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java @@ -3,11 +3,10 @@ import com.simibubi.create.content.equipment.wrench.IWrenchable; import com.simibubi.create.foundation.block.IBE; -import de.mrjulsen.crn.block.be.TrainStationClockBlockEntity; +import de.mrjulsen.crn.block.blockentity.TrainStationClockBlockEntity; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.registry.ModBlockEntities; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; import net.minecraft.core.BlockPos; @@ -65,7 +64,7 @@ public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Pla blockEntity.setColor(dye == DyeColor.ORANGE ? 0xFF9900 : dye.getMaterialColor().col); if (pLevel.isClientSide) { - blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, EUpdateReason.BLOCK_CHANGED); + blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, null); } return InteractionResult.SUCCESS; @@ -77,7 +76,7 @@ public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Pla blockEntity.setGlowing(true); if (pLevel.isClientSide) { - blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, EUpdateReason.BLOCK_CHANGED); + blockEntity.getRenderer().update(pLevel, pPos, pState, blockEntity, null); } return InteractionResult.SUCCESS; diff --git a/common/src/main/java/de/mrjulsen/crn/block/be/AdvancedDisplayBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java similarity index 70% rename from common/src/main/java/de/mrjulsen/crn/block/be/AdvancedDisplayBlockEntity.java rename to common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java index 0c94b00d..458f4f6d 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/be/AdvancedDisplayBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java @@ -1,7 +1,6 @@ -package de.mrjulsen.crn.block.be; +package de.mrjulsen.crn.block.blockentity; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import com.simibubi.create.content.trains.display.FlapDisplayBlock; @@ -11,34 +10,36 @@ import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour; import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.AbstractAdvancedDisplayBlock; import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.block.properties.EDisplayInfo; +import de.mrjulsen.crn.block.properties.EDisplayType; +import de.mrjulsen.crn.block.properties.EDisplayType.EDisplayTypeDataSource; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeInfo; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; import de.mrjulsen.crn.data.CarriageData; -import de.mrjulsen.crn.data.EDisplayInfo; -import de.mrjulsen.crn.data.EDisplayType; -import de.mrjulsen.crn.data.IBlockEntitySerializable; -import de.mrjulsen.crn.data.DeparturePrediction.TrainExitSide; -import de.mrjulsen.crn.data.EDisplayType.EDisplayTypeDataSource; -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.TrainDataRequestPacket; -import de.mrjulsen.crn.network.packets.cts.TrainDataRequestPacket.TrainData; +import de.mrjulsen.crn.data.TrainExitSide; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.data.train.portable.StationDisplayData; +import de.mrjulsen.crn.data.train.portable.TrainDisplayData; +import de.mrjulsen.crn.data.train.portable.TrainStopDisplayData; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.crn.registry.ModDisplayTypes; import de.mrjulsen.mcdragonlib.block.IBERInstance; import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; import de.mrjulsen.mcdragonlib.data.Cache; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; +import de.mrjulsen.mcdragonlib.util.ListUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos.MutableBlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.network.Connection; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; @@ -55,7 +56,6 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements IMultiblockBlockEntity, IContraptionBlockEntity, IBERInstance, - IBlockEntitySerializable, IColorableBlockEntity { private static final String NBT_XSIZE = "XSize"; @@ -63,15 +63,15 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements private static final String NBT_CONTROLLER = "IsController"; private static final String NBT_COLOR = "Color"; private static final String NBT_GLOWING = "Glowing"; - private static final String NBT_INFO_TYPE = "InfoType"; - private static final String NBT_DISPLAY_TYPE = "DisplayType"; - private static final String NBT_PREDICTIONS = "Predictions"; - private static final String NBT_NEXT_DEPARTURE_STOPOVERS = "NextStopovers"; + private static final String NBT_DISPLAY_TYPE_KEY = "DisplayTypeKey"; private static final String NBT_FILTER = "Filter"; private static final String NBT_LAST_REFRESH_TIME = "LastRefreshed"; private static final String NBT_PLATFORM_WIDTH = "PlatformWidth"; private static final String NBT_TRAIN_NAME_WIDTH = "TrainNameWidth"; private static final String NBT_TIME_DISPLAY = "TimeDisplay"; + private static final String NBT_TRAIN_STOPS = "TrainStops"; + @Deprecated private static final String LEGACY_NBT_INFO_TYPE = "InfoType"; + @Deprecated private static final String LEGACY_NBT_DISPLAY_TYPE = "DisplayType"; public static final byte MAX_XSIZE = 16; public static final byte MAX_YSIZE = 16; @@ -82,8 +82,8 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements private byte xSize = 1; private byte ySize = 1; private boolean isController; - private List predictions; - private List nextDepartureStopovers; + private List predictions; + private boolean dataOrderChanged = false; private String stationNameFilter; private StationInfo stationInfo; private byte trainNameWidth; @@ -93,13 +93,12 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements // USER SETTINGS private int color = DyeColor.WHITE.getTextColor(); private boolean glowing = false; - private EDisplayInfo infoType = EDisplayInfo.SIMPLE; - private EDisplayType displayType = EDisplayType.TRAIN_DESTINATION; + private DisplayTypeResourceKey displayTypeKey = ModDisplayTypes.TRAIN_DESTINATION_SIMPLE; // CLIENT DISPLAY ONLY - this data is not saved! private long lastRefreshedTime; - private TrainData trainData = TrainData.empty(false); + private TrainDisplayData trainData = TrainDisplayData.empty(); private CarriageData carriageData = new CarriageData(0, Direction.NORTH, false); // OTHER @@ -110,7 +109,7 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements if (getCarriageData() == null || !getTrainData().getNextStop().isPresent() || !(getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock)) { return TrainExitSide.UNKNOWN; } - TrainExitSide side = getTrainData().getNextStop().get().exitSide(); + TrainExitSide side = getTrainData().getNextStopExitSide(); Direction blockFacing = getBlockState().getValue(HorizontalDirectionalBlock.FACING); if (!carriageData.isOppositeDirection()) { blockFacing = blockFacing.getOpposite(); @@ -166,7 +165,7 @@ public AdvancedDisplayBlockEntity(BlockEntityType type, BlockPos pos, BlockSt reset(); } - public TrainData getTrainData() { + public TrainDisplayData getTrainData() { return trainData; } @@ -219,13 +218,9 @@ public int getColor() { public boolean isGlowing() { return glowing; } - - public EDisplayInfo getInfoType() { - return infoType; - } - public EDisplayType getDisplayType() { - return displayType; + public DisplayTypeResourceKey getDisplayTypeKey() { + return displayTypeKey; } @Override @@ -258,19 +253,15 @@ public Class getBlockEntityType() { return AdvancedDisplayBlockEntity.class; } - public List getPredictions() { + public List getStops() { return predictions; } - public List getNextDepartureStopovers() { - return nextDepartureStopovers; - } - public boolean isPlatformFixed() { return !stationNameFilter.contains("*"); } - public StationInfo getStationInfo() { + public StationInfo getStationInfo2() { return stationInfo; } @@ -280,31 +271,19 @@ public String getStationNameFilter() { public boolean isSingleLine() { if (getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock block) { - return block.isSingleLined() || !( - (getDisplayType() == EDisplayType.PASSENGER_INFORMATION && getInfoType() == EDisplayInfo.INFORMATIVE) || - (getDisplayType() == EDisplayType.PLATFORM && getInfoType() == EDisplayInfo.DETAILED) || - (getDisplayType() == EDisplayType.PLATFORM && getInfoType() == EDisplayInfo.INFORMATIVE) - ); + return block.isSingleLined() || AdvancedDisplaysRegistry.getInfo(displayTypeKey).singleLined(); } return false; } - public int getPlatformInfoLinesCount() { - switch (getInfoType()) { - default: - case SIMPLE: - return 32; - case DETAILED: - return getYSize() * 3 - 1; - case INFORMATIVE: - return getYSize() * 3 - 2; - } + public DisplayTypeInfo getDisplayTypeInfo() { + return AdvancedDisplaysRegistry.getInfo(displayTypeKey); } public void setColor(int color) { this.color = color; if (level.isClientSide) { - getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.BLOCK_CHANGED); + getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.LAYOUT_CHANGED); } } @@ -312,34 +291,29 @@ public void setColor(int color) { public void setGlowing(boolean glowing) { this.glowing = glowing; if (level.isClientSide) { - getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.BLOCK_CHANGED); + getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.LAYOUT_CHANGED); } } - public void setInfoType(EDisplayInfo type) { - this.infoType = type; + public void setDisplayTypeKey(DisplayTypeResourceKey key) { + this.displayTypeKey = key; if (level.isClientSide) { - getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.BLOCK_CHANGED); + getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.LAYOUT_CHANGED); } } - public void setDisplayType(EDisplayType type) { - this.displayType = type; - if (level.isClientSide) { - getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.BLOCK_CHANGED); - } - } - - public void setDepartureData(List predictions, List nextDepartureStopovers, String stationNameFilter, StationInfo staionInfo, long lastRefreshedTime, byte platformWidth, byte trainNameWidth, byte timeDisplayId) { - this.predictions = predictions.stream().sorted(Comparator.comparingInt(x -> x.departureTicks())).toList(); + public void setDepartureData(List predictions, String stationNameFilter, StationInfo staionInfo, long lastRefreshedTime, byte platformWidth, byte trainNameWidth, byte timeDisplayId) { + this.dataOrderChanged = dataOrderChanged || !ListUtils.compareCollections(this.predictions, predictions, StationDisplayData::equals); + + this.predictions = predictions; this.stationNameFilter = stationNameFilter; this.stationInfo = staionInfo; - this.nextDepartureStopovers = nextDepartureStopovers; this.lastRefreshedTime = lastRefreshedTime; this.platformWidth = platformWidth; this.trainNameWidth = trainNameWidth; this.timeDisplay = ETimeDisplay.getById(timeDisplayId); + } @Override @@ -350,8 +324,7 @@ public boolean connectable(BlockGetter getter, BlockPos a, BlockPos b) { if (getter.getBlockEntity(a) instanceof AdvancedDisplayBlockEntity be1 && getter.getBlockEntity(b) instanceof AdvancedDisplayBlockEntity be2 && be1.getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock block1 && be2.getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock block2) { return block1 == block2 && - be1.getDisplayType() == be2.getDisplayType() && - be1.getInfoType() == be2.getInfoType() && + be1.getDisplayTypeKey().equals(be2.getDisplayTypeKey()) && block1.canConnectWithBlock(getter, getter.getBlockState(a), getter.getBlockState(b)) && block2.canConnectWithBlock(getter, getter.getBlockState(b), getter.getBlockState(a)) && (!a.above().equals(b) || (be1.getBlockState().getValue(AbstractAdvancedDisplayBlock.UP) && !be1.isSingleLine())) && (!a.below().equals(b) || (be1.getBlockState().getValue(AbstractAdvancedDisplayBlock.DOWN) && !be1.isSingleLine())) @@ -402,8 +375,7 @@ public AdvancedDisplayBlockEntity getController() { public void copyFrom(AdvancedDisplayBlockEntity other) { if (getColor() == other.getColor() && - getInfoType() == other.getInfoType() && - getDisplayType() == other.getDisplayType() && + getDisplayTypeKey().equals(other.getDisplayTypeKey()) && isGlowing() == other.isGlowing() ) { return; @@ -411,17 +383,17 @@ public void copyFrom(AdvancedDisplayBlockEntity other) { color = other.getColor(); glowing = other.isGlowing(); - displayType = other.getDisplayType(); - infoType = other.getInfoType(); + displayTypeKey = other.getDisplayTypeKey(); notifyUpdate(); } public void reset() { + dataOrderChanged = true; + predictions = List.of(); - nextDepartureStopovers = List.of(); stationNameFilter = ""; platformWidth = -1; - trainNameWidth = 16; + trainNameWidth = 14; xSize = 1; ySize = 1; isController = false; @@ -495,17 +467,17 @@ public void tick() { super.tick(); - if (getDisplayType().getSource() != EDisplayTypeDataSource.PLATFORM) { + if (getDisplayTypeKey().category().getSource() != EDisplayTypeDataSource.PLATFORM) { return; } - syncTicks++; + syncTicks++; if ((syncTicks %= REFRESH_FREQUENCY) == 0) { - if (level.isClientSide) { - boolean shouldUpdate = getPredictions().size() > 0; - + if (level.isClientSide) { + boolean shouldUpdate = getStops().size() > 0 || dataOrderChanged; if (shouldUpdate) { - getRenderer().update(level, getBlockPos(), getBlockState(), this, EUpdateReason.DATA_CHANGED); + getRenderer().update(level, getBlockPos(), getBlockState(), this, dataOrderChanged ? EUpdateReason.LAYOUT_CHANGED : EUpdateReason.DATA_CHANGED); + dataOrderChanged = false; } } } @@ -525,41 +497,42 @@ public void contraptionTick(Level level, BlockPos pos, BlockState state, Carriag return; } - if (getDisplayType().getSource() != EDisplayTypeDataSource.TRAIN_INFORMATION) { + if (getDisplayTypeKey().category().getSource() != EDisplayTypeDataSource.TRAIN_INFORMATION) { return; } syncTicks++; - if ((syncTicks %= 100) == 0) { - long id = InstanceManager.registerClientTrainDataResponseAction((data, refreshTime) -> { - if (data == null) { + if ((syncTicks %= 100) == 0 && level.isClientSide) { + DataAccessor.getFromServer(((CarriageContraptionEntity)carriage.entity).trainId, ModAccessorTypes.GET_TRAIN_DISPLAY_DATA, (data) -> { + if (data.isEmpty() && this.trainData.isEmpty()) { return; } + boolean shouldUpdate = false; - if (trainData != null && trainData.getNextStop().isPresent() && data.getNextStop().isPresent()) { - SimpleDeparturePrediction prediction = trainData.getNextStop().get(); - - shouldUpdate = !trainData.trainName().equals(data.trainName()) || - !prediction.scheduleTitle().equals(data.predictions().get(0).scheduleTitle()) || - !prediction.stationTagName().equals(data.predictions().get(0).stationTagName()) || - trainData.getNextStop().get().exitSide() != data.getNextStop().get().exitSide() || - (getInfoType() == EDisplayInfo.INFORMATIVE && getDisplayType() == EDisplayType.PASSENGER_INFORMATION && trainData.getNextStop().get().departureTicks() + lastRefreshedTime != data.getNextStop().get().departureTicks() + refreshTime) // It's not clean but it works ... for now + if (this.trainData != null && this.trainData.getNextStop().isPresent() && data.getNextStop().isPresent()) { + TrainStopDisplayData prediction = this.trainData.getNextStop().get(); + + shouldUpdate = !this.trainData.getTrainData().getName().equals(data.getTrainData().getName()) || + !prediction.getDestination().equals(data.getNextStop().get().getDestination()) || + prediction.getStationEntryIndex() != data.getNextStop().get().getStationEntryIndex() || + this.trainData.getNextStopExitSide() != data.getNextStopExitSide() || + this.trainData.isWaitingAtStation() != data.isWaitingAtStation() + //(getInfoType() == EDisplayInfo.INFORMATIVE && getDisplayType() == EDisplayType.PASSENGER_INFORMATION && trainData.getNextStop().get().departureTicks() + lastRefreshedTime != data.getNextStop().get().departureTicks() + refreshTime) // It's not clean but it works ... for now ; } - boolean oos = trainData != null && !trainData.trainId().equals(Constants.ZERO_UUID) && !data.getNextStop().isPresent(); - if (oos) { + boolean outOfService = this.trainData != null && !this.trainData.getTrainData().getId().equals(Constants.ZERO_UUID) && !data.getNextStop().isPresent(); + if (outOfService) { shouldUpdate = true; } - this.lastRefreshedTime = refreshTime; - this.trainData = oos ? TrainData.empty(true) : data; + //this.lastRefreshedTime = refreshTime; + this.trainData = outOfService ? TrainDisplayData.empty() : data; this.carriageData = new CarriageData(((CarriageContraptionEntity)carriage.entity).carriageIndex, carriage.getAssemblyDirection(), data.isOppositeDirection()); this.relativeExitDirection.clear(); if (shouldUpdate) { - getRenderer().update(level, pos, state, this, EUpdateReason.DATA_CHANGED); } + getRenderer().update(level, pos, state, this, shouldUpdate ? EUpdateReason.LAYOUT_CHANGED : EUpdateReason.DATA_CHANGED); }); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new TrainDataRequestPacket(id, ((CarriageContraptionEntity)carriage.entity).trainId, true)); } } @@ -570,8 +543,7 @@ protected void write(CompoundTag pTag, boolean clientPacket) { pTag.putByte(NBT_YSIZE, getYSize()); pTag.putInt(NBT_COLOR, getColor()); pTag.putBoolean(NBT_CONTROLLER, isController()); - pTag.putInt(NBT_INFO_TYPE, getInfoType().getId()); - pTag.putInt(NBT_DISPLAY_TYPE, getDisplayType().getId()); + pTag.put(NBT_DISPLAY_TYPE_KEY, getDisplayTypeKey().toNbt()); pTag.putString(NBT_FILTER, getStationNameFilter()); pTag.putBoolean(NBT_GLOWING, isGlowing()); pTag.putLong(NBT_LAST_REFRESH_TIME, getLastRefreshedTime()); @@ -579,21 +551,16 @@ protected void write(CompoundTag pTag, boolean clientPacket) { pTag.putByte(NBT_TRAIN_NAME_WIDTH, getTrainNameWidth()); pTag.putByte(NBT_TIME_DISPLAY, getTimeDisplay().getId()); - getStationInfo().writeNbt(pTag); + getStationInfo2().writeNbt(pTag); - if (getPredictions() != null && !getPredictions().isEmpty()) { + if (getStops() != null && !getStops().isEmpty()) { ListTag list = new ListTag(); - list.addAll(getPredictions().stream().map(x -> x.toNbt()).toList()); - pTag.put(NBT_PREDICTIONS, list); - } - - if (getNextDepartureStopovers() != null && !getNextDepartureStopovers().isEmpty()) { - ListTag stopovers = new ListTag(); - stopovers.addAll(getNextDepartureStopovers().stream().map(x -> StringTag.valueOf(x)).toList()); - pTag.put(NBT_NEXT_DEPARTURE_STOPOVERS, stopovers); + list.addAll(getStops().stream().map(x -> x.toNbt()).toList()); + pTag.put(NBT_TRAIN_STOPS, list); } } + @SuppressWarnings("deprecation") @Override public void read(CompoundTag pTag, boolean clientPacket) { boolean updateClient = false; @@ -604,7 +571,7 @@ public void read(CompoundTag pTag, boolean clientPacket) { getYSize() != pTag.getByte(NBT_YSIZE) || getPlatformWidth() != pTag.getByte(NBT_PLATFORM_WIDTH) || getTrainNameWidth() != pTag.getByte(NBT_TRAIN_NAME_WIDTH) || - (getPredictions().isEmpty() ^ !pTag.contains(NBT_PREDICTIONS)) + (getStops().isEmpty() ^ !pTag.contains(NBT_TRAIN_STOPS)) ) { updateClient = true; } @@ -621,11 +588,13 @@ public void read(CompoundTag pTag, boolean clientPacket) { } glowing = pTag.getBoolean(NBT_GLOWING); isController = pTag.getBoolean(NBT_CONTROLLER); - infoType = EDisplayInfo.getTypeById(pTag.getInt(NBT_INFO_TYPE)); - displayType = EDisplayType.getTypeById(pTag.getInt(NBT_DISPLAY_TYPE)); + if (pTag.contains(LEGACY_NBT_INFO_TYPE) && pTag.contains(LEGACY_NBT_DISPLAY_TYPE)) { + displayTypeKey = ModDisplayTypes.legacy_getKeyForType(EDisplayType.getTypeById(pTag.getInt(LEGACY_NBT_DISPLAY_TYPE)), EDisplayInfo.getTypeById(pTag.getInt(LEGACY_NBT_INFO_TYPE))); + } else { + displayTypeKey = DisplayTypeResourceKey.fromNbt(pTag.getCompound(NBT_DISPLAY_TYPE_KEY)); + } setDepartureData( - pTag.contains(NBT_PREDICTIONS) ? new ArrayList<>(pTag.getList(NBT_PREDICTIONS, Tag.TAG_COMPOUND).stream().map(x -> SimpleDeparturePrediction.fromNbt((CompoundTag)x)).toList()) : new ArrayList<>(), - pTag.contains(NBT_NEXT_DEPARTURE_STOPOVERS) ? pTag.getList(NBT_NEXT_DEPARTURE_STOPOVERS, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList() : new ArrayList<>(), + pTag.contains(NBT_TRAIN_STOPS) ? new ArrayList<>(pTag.getList(NBT_TRAIN_STOPS, Tag.TAG_COMPOUND).stream().map(x -> StationDisplayData.fromNbt((CompoundTag)x)).toList()) : new ArrayList<>(), pTag.getString(NBT_FILTER), info, pTag.getLong(NBT_LAST_REFRESH_TIME), @@ -635,22 +604,8 @@ public void read(CompoundTag pTag, boolean clientPacket) { ); if (updateClient) { - getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.BLOCK_CHANGED); + getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.LAYOUT_CHANGED); } - } - - @Override - public CompoundTag serialize() { - CompoundTag nbt = new CompoundTag(); - nbt.putInt(NBT_INFO_TYPE, getInfoType().getId()); - nbt.putInt(NBT_DISPLAY_TYPE, getDisplayType().getId()); - return nbt; - } - - @Override - public void deserialize(CompoundTag nbt) { - infoType = EDisplayInfo.getTypeById(nbt.getInt(NBT_INFO_TYPE)); - displayType = EDisplayType.getTypeById(nbt.getInt(NBT_DISPLAY_TYPE)); } @Override @@ -701,4 +656,9 @@ public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket pkt) { this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 512); } + public static enum EUpdateReason { + LAYOUT_CHANGED, + DATA_CHANGED + } + } \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/block/be/IColorableBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java similarity index 68% rename from common/src/main/java/de/mrjulsen/crn/block/be/IColorableBlockEntity.java rename to common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java index 17b5aaad..3bdc918a 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/be/IColorableBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.block.be; +package de.mrjulsen.crn.block.blockentity; public interface IColorableBlockEntity { int getColor(); diff --git a/common/src/main/java/de/mrjulsen/crn/block/be/IContraptionBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IContraptionBlockEntity.java similarity index 92% rename from common/src/main/java/de/mrjulsen/crn/block/be/IContraptionBlockEntity.java rename to common/src/main/java/de/mrjulsen/crn/block/blockentity/IContraptionBlockEntity.java index 0137773a..3a2b9d3d 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/be/IContraptionBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IContraptionBlockEntity.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.block.be; +package de.mrjulsen.crn.block.blockentity; import com.simibubi.create.content.trains.entity.CarriageContraption; diff --git a/common/src/main/java/de/mrjulsen/crn/block/be/IMultiblockBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IMultiblockBlockEntity.java similarity index 98% rename from common/src/main/java/de/mrjulsen/crn/block/be/IMultiblockBlockEntity.java rename to common/src/main/java/de/mrjulsen/crn/block/blockentity/IMultiblockBlockEntity.java index 2561dc4a..0d77cf9b 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/be/IMultiblockBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IMultiblockBlockEntity.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.block.be; +package de.mrjulsen.crn.block.blockentity; import java.util.function.Consumer; diff --git a/common/src/main/java/de/mrjulsen/crn/block/be/TrainStationClockBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java similarity index 98% rename from common/src/main/java/de/mrjulsen/crn/block/be/TrainStationClockBlockEntity.java rename to common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java index 73f8cebd..c6ddca2d 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/be/TrainStationClockBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.block.be; +package de.mrjulsen.crn.block.blockentity; import java.util.List; diff --git a/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplayCTBehaviour.java b/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplayCTBehaviour.java index c1d2b669..5603fffa 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplayCTBehaviour.java +++ b/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplayCTBehaviour.java @@ -3,7 +3,7 @@ import com.simibubi.create.foundation.block.connected.CTSpriteShiftEntry; import com.simibubi.create.foundation.block.connected.ConnectedTextureBehaviour; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; diff --git a/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplaySmallCTBehaviour.java b/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplaySmallCTBehaviour.java index 599ec36a..1a5c4097 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplaySmallCTBehaviour.java +++ b/common/src/main/java/de/mrjulsen/crn/block/connected/AdvancedDisplaySmallCTBehaviour.java @@ -3,7 +3,7 @@ import com.simibubi.create.foundation.block.connected.CTSpriteShiftEntry; import com.simibubi.create.foundation.block.connected.ConnectedTextureBehaviour; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java index 84b858f9..5be02f7c 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java +++ b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java @@ -8,7 +8,6 @@ import com.simibubi.create.content.redstone.displayLink.source.DisplaySource; import com.simibubi.create.content.redstone.displayLink.target.DisplayTargetStats; import com.simibubi.create.content.trains.station.GlobalStation; -import com.simibubi.create.content.trains.station.StationBlockEntity; import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; import com.simibubi.create.foundation.utility.Lang; diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java index e6cb0193..ac4b0efe 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java +++ b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java @@ -1,24 +1,21 @@ package de.mrjulsen.crn.block.display; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + import com.simibubi.create.content.redstone.displayLink.DisplayLinkContext; import com.simibubi.create.content.redstone.displayLink.target.DisplayBoardTarget; import com.simibubi.create.content.redstone.displayLink.target.DisplayTargetStats; -import com.simibubi.create.content.trains.display.GlobalTrainDisplayData; -import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; -import de.mrjulsen.crn.data.DeparturePrediction; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainStop; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.data.SimpleTrainSchedule; -import de.mrjulsen.crn.data.SimulatedTrainSchedule; -import de.mrjulsen.crn.util.TrainUtils; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.properties.EDisplayType.EDisplayTypeDataSource; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.TrainUtils; +import de.mrjulsen.crn.data.train.portable.StationDisplayData; +import de.mrjulsen.crn.event.ModCommonEvents; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; @@ -27,7 +24,52 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.phys.AABB; -public class AdvancedDisplayTarget extends DisplayBoardTarget { +public class AdvancedDisplayTarget extends DisplayBoardTarget { + + private static boolean running = false; + private static boolean threadRunning = false; + private static final Queue workerTasks = new ConcurrentLinkedQueue<>(); + + public static void start() { + if (running) stop(); + while (running && threadRunning) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) {} + } + + workerTasks.clear(); + running = true; + new Thread(() -> { + threadRunning = true; + CreateRailwaysNavigator.LOGGER.info("Advanced Display Data Manager has been started."); + + while (running) { + while (!workerTasks.isEmpty()) { + try { + workerTasks.poll().run(); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.info("Error while process Advanced Display Data.", e); + } + } + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) {} + } + workerTasks.clear(); + CreateRailwaysNavigator.LOGGER.info("Advanced Display Data Manager has been stopped."); + threadRunning = false; + }, "Advanced Display Data Manager").start(); + } + + public static void stop() { + CreateRailwaysNavigator.LOGGER.info("Stopping Advanced Display Data Manager..."); + running = false; + } + + private static void queueAdvancedDisplayWorkerTask(Runnable task) { + workerTasks.add(task); + } @Override public void acceptFlapText(int line, List> text, DisplayLinkContext context) { @@ -41,58 +83,31 @@ public void acceptFlapText(int line, List> text, DisplayL if (context.getTargetBlockEntity() instanceof AdvancedDisplayBlockEntity blockEntity) { final AdvancedDisplayBlockEntity controller = blockEntity.getController(); - if (controller != null) { - List preds = prepare(filter, controller.getPlatformInfoLinesCount()).stream().map(x -> new DeparturePrediction(x).simplify()).sorted(Comparator.comparingInt(x -> x.departureTicks())).toList(); - List stopovers = new ArrayList<>(); + long dayTime = context.getTargetBlockEntity().getLevel().getDayTime(); - if (!preds.isEmpty()) { - SimpleDeparturePrediction pred = preds.iterator().next(); - Train train = TrainUtils.getTrain(pred.trainId()); + queueAdvancedDisplayWorkerTask(() -> { + if (controller != null & controller.getDisplayTypeKey().category().getSource() == EDisplayTypeDataSource.PLATFORM) { + List preds = prepare(filter, controller.getDisplayTypeInfo().platformDisplayTrainsCount().apply(controller)); - if (train != null) { - SimulatedTrainSchedule sched = SimpleTrainSchedule.of(TrainUtils.getTrainStopsSorted(pred.trainId(), context.blockEntity().getLevel())).simulate(train, 0, pred.stationName()); - - List stops = new ArrayList<>(sched.getAllStops()); - boolean foundStart = false; - - if (!stops.isEmpty()) { - for (int i = 0; i < stops.size() - 1; i++) { - TrainStop x = stops.get(i); - if (foundStart) { - stopovers.add(x.getStationAlias().getAliasName().get()); - } - foundStart = foundStart || x.getPrediction().getStationName().equals(pred.stationName()); - } - } - } + controller.setDepartureData( + preds, + filter, + GlobalSettings.getInstance().getOrCreateStationTagFor(filter).getInfoForStation(filter), + dayTime, + (byte)context.sourceConfig().getInt(AdvancedDisplaySource.NBT_PLATFORM_WIDTH), + (byte)context.sourceConfig().getInt(AdvancedDisplaySource.NBT_TRAIN_NAME_WIDTH), + context.sourceConfig().getByte(AdvancedDisplaySource.NBT_TIME_DISPLAY_TYPE) + ); + if (ModCommonEvents.hasServer()) { + ModCommonEvents.getCurrentServer().get().executeIfPossible(controller::sendData); + } } - - controller.setDepartureData( - preds, - stopovers, - filter, - GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(filter).getInfoForStation(filter), - context.getTargetBlockEntity().getLevel().getDayTime(), - (byte)context.sourceConfig().getInt(AdvancedDisplaySource.NBT_PLATFORM_WIDTH), - (byte)context.sourceConfig().getInt(AdvancedDisplaySource.NBT_TRAIN_NAME_WIDTH), - context.sourceConfig().getByte(AdvancedDisplaySource.NBT_TIME_DISPLAY_TYPE) - ); - controller.sendData(); - } + }); } } - public static List prepare(String filter, int maxLines) { - String regex = filter.isBlank() ? filter : "\\Q" + filter.replace("*", "\\E.*\\Q") + "\\E"; - return new HashMap<>(GlobalTrainDisplayData.statusByDestination).entrySet() - .stream() - .filter(e -> e.getKey() - .matches(regex)) - .flatMap(e -> e.getValue() - .stream()) - .sorted() - .limit(maxLines) - .toList(); + public static List prepare(String filter, int maxLines) { + return TrainUtils.getDeparturesAtStationName(filter, null).stream().limit(maxLines).map(x -> StationDisplayData.of(x)).toList(); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/data/EBlockAlignment.java b/common/src/main/java/de/mrjulsen/crn/block/properties/EBlockAlignment.java similarity index 95% rename from common/src/main/java/de/mrjulsen/crn/data/EBlockAlignment.java rename to common/src/main/java/de/mrjulsen/crn/block/properties/EBlockAlignment.java index 739026c0..43f22aab 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/EBlockAlignment.java +++ b/common/src/main/java/de/mrjulsen/crn/block/properties/EBlockAlignment.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.data; +package de.mrjulsen.crn.block.properties; import java.util.Arrays; diff --git a/common/src/main/java/de/mrjulsen/crn/data/EDisplayInfo.java b/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayInfo.java similarity index 95% rename from common/src/main/java/de/mrjulsen/crn/data/EDisplayInfo.java rename to common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayInfo.java index 17e79271..49cf7270 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/EDisplayInfo.java +++ b/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayInfo.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.data; +package de.mrjulsen.crn.block.properties; import java.util.Arrays; @@ -6,6 +6,7 @@ import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; import net.minecraft.util.StringRepresentable; +@Deprecated public enum EDisplayInfo implements StringRepresentable, ITranslatableEnum { SIMPLE(0, "simple", ModGuiIcons.LESS_DETAILS), DETAILED(1, "detailed", ModGuiIcons.DETAILED), diff --git a/common/src/main/java/de/mrjulsen/crn/data/EDisplayType.java b/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java similarity index 66% rename from common/src/main/java/de/mrjulsen/crn/data/EDisplayType.java rename to common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java index 953daf90..2cb9b15b 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/EDisplayType.java +++ b/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.data; +package de.mrjulsen.crn.block.properties; import java.util.Arrays; @@ -7,16 +7,16 @@ import net.minecraft.util.StringRepresentable; public enum EDisplayType implements StringRepresentable, ITranslatableEnum { - TRAIN_DESTINATION(0, "train_destination", ModGuiIcons.TRAIN_DESTINATION, EDisplayTypeDataSource.TRAIN_INFORMATION), - PASSENGER_INFORMATION(1, "passenger_information", ModGuiIcons.PASSENGER_INFORMATION, EDisplayTypeDataSource.TRAIN_INFORMATION), - PLATFORM(2, "platform", ModGuiIcons.PLATFORM_INFORMATION, EDisplayTypeDataSource.PLATFORM); + TRAIN_DESTINATION((byte)0, "train_destination", ModGuiIcons.TRAIN_DESTINATION, EDisplayTypeDataSource.TRAIN_INFORMATION), + PASSENGER_INFORMATION((byte)1, "passenger_information", ModGuiIcons.PASSENGER_INFORMATION, EDisplayTypeDataSource.TRAIN_INFORMATION), + PLATFORM((byte)2, "platform", ModGuiIcons.PLATFORM_INFORMATION, EDisplayTypeDataSource.PLATFORM); private String name; - private int id; + private byte id; private ModGuiIcons icon; private EDisplayTypeDataSource source; - private EDisplayType(int id, String name, ModGuiIcons icon, EDisplayTypeDataSource source) { + private EDisplayType(byte id, String name, ModGuiIcons icon, EDisplayTypeDataSource source) { this.name = name; this.id = id; this.icon = icon; @@ -27,7 +27,7 @@ public String getInfoTypeName() { return this.name; } - public int getId() { + public byte getId() { return this.id; } @@ -40,7 +40,7 @@ public EDisplayTypeDataSource getSource() { } public static EDisplayType getTypeById(int id) { - return Arrays.stream(values()).filter(x -> x.getId() == id).findFirst().orElse(EDisplayType.TRAIN_DESTINATION); + return Arrays.stream(values()).filter(x -> x.getId() == (byte)id).findFirst().orElse(EDisplayType.TRAIN_DESTINATION); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/data/ESide.java b/common/src/main/java/de/mrjulsen/crn/block/properties/ESide.java similarity index 95% rename from common/src/main/java/de/mrjulsen/crn/data/ESide.java rename to common/src/main/java/de/mrjulsen/crn/block/properties/ESide.java index ea0d6009..45fed558 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/ESide.java +++ b/common/src/main/java/de/mrjulsen/crn/block/properties/ESide.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.data; +package de.mrjulsen.crn.block.properties; import java.util.Arrays; diff --git a/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java b/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java new file mode 100644 index 00000000..fe56c601 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java @@ -0,0 +1,101 @@ +package de.mrjulsen.crn.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.properties.EDisplayType; +import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; +import de.mrjulsen.crn.client.ber.variants.BERError; +import de.mrjulsen.mcdragonlib.data.Pair; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +public final class AdvancedDisplaysRegistry { + public static record DisplayTypeResourceKey(EDisplayType category, ResourceLocation id) { + private static final String NBT_CATEGORY = "Category"; + private static final String NBT_ID = "Id"; + @Override + public final boolean equals(Object arg) { + return arg instanceof DisplayTypeResourceKey o && category == o.category && id.equals(o.id); + } + @Override + public final int hashCode() { + return Objects.hash(category, id); + } + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putByte(NBT_CATEGORY, category().getId()); + nbt.putString(NBT_ID, id().toString()); + return nbt; + } + public static DisplayTypeResourceKey fromNbt(CompoundTag nbt) { + return new DisplayTypeResourceKey(EDisplayType.getTypeById(nbt.getByte(NBT_CATEGORY)), new ResourceLocation(nbt.getString(NBT_ID))); + } + public String getTranslationKey() { + return "display." + CreateRailwaysNavigator.MOD_ID + "." + category().getEnumValueName() + "." + id.getPath(); + } + } + + /** + * @param singleLined Whether the display can be connected vertically. + * @param platformDisplayTrainsCount For Platform Displays only! Specifies how many trains can be shown on the display, depending on the properties of the display. If used correctly, this reduces network traffic, as data about trains that do not fit on the display are not transferred from the server. + */ + public static record DisplayTypeInfo(boolean singleLined, Function platformDisplayTrainsCount) {} + + private static final Map>, DisplayTypeInfo>>> displayTypes = new HashMap<>(); + + /** + * Registers a new display type that can then be used in CRN. + * @param category The display category to which the type should be assigned. + * @param id The id of the display type. + * @param displayRenderer The reference of the Renderer class that renders the contents of the display. + * @param info Additional information about this display type + * @return + */ + public static DisplayTypeResourceKey registerDisplayType(EDisplayType category, ResourceLocation id, Supplier> displayRenderer, DisplayTypeInfo info) { + Map>, DisplayTypeInfo>> reg = displayTypes.computeIfAbsent(category, x -> new HashMap<>()); + if (reg.containsKey(id)) { + throw new IllegalArgumentException("A display type with the id '" + id + "' is already registered!"); + } + reg.put(id, Pair.of(displayRenderer, info)); + return new DisplayTypeResourceKey(category, id); + } + + public static boolean isRegietered(DisplayTypeResourceKey key) { + return key != null && displayTypes.containsKey(key.category()) && displayTypes.get(key.category()).containsKey(key.id()); + } + + public static IBERRenderSubtype getRenderer(DisplayTypeResourceKey key) { + if (!isRegietered(key)) { + return new BERError(); + } + return displayTypes.get(key.category()).get(key.id()).getFirst().get(); + } + + public static DisplayTypeInfo getInfo(DisplayTypeResourceKey key) { + if (!isRegietered(key)) { + return new DisplayTypeInfo(true, $ -> 0); + } + return displayTypes.get(key.category()).get(key.id()).getSecond(); + } + + public static Map getAllOfType(EDisplayType type) { + return displayTypes.get(type).entrySet().stream().collect(Collectors.toMap(a -> a.getKey(), b -> b.getValue().getSecond())); + } + + public static List getAllOfTypeAsKey(EDisplayType type) { + return displayTypes.get(type).entrySet().stream().map(x -> new DisplayTypeResourceKey(type, x.getKey())).toList(); + } + + public static List getAllIdsOfType(EDisplayType type) { + return displayTypes.get(type).entrySet().stream().map(x -> x.getKey()).toList(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/CRNGui.java b/common/src/main/java/de/mrjulsen/crn/client/CRNGui.java new file mode 100644 index 00000000..c6fe395b --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/CRNGui.java @@ -0,0 +1,10 @@ +package de.mrjulsen.crn.client; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import net.minecraft.resources.ResourceLocation; + +public final class CRNGui { + public static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/gui.png"); + public static final int GUI_WIDTH = 64; + public static final int GUI_HEIGHT = 64; +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java b/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java index ff94d66d..7f3637aa 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java @@ -3,27 +3,31 @@ import java.util.List; import java.util.function.Supplier; +import com.mojang.blaze3d.systems.RenderSystem; + import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; -import de.mrjulsen.crn.client.gui.overlay.RouteDetailsOverlayScreen; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.client.gui.NavigatorToast; import de.mrjulsen.crn.client.gui.screen.AdvancedDisplaySettingsScreen; -import de.mrjulsen.crn.client.gui.screen.LoadingScreen; import de.mrjulsen.crn.client.gui.screen.NavigatorScreen; -import de.mrjulsen.crn.client.gui.screen.RouteOverlaySettingsScreen; +import de.mrjulsen.crn.client.gui.screen.TrainDebugScreen; import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; +import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.network.packets.stc.ServerErrorPacket; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.util.TextUtils; import dev.architectury.networking.NetworkManager.PacketContext; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.MultiLineLabel; import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.client.gui.components.toasts.SystemToast.SystemToastIds; import net.minecraft.client.resources.language.ClientLanguage; import net.minecraft.client.resources.language.LanguageInfo; import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; import net.minecraft.world.level.Level; public class ClientWrapper { @@ -31,17 +35,13 @@ public class ClientWrapper { private static ELanguage currentLanguage; private static Language currentClientLanguage; - public static void showNavigatorGui(Level level) { - DLScreen.setScreen(new LoadingScreen()); - GlobalSettingsManager.syncToClient(() -> { - ClientTrainStationSnapshot.syncToClient(() -> { - DLScreen.setScreen(new NavigatorScreen(level)); - }); - }); + public static void showNavigatorGui() { + DLScreen.setScreen(new NavigatorScreen(null)); } - public static void showRouteOverlaySettingsGui(RouteDetailsOverlayScreen overlay) { - DLScreen.setScreen(new RouteOverlaySettingsScreen(overlay)); + @SuppressWarnings("resource") + public static Level getClientLevel() { + return Minecraft.getInstance().level; } public static void handleErrorMessagePacket(ServerErrorPacket packet, Supplier ctx) { @@ -71,4 +71,28 @@ public static void updateLanguage(ELanguage lang, boolean force) { public static Language getCurrentClientLanguage() { return currentClientLanguage == null ? Language.getInstance() : currentClientLanguage; } + + + public static void sendCRNNotification(Component title, Component description) { + if (ModClientConfig.ROUTE_NOTIFICATIONS.get()) { + Minecraft.getInstance().getToasts().addToast(NavigatorToast.multiline(title, description)); + } + } + + public static int renderMultilineLabelSafe(Graphics graphics, int x, int y, Font font, Component text, int maxWidth, int color) { + MultiLineLabel label = MultiLineLabel.create(font, text, maxWidth); + label.renderLeftAlignedNoShadow(graphics.poseStack(), x, y, font.lineHeight, color); + return font.lineHeight * label.getLineCount(); + } + + public static int getTextBlockHeight(Font font, Component text, int maxWidth) { + int lines = font.split(text, maxWidth).size(); + return lines * font.lineHeight; + } + + public static void showTrainDebugScreen() { + RenderSystem.recordRenderCall(() -> { + DLScreen.setScreen(new TrainDebugScreen(null)); + }); + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ModGuiUtils.java b/common/src/main/java/de/mrjulsen/crn/client/ModGuiUtils.java index 0af16142..fc1e2f90 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ModGuiUtils.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ModGuiUtils.java @@ -58,4 +58,12 @@ public static void endStencil() { GL11.glDisable(GL11.GL_STENCIL_TEST); } + public static boolean useWhiteOrBlackForeColor(int color) { + int red = (color >> 16) & 0xFF; + int green = (color >> 8) & 0xFF; + int blue = color & 0xFF; + + double luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255; + return luminance < 0.5; + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java b/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java index 6c2230f7..dc52bc28 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java @@ -1,140 +1,72 @@ package de.mrjulsen.crn.client.ber; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - -import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Vector3f; import de.mrjulsen.crn.block.AbstractAdvancedDisplayBlock; import de.mrjulsen.crn.block.AbstractAdvancedSidedDisplayBlock; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoDetailed; -import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoInformative; -import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoSimple; -import de.mrjulsen.crn.client.ber.variants.BERPlatformDetailed; -import de.mrjulsen.crn.client.ber.variants.BERPlatformInformative; -import de.mrjulsen.crn.client.ber.variants.BERPlatformSimple; -import de.mrjulsen.crn.client.ber.variants.BERRenderSubtypeBase; -import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationDetailed; -import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationInformative; -import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationSimple; -import de.mrjulsen.crn.client.ber.variants.IBERRenderSubtype; -import de.mrjulsen.crn.data.EDisplayInfo; -import de.mrjulsen.crn.data.EDisplayType; -import de.mrjulsen.crn.data.ESide; -import de.mrjulsen.crn.data.EDisplayType.EDisplayTypeDataSource; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.properties.ESide; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; import de.mrjulsen.mcdragonlib.client.ber.AbstractBlockEntityRenderInstance; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; -import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.LightTexture; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; public class AdvancedDisplayRenderInstance extends AbstractBlockEntityRenderInstance { - private Map>>> renderSubtypes; - - public Collection labels; - public BERText carriageIndexLabel; public IBERRenderSubtype renderSubtype; - + private DisplayTypeResourceKey lastType; private int lastXSize = 0; - private EDisplayType lastType; - private EDisplayInfo lastInfo; public AdvancedDisplayRenderInstance(AdvancedDisplayBlockEntity blockEntity) { super(blockEntity); } @Override - protected void preinit(AdvancedDisplayBlockEntity blockEntity) { - this.labels = new ArrayList<>(); - this.renderSubtypes = Map.of( - EDisplayType.TRAIN_DESTINATION, Map.of( - EDisplayInfo.SIMPLE, () -> new BERTrainDestinationSimple(), - EDisplayInfo.DETAILED, () -> new BERTrainDestinationDetailed(), - EDisplayInfo.INFORMATIVE, () -> new BERTrainDestinationInformative() - ), - EDisplayType.PASSENGER_INFORMATION, Map.of( - EDisplayInfo.SIMPLE, () -> new BERPassengerInfoSimple(), - EDisplayInfo.DETAILED, () -> new BERPassengerInfoDetailed(), - EDisplayInfo.INFORMATIVE, () -> new BERPassengerInfoInformative() - ), - EDisplayType.PLATFORM, Map.of( - EDisplayInfo.SIMPLE, () -> new BERPlatformSimple(), - EDisplayInfo.DETAILED, () -> new BERPlatformDetailed(), - EDisplayInfo.INFORMATIVE, () -> new BERPlatformInformative() - ) - ); - } - - public MutableComponent getStopoversString(AdvancedDisplayBlockEntity blockEntity) { - MutableComponent line = TextUtils.empty(); - - List stopovers = blockEntity.getDisplayType().getSource() == EDisplayTypeDataSource.TRAIN_INFORMATION ? - blockEntity.getTrainData().stopovers().stream().map(x -> x.stationTagName()).toList() : - blockEntity.getNextDepartureStopovers(); - - Iterator i = stopovers.iterator(); - boolean isFirst = true; - while (i.hasNext()) { - if (!isFirst) { - line = line.append(TextUtils.text(" ● ")); - } - line = line.append(TextUtils.text(i.next())); - isFirst = false; - } - return line; - } - - @Override - public void render(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay) { + public void render(BERGraphics graphics, float partialTick) { - if (!pBlockEntity.isController()) { + if (!graphics.blockEntity().isController()) { return; } - final int light = pBlockEntity.isGlowing() ? LightTexture.FULL_BRIGHT : pPackedLight; - - if (pBlockEntity.getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock) { - - Tripple rotation = pBlockEntity.renderRotation.get(); - Pair offset = pBlockEntity.renderOffset.get(); - Pair zOffset = pBlockEntity.renderZOffset.get(); - float scale = pBlockEntity.renderScale.get(); + final int light = graphics.blockEntity().isGlowing() ? LightTexture.FULL_BRIGHT : graphics.packedLight(); - pPoseStack.pushPose(); - pPoseStack.translate(offset.getFirst(), offset.getSecond(), zOffset.getFirst()); - pPoseStack.mulPose(Vector3f.XP.rotationDegrees(rotation.getFirst())); - pPoseStack.mulPose(Vector3f.YP.rotationDegrees(rotation.getSecond())); - pPoseStack.mulPose(Vector3f.ZP.rotationDegrees(rotation.getThird())); - pPoseStack.scale(scale, scale, 1); - renderSubtype.renderAdditional(context, pBlockEntity, this, pPartialTicks, pPoseStack, pBufferSource, light, pOverlay, false); - labels.forEach(x -> x.render(pPoseStack, pBufferSource, light)); - pPoseStack.popPose(); + if (graphics.blockEntity().getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock) { - if (!(pBlockEntity.getBlockState().getBlock() instanceof AbstractAdvancedSidedDisplayBlock) || pBlockEntity.getBlockState().getValue(AbstractAdvancedSidedDisplayBlock.SIDE) == ESide.BOTH) { - pPoseStack.pushPose(); - pPoseStack.mulPose(Vector3f.YP.rotationDegrees(180)); - pPoseStack.translate(-pBlockEntity.getXSize() * 16, 0, -16); - pPoseStack.translate(offset.getFirst(), offset.getSecond(), zOffset.getSecond()); - pPoseStack.mulPose(Vector3f.XP.rotationDegrees(rotation.getFirst())); - pPoseStack.mulPose(Vector3f.YP.rotationDegrees(rotation.getSecond())); - pPoseStack.mulPose(Vector3f.ZP.rotationDegrees(rotation.getThird())); - pPoseStack.scale(scale, scale, 1); - renderSubtype.renderAdditional(context, pBlockEntity, this, pPartialTicks, pPoseStack, pBufferSource, light, pOverlay, true); - labels.forEach(x -> x.render(pPoseStack, pBufferSource, light)); - pPoseStack.popPose(); + renderSubtype.renderTick(Minecraft.getInstance().getDeltaFrameTime()); + + Tripple rotation = graphics.blockEntity().renderRotation.get(); + Pair offset = graphics.blockEntity().renderOffset.get(); + Pair zOffset = graphics.blockEntity().renderZOffset.get(); + float scale = graphics.blockEntity().renderScale.get(); + + graphics.poseStack().pushPose(); + graphics.poseStack().translate(offset.getFirst(), offset.getSecond(), zOffset.getFirst()); + graphics.poseStack().mulPose(Vector3f.XP.rotationDegrees(rotation.getFirst())); + graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(rotation.getSecond())); + graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(rotation.getThird())); + graphics.poseStack().scale(scale, scale, 1); + renderSubtype.render(graphics, partialTick, this, light, false); + graphics.poseStack().popPose(); + + if (!(graphics.blockEntity().getBlockState().getBlock() instanceof AbstractAdvancedSidedDisplayBlock) || graphics.blockEntity().getBlockState().getValue(AbstractAdvancedSidedDisplayBlock.SIDE) == ESide.BOTH) { + graphics.poseStack().pushPose(); + graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(180)); + graphics.poseStack().translate(-graphics.blockEntity().getXSize() * 16, 0, -16); + graphics.poseStack().translate(offset.getFirst(), offset.getSecond(), zOffset.getSecond()); + graphics.poseStack().mulPose(Vector3f.XP.rotationDegrees(rotation.getFirst())); + graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(rotation.getSecond())); + graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(rotation.getThird())); + graphics.poseStack().scale(scale, scale, 1); + renderSubtype.render(graphics, partialTick, this, light, true); + graphics.poseStack().popPose(); } } } @@ -142,35 +74,22 @@ public void render(BlockEntityRendererContext context, AdvancedDisplayBlockEntit @Override public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity) { renderSubtype.tick(level, pos, state, blockEntity, this); - labels.forEach(x -> x.tick()); if (blockEntity.getXSizeScaled() != lastXSize) { - update(level, pos, state, blockEntity, EUpdateReason.BLOCK_CHANGED); + update(level, pos, state, blockEntity, EUpdateReason.LAYOUT_CHANGED); } lastXSize = blockEntity.getXSizeScaled(); } @Override - public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, EUpdateReason reason) { - carriageIndexLabel = null; - EDisplayType type = blockEntity.getDisplayType(); - EDisplayInfo info = blockEntity.getInfoType(); - - if (lastType != type || lastInfo != info) { - if (renderSubtypes.containsKey(type)) { - Map>> selectedType = renderSubtypes.get(type); - if (selectedType.containsKey(info)) { - renderSubtype = selectedType.get(info).get(); - } - } - - if (renderSubtype == null) { - renderSubtype = new BERRenderSubtypeBase<>(); - } + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, Object data) { + EUpdateReason reason = (EUpdateReason)data; + DisplayTypeResourceKey type = blockEntity.getDisplayTypeKey(); + if (lastType == null || !lastType.equals(type)) { + renderSubtype = AdvancedDisplaysRegistry.getRenderer(type); } lastType = type; - lastInfo = info; renderSubtype.update(level, pos, state, blockEntity, this, reason); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERRenderSubtypeBase.java b/common/src/main/java/de/mrjulsen/crn/client/ber/BERRenderSubtypeBase.java similarity index 72% rename from common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERRenderSubtypeBase.java rename to common/src/main/java/de/mrjulsen/crn/client/ber/BERRenderSubtypeBase.java index 57f18669..e20bb7b5 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERRenderSubtypeBase.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/BERRenderSubtypeBase.java @@ -1,7 +1,7 @@ -package de.mrjulsen.crn.client.ber.variants; +package de.mrjulsen.crn.client.ber; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; import de.mrjulsen.mcdragonlib.client.ber.AbstractBlockEntityRenderInstance; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; @@ -9,11 +9,6 @@ public final class BERRenderSubtypeBase, U> implements IBERRenderSubtype{ - @Override - public boolean isSingleLined() { - return false; - } - @Override public void update(Level level, BlockPos pos, BlockState state, T blockEntity, S parent, EUpdateReason reason) {} } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/IBERRenderSubtype.java b/common/src/main/java/de/mrjulsen/crn/client/ber/IBERRenderSubtype.java new file mode 100644 index 00000000..3a936081 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/IBERRenderSubtype.java @@ -0,0 +1,16 @@ +package de.mrjulsen.crn.client.ber; + +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.mcdragonlib.client.ber.AbstractBlockEntityRenderInstance; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +public interface IBERRenderSubtype, U> { + void update(Level level, BlockPos pos, BlockState state, T blockEntity, S parent, EUpdateReason data); + default void renderTick(float deltaTime) {} + default void render(BERGraphics graphics, float partialTick, S parent, int light, boolean backSide) {} + default void tick(Level level, BlockPos pos, BlockState state, T pBlockEntity, S parent) {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java b/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java index b5d00876..1045c129 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java @@ -1,15 +1,15 @@ package de.mrjulsen.crn.client.ber; -import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Vector3f; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.block.be.TrainStationClockBlockEntity; +import de.mrjulsen.crn.block.blockentity.TrainStationClockBlockEntity; import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.ber.AbstractBlockEntityRenderInstance; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.util.BERUtils; import net.minecraft.client.renderer.LightTexture; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.block.HorizontalDirectionalBlock; @@ -22,41 +22,40 @@ public TrainStationClockRenderer(TrainStationClockBlockEntity blockEntity) { } @Override - public void render(BlockEntityRendererContext context, TrainStationClockBlockEntity pBlockEntity, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay) { - - context.renderUtils().initRenderEngine(); + public void render(BERGraphics graphics, float partialTick) { + BERUtils.initRenderEngine(); float z = 3.2f; - pPoseStack.translate(8, 8, 8 + z); - context.renderUtils().renderTexture(DIAL_TEXTURE, pBufferSource, pBlockEntity, pPoseStack, !pBlockEntity.isGlowing(), -7, -7, -0.2f, 14, 14, 0, 0, 1, 1, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), (0xFF << 24) | (pBlockEntity.getColor()), pBlockEntity.isGlowing() ? LightTexture.FULL_BRIGHT : pPackedLight); - - pPoseStack.pushPose(); - pPoseStack.mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(pBlockEntity.getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 12000))); - context.renderUtils().fillColor(pBufferSource, pBlockEntity, 0xFF191919, pPoseStack, -0.5f, -0.5f, 0, 6, 1, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); - pPoseStack.popPose(); - - pPoseStack.pushPose(); - pPoseStack.mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(pBlockEntity.getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 1000))); - context.renderUtils().fillColor(pBufferSource, pBlockEntity, 0xFF222222, pPoseStack, -0.5f, -0.5f, 0.1f, 7, 1, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); - pPoseStack.popPose(); - - pPoseStack.translate(0, 0, -z * 2); - pPoseStack.pushPose(); - pPoseStack.mulPose(Vector3f.YP.rotationDegrees(180)); - context.renderUtils().renderTexture(DIAL_TEXTURE, pBufferSource, pBlockEntity, pPoseStack, !pBlockEntity.isGlowing(), -7, -7, -0.2f, 14, 14, 0, 0, 1, 1, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING).getOpposite(), (0xFF << 24) | (pBlockEntity.getColor()), pBlockEntity.isGlowing() ? LightTexture.FULL_BRIGHT : pPackedLight); - pPoseStack.popPose(); - - pPoseStack.pushPose(); - pPoseStack.mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(pBlockEntity.getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 12000))); - pPoseStack.mulPose(Vector3f.YP.rotationDegrees(180)); - context.renderUtils().fillColor(pBufferSource, pBlockEntity, 0xFF191919, pPoseStack, -0.5f, -0.5f, 0, 6, 1, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); - pPoseStack.popPose(); - - pPoseStack.pushPose(); - pPoseStack.mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(pBlockEntity.getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 1000))); - pPoseStack.mulPose(Vector3f.YP.rotationDegrees(180)); - context.renderUtils().fillColor(pBufferSource, pBlockEntity, 0xFF222222, pPoseStack, -0.5f, -0.5f, 0.1f, 7, 1, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); - pPoseStack.popPose(); + graphics.poseStack().translate(8, 8, 8 + z); + BERUtils.renderTexture(DIAL_TEXTURE, graphics, !graphics.blockEntity().isGlowing(), -7, -7, -0.2f, 14, 14, 0, 0, 1, 1, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), (0xFF << 24) | (graphics.blockEntity().getColor()), graphics.blockEntity().isGlowing() ? LightTexture.FULL_BRIGHT : graphics.packedLight()); + + graphics.poseStack().pushPose(); + graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 12000))); + BERUtils.fillColor(graphics, -0.5f, -0.5f, 0, 6, 1, 0xFF191919, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); + graphics.poseStack().popPose(); + + graphics.poseStack().pushPose(); + graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 1000))); + BERUtils.fillColor(graphics, -0.5f, -0.5f, 0.1f, 7, 1, 0xFF222222, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); + graphics.poseStack().popPose(); + + graphics.poseStack().translate(0, 0, -z * 2); + graphics.poseStack().pushPose(); + graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(180)); + BERUtils.renderTexture(DIAL_TEXTURE, graphics, !graphics.blockEntity().isGlowing(), -7, -7, -0.2f, 14, 14, 0, 0, 1, 1, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING).getOpposite(), (0xFF << 24) | (graphics.blockEntity().getColor()), graphics.blockEntity().isGlowing() ? LightTexture.FULL_BRIGHT : graphics.packedLight()); + graphics.poseStack().popPose(); + + graphics.poseStack().pushPose(); + graphics.poseStack().mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 12000))); + graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(180)); + BERUtils.fillColor(graphics, -0.5f, -0.5f, 0, 6, 1, 0xFF191919, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); + graphics.poseStack().popPose(); + + graphics.poseStack().pushPose(); + graphics.poseStack().mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 1000))); + graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(180)); + BERUtils.fillColor(graphics, -0.5f, -0.5f, 0.1f, 7, 1, 0xFF222222, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); + graphics.poseStack().popPose(); } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/base/BERText.java b/common/src/main/java/de/mrjulsen/crn/client/ber/base/BERText.java deleted file mode 100644 index a982c025..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/base/BERText.java +++ /dev/null @@ -1,350 +0,0 @@ -package de.mrjulsen.crn.client.ber.base; - -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import com.mojang.blaze3d.font.GlyphInfo; -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.mcdragonlib.client.util.FontUtils; -import de.mrjulsen.mcdragonlib.mixin.BakedGlyphAccessor; -import de.mrjulsen.mcdragonlib.util.MathUtils; -import net.minecraft.client.gui.Font; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.network.chat.Component; -import net.minecraft.util.StringDecomposer; - -/** - * A text component designed for block entity rendering. It supports scissoring and scrolling text, e.g. for display boards. - */ -public class BERText { - - private static final byte CHAR_SIZE = 8; - - private final FontUtils fontUtils; - private final float xOffset; - private Supplier> textData; - - private float minX = 0; - private float maxX = Float.MAX_VALUE; - private float minStretchScale = 1.0f; - private float maxStretchScale = 1.0f; - private float maxWidth = Float.MAX_VALUE; - private boolean forceMaxWidth = false; - private float scrollSpeed = 0.0f; - private boolean centered = false; - private int color = 0xFFFFFFFF; - private int ticksPerPage = 200; - private int refreshRate = 0; - private Consumer onUpdate; - - private TextTransformation predefinedTextTransformation = null; - - // stored data - private TextDataCache cache; - private float scrollXOffset = 0.0f; - private int refreshTimer = 0; - private int currentTicks = 0; - private int currentIndex = 0; - private List texts; - - public BERText(FontUtils fontUtils, Component text, float xOffset) { - this.fontUtils = fontUtils; - this.textData = () -> List.of(text); - this.xOffset = xOffset; - } - - public BERText(FontUtils fontUtils, Supplier> texts, float xOffset) { - this.fontUtils = fontUtils; - this.textData = texts; - this.xOffset = xOffset; - } - - public BERText withStencil(float minX, float maxX) { - this.minX = minX; - this.maxX = maxX; - return this; - } - - public BERText withStretchScale(float minScale, float maxScale) { - this.minStretchScale = minScale; - this.maxStretchScale = maxScale; - return this; - } - - public BERText withMaxWidth(float maxWidth, boolean force) { - this.maxWidth = maxWidth; - this.forceMaxWidth = force; - return this; - } - - public BERText withIsCentered(boolean b) { - this.centered = b; - return this; - } - - public BERText withCanScroll(boolean b, float scrollingSpeed) { - this.scrollSpeed = b ? scrollingSpeed : 0; - return this; - } - - public BERText withColor(int color) { - this.color = color; - return this; - } - - public BERText withTicksPerPage(int ticks) { - this.ticksPerPage = ticks; - return this; - } - - /** - * The displayed text is updated every x ticks. If the value is less than or equal to 0, then the text will not be updated. - */ - public BERText withRefreshRate(int ticks) { - this.refreshRate = ticks; - return this; - } - - public BERText withPredefinedTextTransformation(TextTransformation transformation) { - this.predefinedTextTransformation = transformation; - return this; - } - - public BERText withUpdateFunc(Consumer onUpdate) { - this.onUpdate = onUpdate; - return this; - } - - public BERText build() { - fetchCurrentText(); - calc(false); - scrollXOffset = cache.maxWidthScaled(); - return this; - } - - public FontUtils getFontUtils() { - return fontUtils; - } - - private void fetchCurrentText() { - texts = textData.get(); - } - - public Component getCurrentText() { - return texts.get(currentIndex); - } - - public List getTexts() { - return texts; - } - - public int getTicksPerPage() { - return ticksPerPage; - } - - public float getXOffset() { - return xOffset; - } - - public float getMinX() { - return minX; - } - - public float getMaxX() { - return maxX; - } - - public float getMinStretchScale() { - return minStretchScale; - } - - public float getMaxStretchScale() { - return maxStretchScale; - } - - public float getMaxWidth() { - return maxWidth; - } - - public boolean forceMaxWidth() { - return forceMaxWidth; - } - - public boolean canScroll() { - return scrollSpeed > 0; - } - - public float getScrollSpeed() { - return scrollSpeed; - } - - public boolean isCentered() { - return centered; - } - - public float getTextWidth() { - return getFontUtils().font.width(getCurrentText()); - } - - public float getScaledTextWidth() { - return getTextWidth() * cache.textXScale(); - } - - public int getColor() { - return color; - } - - public void recalc() { - calc(false); - } - - protected void calc(boolean callUpdate) { - float textWidth = getFontUtils().font.width(getCurrentText()); - float rawXScale = getMaxWidth() / textWidth; - float finalXScale = MathUtils.clamp(rawXScale, getMinStretchScale(), getMaxStretchScale()); - boolean mustScroll = rawXScale < getMinStretchScale(); - - if (forceMaxWidth() && mustScroll) { - finalXScale = getMaxStretchScale(); - } - - float minX = getMinX() / finalXScale; - float maxWidthScaled = getMaxWidth() / finalXScale; - float maxX = Math.min(forceMaxWidth() ? maxWidthScaled : Float.MAX_VALUE, getMaxX() / finalXScale); - float xOffset = getXOffset() + (isCentered() ? maxWidthScaled / 2 - textWidth / 2 : 0); - cache = new TextDataCache(finalXScale, minX, maxX, xOffset, maxWidthScaled, textWidth, forceMaxWidth() && mustScroll && canScroll()); - - if (callUpdate && onUpdate != null) { - onUpdate.accept(this); - } - } - - public void render(PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight) { - getFontUtils().reset(); - pPoseStack.pushPose(); { - if (predefinedTextTransformation != null) { - pPoseStack.translate(predefinedTextTransformation.x(), predefinedTextTransformation.y(), predefinedTextTransformation.z()); - pPoseStack.scale(predefinedTextTransformation.xScale(), predefinedTextTransformation.yScale(), 1); - } - - pPoseStack.pushPose(); { - pPoseStack.scale(cache.textXScale(), 1, 1); - renderTextInBounds(pPoseStack, getFontUtils(), pBufferSource, getCurrentText(), pPackedLight, cache.mustScroll ? scrollXOffset : cache.xOffset(), cache.minX(), cache.maxX(), getColor()); - } - pPoseStack.popPose(); - } - pPoseStack.popPose(); - } - - private void renderTextInBounds(PoseStack pPoseStack, FontUtils fontUtils, MultiBufferSource pBufferSource, Component text, int pPackedLight, float xOffset, float xLeft, float xRight, int color) { - if (xRight <= xLeft) { - return; - } - - pPoseStack.pushPose(); - pPoseStack.translate(xLeft + (xOffset > 0 ? xOffset : 0), 0, 0); - Font.StringRenderOutput sro = fontUtils.font.new StringRenderOutput(pBufferSource, 0, 0, color, false, pPoseStack.last().pose(), Font.DisplayMode.NORMAL, pPackedLight); - - float newX = xOffset; - float glyphTranslation = 0; - final float charSize = CHAR_SIZE + (text.getStyle().isBold() ? 1 : 0); - - for (int i = 0; i < text.getString().length(); i++) { - int charCode = text.getString().charAt(i); - GlyphInfo info = fontUtils.fontSet.getGlyphInfo(charCode, false); - float glyphWidth = info.getAdvance(text.getStyle().isBold()); - float oldX = newX; - newX += glyphWidth; - - if (newX > xLeft && oldX < xLeft) { - float diff = xLeft - oldX; - BakedGlyphAccessor glyph = fontUtils.getGlyphAccessor(charCode); - float glyphUVDiff = glyph.getU1() - glyph.getU0(); - float scale = (1.0f / charSize * diff); - float sub = glyphUVDiff * scale; - - fontUtils.pushUV(charCode); - glyph.setU0(glyph.getU0() + sub); - - pPoseStack.pushPose(); - float invScale = 1.0f - scale; - pPoseStack.scale(invScale, 1, 1); - Font.StringRenderOutput sro2 = fontUtils.font.new StringRenderOutput(pBufferSource, 0 , 0, color, false, pPoseStack.last().pose(), Font.DisplayMode.NORMAL, pPackedLight); - StringDecomposer.iterateFormatted(String.valueOf((char)charCode), text.getStyle(), sro2); - pPoseStack.popPose(); - fontUtils.popUV(charCode); - pPoseStack.translate(glyphWidth - (charSize * scale), 0, 0); - continue; - } else if (newX > xRight) { - float diff = newX - xRight; - float charRightSpace = charSize - glyphWidth; - float totalDiff = diff + charRightSpace; - - BakedGlyphAccessor glyph = fontUtils.getGlyphAccessor(charCode); - float glyphUVDiff = glyph.getU1() - glyph.getU0(); - float scale = (1.0f / charSize * totalDiff); - float sub = glyphUVDiff * scale; - - fontUtils.pushUV(charCode); - glyph.setU1(glyph.getU1() - sub); - pPoseStack.pushPose(); - float invScale = 1.0f - scale; - pPoseStack.scale(invScale, 1, 1); - pPoseStack.translate(glyphTranslation / invScale, 0, 0); - Font.StringRenderOutput sro2 = fontUtils.font.new StringRenderOutput(pBufferSource, 0, 0, color, false, pPoseStack.last().pose(), Font.DisplayMode.NORMAL, pPackedLight); - StringDecomposer.iterateFormatted(String.valueOf((char)charCode), text.getStyle(), sro2); - pPoseStack.popPose(); - fontUtils.popUV(charCode); - break; - } else if (oldX >= xLeft && newX <= xRight) { - StringDecomposer.iterateFormatted(String.valueOf((char)charCode), text.getStyle(), sro); - } else { - continue; - } - - glyphTranslation += glyphWidth; - } - pPoseStack.popPose(); - } - - public void tick() { - - if (refreshRate > 0) { - refreshTimer++; - if ((refreshTimer %= refreshRate) == 0) { - fetchCurrentText(); - calc(true); - } - } - - boolean multiText = getTexts().size() > 1; - - if (cache.mustScroll()) { - scrollXOffset -= getScrollSpeed() / this.getMaxStretchScale(); - if (scrollXOffset < -cache.textWidth()) { - scrollXOffset = cache.maxWidthScaled(); - - if (multiText) { - currentIndex++; - fetchCurrentText(); - currentIndex %= getTexts().size(); - calc(true); - } - } - } else if (multiText) { - currentTicks++; - if ((currentTicks %= getTicksPerPage()) == 0) { - currentIndex++; - fetchCurrentText(); - currentIndex %= getTexts().size(); - calc(true); - } - } - } - - protected static record TextDataCache(float textXScale, float minX, float maxX, float xOffset, float maxWidthScaled, float textWidth, boolean mustScroll) {} - public static record TextTransformation(float x, float y, float z, float xScale, float yScale) {} - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java new file mode 100644 index 00000000..9a8af342 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java @@ -0,0 +1,41 @@ +package de.mrjulsen.crn.client.ber.variants; + +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +public class BERError implements IBERRenderSubtype { + + private final BERLabel label = new BERLabel() + .setPos(3, 3) + .setScale(0.5f, 0.5f) + .setYScale(0.5f) + ; + + @Override + public void renderTick(float deltaTime) { + label.renderTick(); + } + + @Override + public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + label.render(graphics); + } + + @Override + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { + label + .setText(TextUtils.text("Error! Unrecognized display type!")) + .setColor(0xFFFF0000) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) + ; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoDetailed.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoDetailed.java deleted file mode 100644 index df15f40a..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoDetailed.java +++ /dev/null @@ -1,234 +0,0 @@ -package de.mrjulsen.crn.client.ber.variants; - -import java.util.List; - -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; -import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; -import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.DeparturePrediction.TrainExitSide; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.event.listeners.JourneyListener.State; -import de.mrjulsen.crn.util.ModUtils; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; -import de.mrjulsen.mcdragonlib.util.DLUtils; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.HorizontalDirectionalBlock; -import net.minecraft.world.level.block.state.BlockState; - -public class BERPassengerInfoDetailed implements IBERRenderSubtype { - - private State state = State.WHILE_TRAVELING; - - private static final String keyNextStop = "gui.createrailwaysnavigator.route_overview.next_stop"; - private static final String keyDate = "gui.createrailwaysnavigator.route_overview.date"; - - private BERText announceNextStopLabel; - private BERText whileNextStopLabel; - - @Override - public boolean isSingleLined() { - return true; - } - - @Override - public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent) { - if (pBlockEntity.getTrainData() == null) { - return; - } - - DLUtils.doIfNotNull(announceNextStopLabel, x -> x.tick()); - DLUtils.doIfNotNull(whileNextStopLabel, x -> x.tick()); - - boolean dirty = false; - - if (pBlockEntity.getTrainData().getNextStop().isPresent()) { - if (this.state != State.WHILE_NEXT_STOP && pBlockEntity.getTrainData().getNextStop().get().departureTicks() <= 0) { - this.state = State.WHILE_NEXT_STOP; - dirty = true; - } else if (this.state != State.BEFORE_NEXT_STOP && pBlockEntity.getTrainData().getNextStop().get().departureTicks() <= ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get() && pBlockEntity.getTrainData().getNextStop().get().departureTicks() > 0) { - this.state = State.BEFORE_NEXT_STOP; - dirty = true; - } else if (this.state != State.WHILE_TRAVELING && pBlockEntity.getTrainData().getNextStop().get().departureTicks() > ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) { - this.state = State.WHILE_TRAVELING; - dirty = true; - } - } - - if (dirty) { - update(level, pos, state, pBlockEntity, parent, EUpdateReason.DATA_CHANGED); - } - } - - @Override - public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - if (blockEntity.getTrainData() == null) { - return; - } - - parent.labels.clear(); - announceNextStopLabel = null; - whileNextStopLabel = null; - - switch (this.state) { - case BEFORE_NEXT_STOP: - updateAnnounceNextStop(level, pos, state, blockEntity, parent); - break; - case WHILE_NEXT_STOP: - updateWhileNextStop(level, pos, state, blockEntity, parent); - break; - default: - updateDefault(level, pos, state, blockEntity, parent); - break; - } - } - - @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - if (state == State.WHILE_NEXT_STOP || state == State.BEFORE_NEXT_STOP) { - context.renderUtils().initRenderEngine(); - TrainExitSide side = pBlockEntity.relativeExitDirection.get(); - float uv = 1.0f / 256.0f; - - if (backSide) { - side = side.getOpposite(); - } - - switch (side) { - case RIGHT: - context.renderUtils().renderTexture( - ModGuiIcons.ICON_LOCATION, - pBufferSource, - pBlockEntity, - pPoseStack, - false, - pBlockEntity.getXSizeScaled() * 16 - 3 - 8, - 4, - 0, - 8, - 8, - uv * ModGuiIcons.ARROW_RIGHT.getU(), - uv * ModGuiIcons.ARROW_RIGHT.getV(), - uv * (ModGuiIcons.ARROW_RIGHT.getU() + ModGuiIcons.ICON_SIZE), - uv * (ModGuiIcons.ARROW_RIGHT.getV() + ModGuiIcons.ICON_SIZE), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (pBlockEntity.getColor()), - pPackedLight - ); - break; - case LEFT: - context.renderUtils().renderTexture( - ModGuiIcons.ICON_LOCATION, - pBufferSource, - pBlockEntity, - pPoseStack, - false, - 3f, - 4, - 0, - 8, - 8, - uv * ModGuiIcons.ARROW_LEFT.getU(), - uv * ModGuiIcons.ARROW_LEFT.getV(), - uv * (ModGuiIcons.ARROW_LEFT.getU() + ModGuiIcons.ICON_SIZE), - uv * (ModGuiIcons.ARROW_LEFT.getV() + ModGuiIcons.ICON_SIZE), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (pBlockEntity.getColor()), - pPackedLight - ); - break; - default: - break; - } - - if (backSide) { - switch (side) { - case LEFT: - pPoseStack.translate(10, 0, 0); - break; - case RIGHT: - pPoseStack.translate(-10, 0, 0); - break; - default: - break; - } - } - - DLUtils.doIfNotNull(announceNextStopLabel, x -> x.render(pPoseStack, pBufferSource, pPackedLight)); - DLUtils.doIfNotNull(whileNextStopLabel, x -> x.render(pPoseStack, pBufferSource, pPackedLight)); - } - } - - - private void updateDefault(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - boolean isSingleBlock = blockEntity.getXSizeScaled() <= 1; - - float maxWidth = displayWidth * 16 - 6; - parent.labels.add(new BERText(parent.getFontUtils(), () -> List.of( - TextUtils.text(blockEntity.getTrainData().trainName()).append(" ").append(TextUtils.text(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().scheduleTitle() : "")), - ModUtils.calcSpeedString(blockEntity.getTrainData().speed(), ModClientConfig.SPEED_UNIT.get()), - isSingleBlock ? - TextUtils.text(TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())) : - ELanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / DragonLib.TICKS_PER_DAY, TimeUtils.parseTime((int)(blockEntity.getLevel().dayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())) - ), 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3, 5.5f, 0.0f, 1, 0.75f)) - .build() - ); - } - - private void updateAnnounceNextStop(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - TrainExitSide side = blockEntity.relativeExitDirection.get(); - - MutableComponent line = ELanguage.translate(keyNextStop, GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().stationTagName() : "").getAliasName().get()); - float maxWidth = displayWidth * 16 - 6 - (side != TrainExitSide.UNKNOWN ? 10 : 0); - announceNextStopLabel = (new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3 + (side == TrainExitSide.LEFT ? 10 : 0), 5.5f, 0.0f, 1, 0.75f)) - .build() - ); - } - - private void updateWhileNextStop(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - - TrainExitSide side = blockEntity.relativeExitDirection.get(); - MutableComponent line = TextUtils.text(GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().stationTagName() : null).getAliasName().get()); - - float maxWidth = displayWidth * 16 - 6 - (side != TrainExitSide.UNKNOWN ? 10 : 0); - whileNextStopLabel = (new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3 + (side == TrainExitSide.LEFT ? 10 : 0), 5.5f, 0.0f, 1, 0.75f)) - .build() - ); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java index 53fe30ce..82449018 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java @@ -1,35 +1,33 @@ package de.mrjulsen.crn.client.ber.variants; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import com.mojang.blaze3d.vertex.PoseStack; - +import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.client.CRNGui; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.SimpleTrainConnection; -import de.mrjulsen.crn.data.DeparturePrediction.TrainExitSide; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.event.listeners.JourneyListener.State; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.NextConnectionsRequestPacket; +import de.mrjulsen.crn.data.TrainExitSide; +import de.mrjulsen.crn.data.train.portable.NextConnectionsDisplayData; +import de.mrjulsen.crn.data.train.portable.TrainDisplayData; +import de.mrjulsen.crn.data.train.portable.TrainStopDisplayData; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.crn.registry.data.NextConnectionsRequestData; +import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.client.util.BERUtils; +import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; import net.minecraft.ChatFormatting; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.HorizontalDirectionalBlock; @@ -37,490 +35,590 @@ public class BERPassengerInfoInformative implements IBERRenderSubtype { - private static final ResourceLocation TEXTURE = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/overview.png"); - private static final int TEX_ROUTE_PATH_U = 226; - private static final int TEX_ROUTE_PATH_H = 14; - private static final float PANEL_LINE_HEIGHT = 2.0f; - private static final float PANEL_Y_START = 5.75f; - private static final int NEXT_CONNECTIONS_MAX_ENTRIES_PER_PAGE = 3; - private static final int NEXT_CONNECTIONS_PAGE_TIMER = 100; - - private BERText timeLabel; - private BERText titleLabel; - private State state = State.WHILE_TRAVELING; - - // data - private List nextConnections; - private long nextConnectionsRefreshTime = 0; - private int nextConnectionsPage = 0; - private int nextConnectionsMaxPage = 0; - private int nextConnectionsTimer = 0; - - // Cache - private TrainExitSide lastKnownExitSide = TrainExitSide.UNKNOWN; - + private static final ResourceLocation CARRIAGE_ICON = new ResourceLocation("create:textures/gui/assemble.png"); + private static final ResourceLocation ICONS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/icons.png"); + private static final String keyDate = "gui.createrailwaysnavigator.route_overview.date"; private static final String keyNextStop = "gui.createrailwaysnavigator.route_overview.next_stop"; - private static final Component textNextConnections = ELanguage.translate("gui.createrailwaysnavigator.route_overview.next_connections").withStyle(ChatFormatting.BOLD); - - @Override - public boolean isSingleLined() { - return false; + private static final String keyNextConnections = "gui.createrailwaysnavigator.route_overview.next_connections"; + private static final int MAX_LINES = 4; + + private NextConnectionsDisplayData nextConnections = null; + private boolean nextStopAnnounced = false; + private TrainExitSide exitSide = TrainExitSide.UNKNOWN; + + private final BERLabel timeLabel = new BERLabel() + .setScale(0.25f, 0.25f) + .setYScale(0.25f) + ; + private final BERLabel carriageLabel = new BERLabel() + .setScale(0.25f, 0.25f) + .setYScale(0.25f) + ; + private final BERLabel trainLineLabel = new BERLabel() + .setPos(3, 2.5f) + .setScale(0.25f, 0.15f) + .setYScale(0.25f) + ; + private final BERLabel speedLabel = new BERLabel() + .setPos(3, 6) + .setScale(0.25f, 0.2f) + .setYScale(0.30f) + .setCentered(true) + ; + private final BERLabel dateLabel = new BERLabel() + .setPos(3, 9) + .setScale(0.2f, 0.15f) + .setYScale(0.2f) + .setCentered(true) + ; + private final BERLabel carriageInfoLabel = new BERLabel() + .setPos(4.5f, 11) + .setScale(0.2f, 0.15f) + .setYScale(0.2f) + .setCentered(true) + ; + private final BERLabel nextConnectionsTitleLabel = new BERLabel(ELanguage.translate(keyNextConnections).withStyle(ChatFormatting.BOLD)) + .setPos(3, 5.5f) + .setScale(0.15f, 0.15f) + .setYScale(0.15f) + ; + private final BERLabel pageIndicatorLabel = new BERLabel() + .setPos(3, 12.5f) + .setScale(0.15f, 0.15f) + .setYScale(0.15f) + .setCentered(true) + ; + + private BERLabel[][] scheduleLines; + private BERLabel[][] nextConnectionsLines = new BERLabel[MAX_LINES - 1][]; + + + private boolean shouldRenderNextConnections() { + return nextConnections != null && !nextConnections.getConnections().isEmpty() && nextStopAnnounced; } - @Override - public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent) { - if (pBlockEntity.getTrainData() == null) { - return; + private String generatePageIndexString(int current, int max) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < current; i++) { + sb.append(" □"); + } + sb.append(" ■"); + for (int i = current + 1; i < max; i++) { + sb.append(" □"); } + return sb.toString(); + } - boolean dirty = false; - - if (pBlockEntity.getTrainData().getNextStop().isPresent()) { - if (this.state != State.WHILE_NEXT_STOP && pBlockEntity.getTrainData().getNextStop().get().departureTicks() <= 0) { - this.state = State.WHILE_NEXT_STOP; - dirty = true; - } else if (this.state != State.BEFORE_NEXT_STOP && pBlockEntity.getTrainData().getNextStop().get().departureTicks() <= ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get() && pBlockEntity.getTrainData().getNextStop().get().departureTicks() > 0) { - this.state = State.BEFORE_NEXT_STOP; - this.nextConnections = null; - long id = InstanceManager.registerClientNextConnectionsResponseAction((data, refreshTime) -> { - this.nextConnections = new ArrayList<>(data); - nextConnectionsPage = 0; - nextConnectionsTimer = 0; - nextConnectionsRefreshTime = refreshTime; - if (data != null && !data.isEmpty()) { - nextConnectionsMaxPage = (int)(nextConnections.size() / NEXT_CONNECTIONS_MAX_ENTRIES_PER_PAGE + (nextConnections.size() % NEXT_CONNECTIONS_MAX_ENTRIES_PER_PAGE == 0 ? 0 : 1)); - parent.labels.clear(); - updateNextConnections(level, pos, state, pBlockEntity, parent); + @Override + public void renderTick(float deltaTime) { + timeLabel.renderTick(); + dateLabel.renderTick(); + trainLineLabel.renderTick(); + speedLabel.renderTick(); + carriageInfoLabel.renderTick(); + nextConnectionsTitleLabel.renderTick(); + pageIndicatorLabel.renderTick(); + DLUtils.doIfNotNull(scheduleLines, x -> { + for (int i = 0; i < x.length; i++) { + DLUtils.doIfNotNull(x[i], y -> { + for (int j = 0; j < y.length; j++) { + DLUtils.doIfNotNull(y[j], z -> z.renderTick()); } }); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new NextConnectionsRequestPacket(id, pBlockEntity.getTrainData().trainId(), pBlockEntity.getTrainData().getNextStop().get().stationTagName(), pBlockEntity.getTrainData().getNextStop().get().departureTicks())); - dirty = true; - } else if (this.state != State.WHILE_TRAVELING && pBlockEntity.getTrainData().getNextStop().get().departureTicks() > ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) { - this.state = State.WHILE_TRAVELING; - dirty = true; } - } - - if (this.state == State.BEFORE_NEXT_STOP && this.nextConnections != null && !this.nextConnections.isEmpty()) { - nextConnectionsTimer++; - if ((nextConnectionsTimer %= NEXT_CONNECTIONS_PAGE_TIMER) == 0) { - nextConnectionsPage++; - nextConnectionsPage %= nextConnectionsMaxPage; - - parent.labels.clear(); - updateNextConnections(level, pos, state, pBlockEntity, parent); + }); + DLUtils.doIfNotNull(nextConnectionsLines, x -> { + for (int i = 0; i < x.length; i++) { + DLUtils.doIfNotNull(x[i], y -> { + for (int j = 0; j < y.length; j++) { + DLUtils.doIfNotNull(y[j], z -> z.renderTick()); + } + }); } - } - - if (lastKnownExitSide != pBlockEntity.relativeExitDirection.get()) { - dirty = true; - } - - lastKnownExitSide = pBlockEntity.relativeExitDirection.get(); - - if (dirty) { - update(level, pos, state, pBlockEntity, parent, EUpdateReason.DATA_CHANGED); - } else { - generateTimeLabel(level, pos, state, pBlockEntity, parent); - } - - if (titleLabel != null) { - titleLabel.tick(); - } + }); } @Override - public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - if (blockEntity.getTrainData() == null) { - return; - } - - generateTitleBar(level, pos, state, blockEntity, parent, reason); - - if (this.state == State.BEFORE_NEXT_STOP && this.nextConnections != null && !this.nextConnections.isEmpty()) { - return; - } - - parent.labels.clear(); - updateOverview(level, pos, state, blockEntity, parent); + public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { + timeLabel + .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - timeLabel.getTextWidth() - (this.exitSide != TrainExitSide.UNKNOWN ? 4 : 0), 2.5f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; } - @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - - // render title bar - timeLabel.render(pPoseStack, pBufferSource, pPackedLight); - titleLabel.render(pPoseStack, pBufferSource, pPackedLight); - - context.renderUtils().initRenderEngine(); - context.renderUtils().fillColor(pBufferSource, pBlockEntity, (0xFF << 24) | (pBlockEntity.getColor() & 0x00FFFFFF), pPoseStack, 2.5f, 4.75f, 0.0f, pBlockEntity.getXSizeScaled() * 16 - 5, 0.25f, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); - float uv = 1.0f / 256.0f; - float y = 5f; + public void renderHeader(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + final float uv255 = 1f / 256f; + TrainExitSide side = exitSide; + if (backSide) { + side = side.getOpposite(); + } - if (notInService(pBlockEntity)) { - return; + graphics.poseStack().pushPose(); + if (side == TrainExitSide.LEFT) { + graphics.poseStack().translate(4, 0, 0); } - // Render route path - if (this.state != State.BEFORE_NEXT_STOP || nextConnections == null || nextConnections.isEmpty()) { - float tempH = PANEL_LINE_HEIGHT - 0.2857142f; - context.renderUtils().renderTexture( - TEXTURE, - pBufferSource, - pBlockEntity, - pPoseStack, + // Render time + if (graphics.blockEntity().getXSizeScaled() > 1 && !nextStopAnnounced) { + timeLabel.render(graphics, light); + BERUtils.renderTexture( + ICONS, + graphics, false, - 8, - y, + timeLabel.getX() - 2.5f, + 2.5f, 0.0f, - 1, - tempH, - uv * TEX_ROUTE_PATH_U, - uv * 2, - uv * (TEX_ROUTE_PATH_U + 7), - uv * (TEX_ROUTE_PATH_H), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - 0xFFFFFFFF, - pPackedLight + 2, + 2, + uv255 * 227, + uv255 * 19, + uv255 * (227 + 10), + uv255 * (19 + 10), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light ); - y += tempH; - for (int i = 0; i < 2 && i < pBlockEntity.getTrainData().stopovers().size(); i++) { - context.renderUtils().renderTexture( - TEXTURE, - pBufferSource, - pBlockEntity, - pPoseStack, - false, - 8, - y, - 0.0f, - 1, - 2, - uv * TEX_ROUTE_PATH_U, - uv * TEX_ROUTE_PATH_H * 1, - uv * (TEX_ROUTE_PATH_U + 7), - uv * (TEX_ROUTE_PATH_H * 2), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - 0xFFFFFFFF, - pPackedLight - ); - y += PANEL_LINE_HEIGHT; - } - - if (pBlockEntity.getTrainData().predictions().size() > 1) { - context.renderUtils().renderTexture( - TEXTURE, - pBufferSource, - pBlockEntity, - pPoseStack, - false, - 8, - y, - 0.0f, - 1, - 2, - uv * TEX_ROUTE_PATH_U, - uv * TEX_ROUTE_PATH_H * 2, - uv * (TEX_ROUTE_PATH_U + 7), - uv * (TEX_ROUTE_PATH_H * 3), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - 0xFFFFFFFF, - pPackedLight - ); - } - } - // EXIT ARROW - TrainExitSide side = pBlockEntity.relativeExitDirection.get(); - if (backSide) { - side = side.getOpposite(); + if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { + graphics.poseStack().popPose(); + return; } - if (state != State.WHILE_TRAVELING && side != TrainExitSide.UNKNOWN) { - context.renderUtils().renderTexture( - ModGuiIcons.ICON_LOCATION, - pBufferSource, - pBlockEntity, - pPoseStack, + + trainLineLabel.render(graphics, light); + + // Carriage label + if (graphics.blockEntity().getXSizeScaled() > 2 && !nextStopAnnounced) { + carriageLabel.render(graphics, light); + BERUtils.renderTexture( + CARRIAGE_ICON, + graphics, false, - pBlockEntity.getXSizeScaled() * 16 - 3f - 2, - 2.25f, - 0, + carriageLabel.getX() - 3.5f, 2.5f, - 2.5f, - uv * (side == TrainExitSide.RIGHT ? ModGuiIcons.ARROW_RIGHT : ModGuiIcons.ARROW_LEFT).getU(), - uv * (side == TrainExitSide.RIGHT ? ModGuiIcons.ARROW_RIGHT : ModGuiIcons.ARROW_LEFT).getV(), - uv * ((side == TrainExitSide.RIGHT ? ModGuiIcons.ARROW_RIGHT : ModGuiIcons.ARROW_LEFT).getU() + ModGuiIcons.ICON_SIZE), - uv * ((side == TrainExitSide.RIGHT ? ModGuiIcons.ARROW_RIGHT : ModGuiIcons.ARROW_LEFT).getV() + ModGuiIcons.ICON_SIZE), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (pBlockEntity.getColor()), - pPackedLight + 0.0f, + 3, + 2, + uv255 * 22, + uv255 * 231, + uv255 * (22 + 13), + uv255 * (231 + 5), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light ); } - } - - private boolean notInService(AdvancedDisplayBlockEntity blockEntity) { - Optional optPred = blockEntity.getTrainData().getNextStop(); - return !optPred.isPresent() || optPred.get().stationTagName() == null || optPred.get().stationTagName().isBlank(); - } - - private float generateTimeLabel(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - float arrowOffset = (this.state != State.WHILE_TRAVELING && blockEntity.relativeExitDirection.get() != TrainExitSide.UNKNOWN ? 4 : 0); - float maxWidth = blockEntity.getXSizeScaled() * 16 - arrowOffset; - MutableComponent line = TextUtils.text(TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())).withStyle(ChatFormatting.BOLD); - float rawTextWidth = Math.min(parent.getFontUtils().font.width(line), maxWidth); - float textWidth = rawTextWidth * 0.25f; - timeLabel = parent.carriageIndexLabel = new BERText(parent.getFontUtils(), textWidth > parent.getFontUtils().font.width(line) * 0.1f ? line : TextUtils.empty(), 0) - .withIsCentered(false) - .withMaxWidth(textWidth, true) - .withStretchScale(0.1f, 0.25f) - .withColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) - .withPredefinedTextTransformation(new TextTransformation(blockEntity.getXSizeScaled() * 16 - 2.5f - textWidth - (this.state != State.WHILE_TRAVELING && blockEntity.relativeExitDirection.get() != TrainExitSide.UNKNOWN ? 4 : 0), 2.5f, 0.0f, 1, 0.25f)) - .build(); - return textWidth; - } - - private void generateTitleBar(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - int displayWidth = blockEntity.getXSizeScaled(); - - float maxWidth = displayWidth * 16 - 6 - (this.state != State.WHILE_TRAVELING && blockEntity.relativeExitDirection.get() != TrainExitSide.UNKNOWN ? 4 : 0); - //maxWidth *= 2; - float timeWidth = generateTimeLabel(level, pos, state, blockEntity, parent); - - MutableComponent line = TextUtils.text(blockEntity.getTrainData().trainName()).withStyle(ChatFormatting.BOLD); - if (blockEntity.getTrainData().getNextStop().isPresent()) { - switch (this.state) { - case BEFORE_NEXT_STOP: - line = ELanguage.translate(keyNextStop, blockEntity.getTrainData().getNextStop().get().stationTagName()); + graphics.poseStack().popPose(); + + if (nextStopAnnounced || graphics.blockEntity().getTrainData().isWaitingAtStation()) { + switch (side) { + case RIGHT: + BERUtils.renderTexture( + ModGuiIcons.ICON_LOCATION, + graphics, + false, + graphics.blockEntity().getXSizeScaled() * 16 - 3 - 3, + 2.05f, + 0, + 3, + 3, + uv255 * ModGuiIcons.ARROW_RIGHT.getU(), + uv255 * ModGuiIcons.ARROW_RIGHT.getV(), + uv255 * (ModGuiIcons.ARROW_RIGHT.getU() + ModGuiIcons.ICON_SIZE), + uv255 * (ModGuiIcons.ARROW_RIGHT.getV() + ModGuiIcons.ICON_SIZE), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor()), + light + ); break; - case WHILE_NEXT_STOP: - line = ELanguage.translate(blockEntity.getTrainData().trainName() + " " + blockEntity.getTrainData().getNextStop().get().stationTagName()).withStyle(ChatFormatting.BOLD); + case LEFT: + BERUtils.renderTexture( + ModGuiIcons.ICON_LOCATION, + graphics, + false, + 3, + 2.05f, + 0, + 3, + 3, + uv255 * ModGuiIcons.ARROW_LEFT.getU(), + uv255 * ModGuiIcons.ARROW_LEFT.getV(), + uv255 * (ModGuiIcons.ARROW_LEFT.getU() + ModGuiIcons.ICON_SIZE), + uv255 * (ModGuiIcons.ARROW_LEFT.getV() + ModGuiIcons.ICON_SIZE), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor()), + light + ); break; default: break; } } - - if (titleLabel != null && line.getString().equals(titleLabel.getCurrentText().getString()) && reason == EUpdateReason.DATA_CHANGED) { - return; - } - - titleLabel = new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth - timeWidth - 1, true) - .withStretchScale(0.15f, 0.25f) - .withStencil(0, maxWidth - timeWidth - 1) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) - .withPredefinedTextTransformation(new TextTransformation(3.0f, 2.5f, 0.0f, 1, 0.25f)) - .build(); } - private void updateOverview(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); + @Override + public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + final float uv255 = 1f / 256f; + renderHeader(graphics, partialTick, parent, light, backSide); + BERUtils.fillColor(graphics, 2.5f, 5.0f, 0.01f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); - // ### CONTENT PANEL - if (notInService(blockEntity) || !blockEntity.getTrainData().getNextStop().isPresent()) { + if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { return; } - float y = PANEL_Y_START; - // DESTINATION - SimpleDeparturePrediction pred = blockEntity.getTrainData().getNextStop().get(); - float maxWidth = displayWidth * 16 - 12.5f; - int rawTime = (int)(blockEntity.getLastRefreshedTime() % 24000 + pred.departureTicks() + DragonLib.DAYTIME_SHIFT); - MutableComponent line = TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get())); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(4, true) - .withStretchScale(0.08f, 0.14f) - .withStencil(0, 7) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, y + 0.3f, 0.0f, 1, 0.14f)) - .build() - ); - - - line = TextUtils.text(pred.stationTagName()).withStyle(ChatFormatting.BOLD); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.15f, 0.2f) - .withStencil(0, maxWidth) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(10.0f, y, 0.0f, 1, 0.2f)) - .build() - ); - y += PANEL_LINE_HEIGHT; - - for (int i = 0; i < 2 && i < blockEntity.getTrainData().stopovers().size(); i++) { - pred = blockEntity.getTrainData().stopovers().get(i); - rawTime = (int)(blockEntity.getLastRefreshedTime() % 24000 + pred.departureTicks() + DragonLib.DAYTIME_SHIFT); - line = TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get())); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(4, true) - .withStretchScale(0.08f, 0.14f) - .withStencil(0, 7) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, y + 0.1f, 0.0f, 1, 0.14f)) - .build() - ); - line = TextUtils.text(pred.stationTagName()); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.15f, 0.16f) - .withStencil(0, maxWidth) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(10.0f, y + 0.2f, 0.0f, 1, 0.16f)) - .build() + if (shouldRenderNextConnections()) { + DLUtils.doIfNotNull(nextConnectionsLines, x -> { + for (int i = 0; i < x.length; i++) { + DLUtils.doIfNotNull(x[i], a -> { + for (int j = 0; j < a.length; j++) { + DLUtils.doIfNotNull(a[j], b -> b.render(graphics, light)); + } + }); + } + }); + nextConnectionsTitleLabel.render(graphics, light); + pageIndicatorLabel.render(graphics, light); + } else if (DragonLib.getCurrentWorldTime() % 500 < 200 && !graphics.blockEntity().getTrainData().isWaitingAtStation()) { + // render stats + speedLabel.render(graphics, light); + dateLabel.render(graphics, light); + carriageInfoLabel.render(graphics, light); + BERUtils.renderTexture( + CARRIAGE_ICON, + graphics, + false, + graphics.blockEntity().getXSizeScaled() * 16 / 2f - carriageInfoLabel.getTextWidth() / 2f - 1.5f, + carriageInfoLabel.getY(), + 0.0f, + 2.25f, + 1.5f, + uv255 * 22, + uv255 * 231, + uv255 * (22 + 13), + uv255 * (231 + 5), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light ); - y += PANEL_LINE_HEIGHT; + } else { + // Render schedule + DLUtils.doIfNotNull(scheduleLines, x -> { + for (int i = 0; i < x.length; i++) { + final int idx = i; + DLUtils.doIfNotNull(x[i], a -> { + for (int j = 0; j < a.length; j++) { + DLUtils.doIfNotNull(a[j], b -> b.render(graphics, light)); + } + + final float uv32 = 1f / CRNGui.GUI_WIDTH; + if (idx == 0 && scheduleLines.length > 1) { + BERUtils.renderTexture( + CRNGui.GUI, + graphics, + false, + (a[LineComponent.REAL_TIME.i()] == null ? a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getMaxWidth() : a[LineComponent.REAL_TIME.i()].getX() + a[LineComponent.REAL_TIME.i()].getMaxWidth()) - 1, + a[LineComponent.SCHEDULED_TIME.i()].getY() - 1, + 0.0f, + 1, + 2, + uv32 * 21, + uv32 * 30, + uv32 * (21 + 7), + uv32 * (30 + 14), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light + ); + } else if (idx >= MAX_LINES - 1) { + BERUtils.renderTexture( + CRNGui.GUI, + graphics, + false, + (a[LineComponent.REAL_TIME.i()] == null ? a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getMaxWidth() : a[LineComponent.REAL_TIME.i()].getX() + a[LineComponent.REAL_TIME.i()].getMaxWidth()) - 1, + a[LineComponent.SCHEDULED_TIME.i()].getY() - 1, + 0.0f, + 1, + 2, + uv32 * 35, + uv32 * 30, + uv32 * (35 + 7), + uv32 * (30 + 14), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light + ); + } else { + BERUtils.renderTexture( + CRNGui.GUI, + graphics, + false, + (a[LineComponent.REAL_TIME.i()] == null ? a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getMaxWidth() : a[LineComponent.REAL_TIME.i()].getX() + a[LineComponent.REAL_TIME.i()].getMaxWidth()) - 1, + a[LineComponent.SCHEDULED_TIME.i()].getY() - 1, + 0.0f, + 1, + 2, + uv32 * 28, + uv32 * 30, + uv32 * (28 + 7), + uv32 * (30 + 14), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light + ); + } + }); + } + }); } + + } - if (blockEntity.getTrainData().predictions().size() <= 1) { + @Override + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { + if (blockEntity.getTrainData() == null || blockEntity.getTrainData().isEmpty()) { return; } + TrainDisplayData data = blockEntity.getTrainData(); + boolean wasNextStopAnnounced = nextStopAnnounced; + nextStopAnnounced = !data.isWaitingAtStation() && data.getNextStop().isPresent() && data.getNextStop().get().getRealTimeArrivalTime() - DragonLib.getCurrentWorldTime() < ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get(); + this.exitSide = !nextStopAnnounced && !data.isWaitingAtStation() ? TrainExitSide.UNKNOWN : (data.isWaitingAtStation() ? exitSide : blockEntity.relativeExitDirection.get()); + + if (blockEntity.getXSizeScaled() > 1 && nextStopAnnounced && !wasNextStopAnnounced && data.getNextStop().isPresent()) { + DataAccessor.getFromServer(new NextConnectionsRequestData(data.getNextStop().get().getName(), data.getTrainData().getId()), ModAccessorTypes.GET_NEXT_CONNECTIONS_DISPLAY_DATA, (res) -> { + nextConnections = res; + updateLayout(blockEntity, data); + updateContent(blockEntity, data); + }); + } - // DESTINATION - pred = blockEntity.getTrainData().getLastStop().get(); - rawTime = (int)(blockEntity.getLastRefreshedTime() % 24000 + pred.departureTicks() + DragonLib.DAYTIME_SHIFT); - line = TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get())); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(4, true) - .withStretchScale(0.08f, 0.14f) - .withStencil(0, 7) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, y + 0.3f, 0.0f, 1, 0.14f)) - .build() - ); - line = TextUtils.text(pred.stationTagName()).withStyle(ChatFormatting.BOLD); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.2f, 0.2f) - .withStencil(0, maxWidth) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(10.0f, y, 0.0f, 1, 0.2f)) - .build() - ); + if (reason == EUpdateReason.LAYOUT_CHANGED || !nextStopAnnounced) { + updateLayout(blockEntity, data); + nextConnections = null; + } + updateContent(blockEntity, data); } - private void updateNextConnections(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - - // ### CONTENT PANEL - if (notInService(blockEntity)) { - return; + private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayData data) { + timeLabel + .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - timeLabel.getTextWidth() - (this.exitSide != TrainExitSide.UNKNOWN ? 4 : 0), 2.5f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + carriageLabel + .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) + .setPos(timeLabel.getX() - 4 - carriageLabel.getTextWidth(), 2.5f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + trainLineLabel + .setText(nextStopAnnounced ? ELanguage.translate(keyNextStop, data.getNextStop().get().getName()) : TextUtils.text(data.getTrainData().getName()).withStyle(ChatFormatting.BOLD)) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6 - (blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? timeLabel.getTextWidth() - 4 : 0) - (blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? carriageLabel.getTextWidth() - 5 : 0) - (this.exitSide != TrainExitSide.UNKNOWN ? 4 : 0), BoundsHitReaction.SCALE_SCROLL) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + speedLabel + .setText(ModUtils.calcSpeedString(data.getSpeed(), ModClientConfig.SPEED_UNIT.get()).withStyle(ChatFormatting.BOLD)) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + dateLabel + .setText(ELanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / Level.TICKS_PER_DAY, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + carriageInfoLabel + .setText(TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1))) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + + if (shouldRenderNextConnections() && !nextConnections.getConnections().isEmpty()) { + final int pages = (int)Math.ceil((float)nextConnections.getConnections().size() / (MAX_LINES - 1)); + final int page = (int)((DragonLib.getCurrentWorldTime() % (100 * pages)) / 100); + pageIndicatorLabel + .setText(TextUtils.text(generatePageIndexString(page, pages))) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + nextConnectionsTitleLabel + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + DLUtils.doIfNotNull(nextConnectionsLines, x -> { + for (int i = 0; i < MAX_LINES - 1; i++) { + final int k = i; + final int connectionIdx = i + (page * (MAX_LINES - 1)); + DLUtils.doIfNotNull(nextConnectionsLines[i], a -> { + if (connectionIdx >= nextConnections.getConnections().size()) { + a[LineComponent.SCHEDULED_TIME.i()].setText(TextUtils.empty()); + if (a[LineComponent.REAL_TIME.i()] != null) { + a[LineComponent.REAL_TIME.i()].setText(TextUtils.empty()); + } + a[LineComponent.TRAIN_NAME.i()].setText(TextUtils.empty()); + a[LineComponent.DESTINATION.i()].setText(TextUtils.empty()); + a[LineComponent.PLATFORM.i()].setText(TextUtils.empty()); + return; + } + + TrainStopDisplayData stop = nextConnections.getConnections().get(connectionIdx); + a[LineComponent.PLATFORM.i()] + .setText(TextUtils.text(stop.getStationInfo().platform())) + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - a[LineComponent.PLATFORM.i()].getTextWidth(), 7.5f + k * 1.7f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + if (a[LineComponent.REAL_TIME.i()] != null) { + a[LineComponent.SCHEDULED_TIME.i()] + .setPos(3, 7.5f + k * 1.7f) + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + a[LineComponent.REAL_TIME.i()] + .setPos(a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getTextWidth() + 1, 7.5f + k * 1.7f) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setColor(stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) + ; + } else { + a[LineComponent.SCHEDULED_TIME.i()] + .setPos(3, 7.5f + k * 1.7f) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setColor(stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) + ; + } + float pX = a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getTextWidth() + 1 + (a[LineComponent.REAL_TIME.i()] == null ? 0 : a[LineComponent.REAL_TIME.i()].getTextWidth() + 1); + a[LineComponent.TRAIN_NAME.i()] + .setPos(pX, 7.5f + k * 1.7f) + .setText(TextUtils.text(stop.getTrainName())) + .setMaxWidth(6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + a[LineComponent.DESTINATION.i()] + .setPos(pX + 7, 7.5f + k * 1.7f) + .setText(TextUtils.text(stop.getDestination())) + .setMaxWidth(a[LineComponent.PLATFORM.i()].getX() - 1 - pX - 7, BoundsHitReaction.SCALE_SCROLL) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + }); + } + }); + } else { + DLUtils.doIfNotNull(scheduleLines, x -> { + int totalStationsCount = data.getStopsFromCurrentStation().size(); + int linesCount = Math.min(scheduleLines.length, totalStationsCount); + for (int i = 0; i < linesCount; i++) { + final int j = i; + int k = i >= linesCount - 1 ? totalStationsCount - 1 : i; + DLUtils.doIfNotNull(scheduleLines[i], a -> { + TrainStopDisplayData stop = data.getStopsFromCurrentStation().get(k); + if (a[LineComponent.REAL_TIME.i()] != null) { + a[LineComponent.SCHEDULED_TIME.i()] + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledArrivalTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + a[LineComponent.REAL_TIME.i()] + .setPos(a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getTextWidth() + 1, 6 + j * 2) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeArrivalTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setColor(stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) + ; + } else { + a[LineComponent.SCHEDULED_TIME.i()] + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeArrivalTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setColor(stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) + ; + } + float pX = a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getTextWidth() + 3 + (a[LineComponent.REAL_TIME.i()] == null ? 0 : a[LineComponent.REAL_TIME.i()].getTextWidth() + 1); + a[LineComponent.DESTINATION.i()] + .setPos(pX, 6 + j * 2) + .setText(TextUtils.text(stop.getName()).withStyle(j >= linesCount - 1 ? ChatFormatting.BOLD : ChatFormatting.RESET)) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 3 - pX, BoundsHitReaction.SCALE_SCROLL) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + }); + } + }); } - - float y = PANEL_Y_START; - float maxWidth = displayWidth * 16 - 3; - - MutableComponent ln = TextUtils.text(generatePageIndexString()); - float rawTextWidth = Math.min(parent.getFontUtils().font.width(ln) * 0.2f, maxWidth - 16.0f); - parent.labels.add(new BERText(parent.getFontUtils(), ln, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth - 16.0f, true) - .withStretchScale(0.2f, 0.2f) - .withStencil(0, maxWidth - 16.0f) - .withCanScroll(false, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(maxWidth - rawTextWidth - 0.25f, y - 0.2f, 0.0f, 1, 0.2f)) - .build() - ); - - parent.labels.add(new BERText(parent.getFontUtils(), textNextConnections, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth - 6.0f - rawTextWidth, true) - .withStretchScale(0.1f, 0.15f) - .withStencil(0, maxWidth - 6.0f - rawTextWidth) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, y, 0.0f, 1, 0.15f)) - .build() - ); - y += PANEL_LINE_HEIGHT; - - if (this.nextConnections == null) { - return; + } + + private BERLabel[] createStationLine(AdvancedDisplayBlockEntity blockEntity, int index) { + BERLabel timeLabel = new BERLabel() + .setPos(3, 6 + index * 2) + .setScale(0.15f, 0.1f) + .setYScale(0.15f) + .setMaxWidth(6, BoundsHitReaction.CUT_OFF) + ; + BERLabel realTimeLabel = null; + if (blockEntity.getXSizeScaled() > 1) { + realTimeLabel = new BERLabel() + .setScale(0.15f, 0.1f) + .setYScale(0.15f) + .setMaxWidth(6, BoundsHitReaction.CUT_OFF) + ; } + BERLabel destinationLabel = new BERLabel() + .setScale(0.15f, 0.08f) + .setYScale(0.15f) + ; - for (int i = nextConnectionsPage * NEXT_CONNECTIONS_MAX_ENTRIES_PER_PAGE; i < (nextConnectionsPage + 1) * NEXT_CONNECTIONS_MAX_ENTRIES_PER_PAGE && i < nextConnections.size(); i++) { - - SimpleTrainConnection connection = nextConnections.get(i); - int rawTime = (int)(nextConnectionsRefreshTime % 24000 + connection.ticks() + DragonLib.DAYTIME_SHIFT); - MutableComponent line = TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get())); - - // Time - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(4, true) - .withStretchScale(0.08f, 0.14f) - .withStencil(0, 4) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(4.0f, y + 0.1f, 0.0f, 1, 0.14f)) - .build() - ); - - // Train Name - line = TextUtils.text(connection.trainName()); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(5, true) - .withStretchScale(0.1f, 0.16f) - .withStencil(0, 5) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(8.5f, y, 0.0f, 1, 0.16f)) - .build() - ); - - // Platform - line = TextUtils.text(connection.stationDetails().platform()); - rawTextWidth = Math.min(parent.getFontUtils().font.width(line) * 0.14f, maxWidth - 16.0f); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth - 16.0f, true) - .withStretchScale(0.14f, 0.16f) - .withStencil(0, maxWidth - 16.0f) - .withCanScroll(false, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(maxWidth - rawTextWidth, y, 0.0f, 1, 0.16f)) - .build() - ); + return new BERLabel[] { timeLabel, realTimeLabel, null, destinationLabel }; + } - // Destination - line = TextUtils.text(connection.scheduleTitle()); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth - 17.0f - rawTextWidth, true) - .withStretchScale(0.1f, 0.16f) - .withStencil(0, maxWidth - 17.0f - rawTextWidth) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(14.0f, y, 0.0f, 1, 0.16f)) - .build() - ); - y += PANEL_LINE_HEIGHT; + private BERLabel[] createNextConnectionsLine(AdvancedDisplayBlockEntity blockEntity, int index) { + BERLabel timeLabel = new BERLabel() + .setPos(3, 7 + index * 2) + .setScale(0.15f, 0.1f) + .setYScale(0.15f) + .setMaxWidth(6, BoundsHitReaction.CUT_OFF) + ; + BERLabel realTimeLabel = null; + if (blockEntity.getXSizeScaled() > 2) { + realTimeLabel = new BERLabel() + .setScale(0.15f, 0.1f) + .setYScale(0.15f) + .setMaxWidth(6, BoundsHitReaction.CUT_OFF) + ; } + BERLabel trainNameLabel = new BERLabel() + .setScale(0.15f, 0.08f) + .setYScale(0.15f) + ; + BERLabel destinationLabel = new BERLabel() + .setScale(0.15f, 0.08f) + .setYScale(0.15f) + ; + BERLabel platformLabel = new BERLabel() + .setScale(0.15f, 0.08f) + .setYScale(0.15f) + ; + + return new BERLabel[] { timeLabel, realTimeLabel, trainNameLabel, destinationLabel, platformLabel }; } - private String generatePageIndexString() { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < nextConnectionsPage; i++) { - sb.append(" □"); + private void updateLayout(AdvancedDisplayBlockEntity blockEntity, TrainDisplayData data) { + if (shouldRenderNextConnections()) { + for (int i = 0; i < MAX_LINES - 1; i++) { + this.nextConnectionsLines[i] = createNextConnectionsLine(blockEntity, i); + } + return; } - sb.append(" ■"); - for (int i = nextConnectionsPage + 1; i < nextConnectionsMaxPage; i++) { - sb.append(" □"); + int totalStationsCount = data.getStopsFromCurrentStation().size(); + int linesCount = Math.min(MAX_LINES, totalStationsCount); + this.scheduleLines = new BERLabel[linesCount][]; + for (int i = 0; i < linesCount; i++) { + this.scheduleLines[i] = createStationLine(blockEntity, i); } + } - return sb.toString(); + private static enum LineComponent { + SCHEDULED_TIME(0), + REAL_TIME(1), + TRAIN_NAME(2), + DESTINATION(3), + PLATFORM(4); + int i; + LineComponent(int i) { + this.i = i; + } + public int i() { + return i; + } } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java index 9f3e3947..41d13542 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java @@ -1,220 +1,141 @@ package de.mrjulsen.crn.client.ber.variants; -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.DeparturePrediction.TrainExitSide; -import de.mrjulsen.crn.event.listeners.JourneyListener.State; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; -import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.crn.data.TrainExitSide; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.client.util.BERUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.state.BlockState; public class BERPassengerInfoSimple implements IBERRenderSubtype { - private State state = State.WHILE_TRAVELING; - private static final String keyNextStop = "gui.createrailwaysnavigator.route_overview.next_stop"; + private static final String keyDate = "gui.createrailwaysnavigator.route_overview.date"; - private BERText announceNextStopLabel; - private BERText whileNextStopLabel; + private static final int TICKS_PER_SLIDE = 100; + + private TrainExitSide exitSide = TrainExitSide.UNKNOWN; + private final BERLabel label = new BERLabel() + .setPos(3, 5.5f) + .setYScale(0.75f) + .setScale(0.75f, 0.75f) + .setCentered(true) + .setScrollingSpeed(2) + ; @Override - public boolean isSingleLined() { - return true; + public void renderTick(float deltaTime) { + label.renderTick(); } + @Override - public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent) { - if (pBlockEntity.getTrainData() == null) { + public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { return; } - - DLUtils.doIfNotNull(announceNextStopLabel, x -> x.tick()); - DLUtils.doIfNotNull(whileNextStopLabel, x -> x.tick()); - boolean dirty = false; - if (pBlockEntity.getTrainData().getNextStop().isPresent()) { - if (this.state != State.WHILE_NEXT_STOP && pBlockEntity.getTrainData().getNextStop().get().departureTicks() <= 0) { - this.state = State.WHILE_NEXT_STOP; - dirty = true; - } else if (this.state != State.BEFORE_NEXT_STOP && pBlockEntity.getTrainData().getNextStop().get().departureTicks() <= ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get() && pBlockEntity.getTrainData().getNextStop().get().departureTicks() > 0) { - this.state = State.BEFORE_NEXT_STOP; - dirty = true; - } else if (this.state != State.WHILE_TRAVELING && pBlockEntity.getTrainData().getNextStop().get().departureTicks() > ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) { - this.state = State.WHILE_TRAVELING; - dirty = true; - } + float uv = 1.0f / 256.0f; + TrainExitSide side = exitSide; + if (backSide) { + side = side.getOpposite(); } - - if (dirty) { - update(level, pos, state, pBlockEntity, parent, EUpdateReason.DATA_CHANGED); - } - } - - @Override - public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - if (blockEntity.getTrainData() == null) { - return; + switch (side) { + case RIGHT: + BERUtils.renderTexture( + ModGuiIcons.ICON_LOCATION, + graphics, + false, + graphics.blockEntity().getXSizeScaled() * 16 - 3 - 8, + 4, + 0, + 8, + 8, + uv * ModGuiIcons.ARROW_RIGHT.getU(), + uv * ModGuiIcons.ARROW_RIGHT.getV(), + uv * (ModGuiIcons.ARROW_RIGHT.getU() + ModGuiIcons.ICON_SIZE), + uv * (ModGuiIcons.ARROW_RIGHT.getV() + ModGuiIcons.ICON_SIZE), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor()), + light + ); + break; + case LEFT: + BERUtils.renderTexture( + ModGuiIcons.ICON_LOCATION, + graphics, + false, + 3, + 4, + 0, + 8, + 8, + uv * ModGuiIcons.ARROW_LEFT.getU(), + uv * ModGuiIcons.ARROW_LEFT.getV(), + uv * (ModGuiIcons.ARROW_LEFT.getU() + ModGuiIcons.ICON_SIZE), + uv * (ModGuiIcons.ARROW_LEFT.getV() + ModGuiIcons.ICON_SIZE), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor()), + light + ); + break; + default: + break; } - - parent.labels.clear(); - announceNextStopLabel = null; - whileNextStopLabel = null; - switch (this.state) { - case BEFORE_NEXT_STOP: - updateAnnounceNextStop(level, pos, state, blockEntity, parent); - break; - case WHILE_NEXT_STOP: - updateWhileNextStop(level, pos, state, blockEntity, parent); + graphics.poseStack().pushPose(); + switch (side) { + case LEFT: + graphics.poseStack().translate(10, 0, 0); break; default: - updateDefault(level, pos, state, blockEntity, parent); break; } + label.render(graphics, light); + graphics.poseStack().popPose(); } - + @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - if (state == State.WHILE_NEXT_STOP || state == State.BEFORE_NEXT_STOP) { - context.renderUtils().initRenderEngine(); - TrainExitSide side = pBlockEntity.relativeExitDirection.get(); - float uv = 1.0f / 256.0f; - - if (backSide) { - side = side.getOpposite(); - } + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason data) { + if (blockEntity.getTrainData() == null ||blockEntity.getTrainData().isEmpty()) { + return; + } - switch (side) { - case RIGHT: - context.renderUtils().renderTexture( - ModGuiIcons.ICON_LOCATION, - pBufferSource, - pBlockEntity, - pPoseStack, - false, - pBlockEntity.getXSizeScaled() * 16 - 3 - 8, - 4, - 0, - 8, - 8, - uv * ModGuiIcons.ARROW_RIGHT.getU(), - uv * ModGuiIcons.ARROW_RIGHT.getV(), - uv * (ModGuiIcons.ARROW_RIGHT.getU() + ModGuiIcons.ICON_SIZE), - uv * (ModGuiIcons.ARROW_RIGHT.getV() + ModGuiIcons.ICON_SIZE), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (pBlockEntity.getColor()), - pPackedLight - ); - break; - case LEFT: - context.renderUtils().renderTexture( - ModGuiIcons.ICON_LOCATION, - pBufferSource, - pBlockEntity, - pPoseStack, - false, - 3f, - 4, - 0, - 8, - 8, - uv * ModGuiIcons.ARROW_LEFT.getU(), - uv * ModGuiIcons.ARROW_LEFT.getV(), - uv * (ModGuiIcons.ARROW_LEFT.getU() + ModGuiIcons.ICON_SIZE), - uv * (ModGuiIcons.ARROW_LEFT.getV() + ModGuiIcons.ICON_SIZE), - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (pBlockEntity.getColor()), - pPackedLight - ); - break; - default: - break; + this.exitSide = blockEntity.getTrainData().isWaitingAtStation() ? exitSide : blockEntity.relativeExitDirection.get(); + if (!blockEntity.getTrainData().getNextStop().isPresent()) { + label.setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName())); + } else if (blockEntity.getTrainData().isWaitingAtStation()) { + label.setText(TextUtils.text(blockEntity.getTrainData().getNextStop().get().getName())); + } else if (blockEntity.getTrainData().getNextStop().get().getRealTimeArrivalTime() - DragonLib.getCurrentWorldTime() < ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) { + label.setText(ELanguage.translate(keyNextStop, blockEntity.getTrainData().getNextStop().get().getName())); + } else { + final int slides = 3; + int slide = (int)(DragonLib.getCurrentWorldTime() % (TICKS_PER_SLIDE * slides)) / TICKS_PER_SLIDE; + switch (slide) { + case 0 -> label.setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName() + " " + blockEntity.getTrainData().getNextStop().get().getDestination())); + case 1 -> label.setText(ELanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / Level.TICKS_PER_DAY, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))); + case 2 -> label.setText(ModUtils.calcSpeedString(blockEntity.getTrainData().getSpeed(), ModClientConfig.SPEED_UNIT.get())); } - - if (backSide) { - switch (side) { - case LEFT: - pPoseStack.translate(10, 0, 0); - break; - case RIGHT: - pPoseStack.translate(-10, 0, 0); - break; - default: - break; - } - } - - DLUtils.doIfNotNull(announceNextStopLabel, x -> x.render(pPoseStack, pBufferSource, pPackedLight)); - DLUtils.doIfNotNull(whileNextStopLabel, x -> x.render(pPoseStack, pBufferSource, pPackedLight)); + this.exitSide = TrainExitSide.UNKNOWN; } - } - - private void updateDefault(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - - MutableComponent line = TextUtils.text(blockEntity.getTrainData().trainName()).append(" ").append(TextUtils.text(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().scheduleTitle() : "")); - float maxWidth = displayWidth * 16 - 6; - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3, 5.5f, 0.0f, 1, 0.75f)) - .build() - ); - } - - private void updateAnnounceNextStop(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - TrainExitSide side = blockEntity.relativeExitDirection.get(); - - MutableComponent line = ELanguage.translate(keyNextStop, GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().stationTagName() : "").getAliasName().get()); - float maxWidth = displayWidth * 16 - 6 - (side != TrainExitSide.UNKNOWN ? 10 : 0); - announceNextStopLabel = (new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3 + (side == TrainExitSide.LEFT ? 10 : 0), 5.5f, 0.0f, 1, 0.75f)) - .build() - ); - } - - private void updateWhileNextStop(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { - int displayWidth = blockEntity.getXSizeScaled(); - - TrainExitSide side = blockEntity.relativeExitDirection.get(); - MutableComponent line = TextUtils.text(GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().stationTagName() : "").getAliasName().get()); - float maxWidth = displayWidth * 16 - 6 - (side != TrainExitSide.UNKNOWN ? 10 : 0); - whileNextStopLabel = (new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3 + (side == TrainExitSide.LEFT ? 10 : 0), 5.5f, 0.0f, 1, 0.75f)) - .build() - ); + label + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6 - (exitSide == TrainExitSide.UNKNOWN ? 0 : 10), BoundsHitReaction.SCROLL) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java index fdc502e0..3d560f3b 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java @@ -1,27 +1,30 @@ package de.mrjulsen.crn.client.ber.variants; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.data.train.portable.StationDisplayData; import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; @@ -29,202 +32,226 @@ public class BERPlatformDetailed implements IBERRenderSubtype lastPredictions = new ArrayList<>(); + private static final float LINE_HEIGHT = 5.4f; - private BERText timeLabel; - private BERText[][] additionalLabels; - - private int timer; - private static final int MAX_TIMER = 100; - private boolean showTime = false; + private boolean showInfoLine = false; + private MutableComponent infoLineText = TextUtils.empty(); + private int maxLines = 0; + + private final BERLabel timeLabel = new BERLabel(TextUtils.empty()) + .setCentered(true) + .setScale(0.4f, 0.4f) + .setYScale(0.4f); + private final BERLabel statusLabel = new BERLabel(TextUtils.empty()) + .setCentered(true) + .setScale(0.4f, 0.4f) + .setYScale(0.4f) + .setColor(0xFF111111) + .setBackground(0xFFFFFFFF, true) + .setScrollingSpeed(2); + private BERLabel[][] lines = new BERLabel[0][]; - @Override - public boolean isSingleLined() { - return false; - } @Override - public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent) { - if (timeLabel != null) timeLabel.tick(); - - if (additionalLabels != null) { - for (int i = 0; i < additionalLabels.length; i++) { - if (additionalLabels[i] == null) { - continue; - } - - for (int k = 0; k < additionalLabels[i].length; k++) { - if (additionalLabels[i][k] == null) { - continue; - } - - additionalLabels[i][k].tick(); + public void renderTick(float deltaTime) { + timeLabel.renderTick(); + statusLabel.renderTick(); + DLUtils.doIfNotNull(lines, x -> { + for (int i = 0; i < x.length; i++) { + BERLabel[] line = x[i]; + if (line == null) continue; + for (int k = 0; k < line.length; k++) { + DLUtils.doIfNotNull(line[k], y -> y.renderTick()); } } - } + }); + } - timer++; - if ((timer %= MAX_TIMER) == 0) { - showTime = !showTime; - } + @Override + public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { + timeLabel + .setText(ELanguage.translate(keyTime, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + ; } @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - if (additionalLabels != null) { - for (int i = 0; i < additionalLabels.length; i++) { - if (additionalLabels[i] == null) { + public void render(BERGraphics graphics, float pPartialTicks, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + for (int i = 0; i < lines.length && i < maxLines; i++) { + for (int k = 0; k < lines[i].length; k++) { + if (i >= maxLines - 1 && (DragonLib.getCurrentWorldTime() % 200 > 100)) { + timeLabel.render(graphics, light); continue; } + lines[i][k].render(graphics, light); + } + } - if (i >= pBlockEntity.getPlatformInfoLinesCount() - 1 && showTime) { - break; - } else { - for (int k = 0; k < additionalLabels[i].length; k++) { - if (additionalLabels[i][k] == null) { - continue; - } - - additionalLabels[i][k].render(pPoseStack, pBufferSource, pPackedLight); - } + if (lines.length < maxLines) { + timeLabel.render(graphics, light); + } - if (i >= pBlockEntity.getPlatformInfoLinesCount() - 1) { - return; - } - } - } + if (showInfoLine) { + statusLabel.render(graphics, light); } - timeLabel.render(pPoseStack, pBufferSource, pPackedLight); } @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - - parent.labels.clear(); - - List preds = blockEntity.getPredictions().stream().filter(x -> x.departureTicks() < ModClientConfig.DISPLAY_LEAD_TIME.get()).toList(); + List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getScheduledArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); + + showInfoLine = !preds.isEmpty() && preds.get(0).getStationData().isDepartureDelayed() && preds.get(0).getTrainData().hasStatusInfo(); + if (showInfoLine) { + // Update status label + this.infoLineText = TextUtils.concat(TextUtils.text(" +++ "), preds.stream().limit(maxLines).filter(x -> x.getTrainData().hasStatusInfo() && x.getStationData().isDepartureDelayed()).flatMap(x -> { + Collection content = new ArrayList<>(); + if (x.getTrainData().isCancelled()) { + content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_cancelled", x.getTrainData().getName())); + return content.stream(); + } + content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_delayed", x.getTrainData().getName(), TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation()))); + for (CompiledTrainStatus status : x.getTrainData().getStatus()) { + content.add(status.text()); + } + return content.stream(); + }).toArray(Component[]::new)); + } else { + infoLineText = TextUtils.empty(); + } - if (preds.size() <= 0) { - additionalLabels = null; - setTimer(level, pos, state, blockEntity, parent, reason, 4f); - return; + int defaultMaxLines = blockEntity.getYSizeScaled() * 3 - 1; + this.maxLines = defaultMaxLines - (showInfoLine ? 1 : 0); + int maxIndices = Math.max(0, Math.min(this.maxLines, preds.size())); + if (reason == EUpdateReason.LAYOUT_CHANGED || this.lines == null || lines.length != maxIndices) { + updateLayout(blockEntity, preds, maxIndices); + } + + for (int i = 0; i < this.lines.length; i++) { + StationDisplayData stop = preds.get(i); + updateContent(blockEntity, stop, i); } - - int maxLines = blockEntity.getPlatformInfoLinesCount(); - boolean refreshAll = reason != EUpdateReason.DATA_CHANGED || !DLUtils.compareCollections(lastPredictions, preds, (a, b) -> a.stationInfo().platform().equals(b.stationInfo().platform()) && a.trainId().equals(b.trainId())); - - lastPredictions = preds; - if (refreshAll) { - additionalLabels = null; - timeLabel = null; - additionalLabels = new BERText[Math.min(preds.size(), maxLines)][]; + statusLabel + .setText(infoLineText) + .setPos(3, blockEntity.getYSizeScaled() * 16 - 12 * statusLabel.getYScale() - 2) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) + ; + } - for (int i = 0; i < additionalLabels.length; i++) { - additionalLabels[i] = addLine(level, pos, state, blockEntity, parent, reason, i, 4 + (i * 5.4f)); - } - setTimer(level, pos, state, blockEntity, parent, reason, 4 + ((additionalLabels.length < maxLines ? additionalLabels.length : maxLines - 1) * 5.34f)); + private void updateLayout(AdvancedDisplayBlockEntity blockEntity, List preds, int maxIndices) { + this.lines = new BERLabel[maxIndices][]; + for (int i = 0; i < this.lines.length; i++) { + StationDisplayData stop = preds.get(i); + this.lines[i] = createLine(blockEntity, stop, i); + updateContent(blockEntity, stop, i); } - + statusLabel + .setBackground((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF), false) + ; + timeLabel + .setPos(3, 3 + (Math.min(lines.length, maxLines) - (lines.length < maxLines ? 0 : 1)) * LINE_HEIGHT) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; } - - private BERText[] addLine(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason, int predictionIdx, float y) { - float displayWidth = blockEntity.getXSizeScaled() * 16 - 4; - - BERText[] labels = new BERText[4]; - - // PLATFORM - Component label = TextUtils.text(lastPredictions.get(predictionIdx).stationInfo().platform()); - float labelWidth = blockEntity.getPlatformWidth() < 0 ? parent.getFontUtils().font.width(label) * 0.4f : Math.min(parent.getFontUtils().font.width(label) * 0.4f, blockEntity.getPlatformWidth() - 2); - int platformMaxWidth = blockEntity.getPlatformWidth() < 0 ? (int)(displayWidth - 6) : blockEntity.getPlatformWidth() - 2; - - BERText lastLabel = new BERText(parent.getFontUtils(), label, 0) - .withIsCentered(false) - .withMaxWidth(platformMaxWidth, true) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, platformMaxWidth) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(blockEntity.getXSizeScaled() * 16 - 3 - labelWidth, y, 0.01f, 1, 0.4f)) - .build(); - labels[0] = lastLabel; - - // TIME - labels[1] = new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - switch (blockEntity.getTimeDisplay()) { - case ETA: - texts.add(TextUtils.text(ModUtils.timeRemainingString(lastPredictions.get(predictionIdx).departureTicks()))); - break; - default: - int rawTime = (int)(blockEntity.getLastRefreshedTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT + lastPredictions.get(predictionIdx).departureTicks()); - texts.add(TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get()))); - break; - } - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(TIME_LABEL_WIDTH - 4, true) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, TIME_LABEL_WIDTH - 4) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withRefreshRate(100) - .withPredefinedTextTransformation(new TextTransformation(3, y, 0.01f, 1, 0.4f)) - .build() - ; - - float platformWidth = blockEntity.getPlatformWidth() < 0 ? lastLabel.getScaledTextWidth() + 2 : blockEntity.getPlatformWidth(); - int trainNameWidth = blockEntity.getTrainNameWidth(); - - lastLabel = new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - texts.add(TextUtils.text(lastPredictions.get(predictionIdx).trainName())); - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(Math.min(trainNameWidth - 1, displayWidth - TIME_LABEL_WIDTH - platformWidth - 1), false) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, Math.min(trainNameWidth - 1, displayWidth - TIME_LABEL_WIDTH - platformWidth - 1)) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH, y, 0.01f, 1, 0.4f)) - .build() - ; - labels[2] = lastLabel; - - lastLabel = new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - texts.add(TextUtils.text(lastPredictions.get(predictionIdx).scheduleTitle())); - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(displayWidth - TIME_LABEL_WIDTH - trainNameWidth - platformWidth + 1, true) - .withStretchScale(0.25f, 0.4f) - .withStencil(0, displayWidth - TIME_LABEL_WIDTH - trainNameWidth - platformWidth + 1) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH + trainNameWidth, y, 0.01f, 1, 0.4f)) - .build() + private void updateContent(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index) { + boolean isLast = stop.isLastStop(); + BERLabel[] components = lines[index]; + components[LineComponent.TIME.i()] + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + ; + components[LineComponent.REAL_TIME.i()] + .setText(TextUtils.text(stop.getTrainData().isCancelled() ? + " \u274C " : // X + (stop.getStationData().isDepartureDelayed() ? + (ModUtils.formatTime(stop.getRealTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)) : + ""))) // Nothing (not delayed) + ; + components[LineComponent.TRAIN_NAME.i()] + .setText(TextUtils.text(stop.getTrainData().getName())) ; + components[LineComponent.PLATFORM.i()] + .setText(blockEntity.isPlatformFixed() ? + TextUtils.empty() : + TextUtils.text(stop.getStationData().getStationInfo().platform())) + ; + components[LineComponent.DESTINATION.i()] + .setText(isLast ? + ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", stop.getFirstStopName()) : + TextUtils.text(stop.getStationData().getDestination())) + ; + + int x = 3; + components[LineComponent.TIME.i()].setPos(x, 3 + index * LINE_HEIGHT); + x += components[LineComponent.TIME.i()].getTextWidth() + 2; + components[LineComponent.REAL_TIME.i()].setPos(x, 3 + index * LINE_HEIGHT); + x += components[LineComponent.REAL_TIME.i()].getTextWidth() + 2 + (!components[LineComponent.REAL_TIME.i()].getText().getString().isEmpty() ? 2 : 0); - labels[3] = lastLabel; - return labels; - } + BERLabel trainNameLabel = components[LineComponent.TRAIN_NAME.i()] + .setPos(x, 3 + index * LINE_HEIGHT) + .setMaxWidth(blockEntity.getTrainNameWidth(), BoundsHitReaction.SCALE_SCROLL) + ; + x += trainNameLabel.getMaxWidth() + 2; + + BERLabel platformLabel = components[LineComponent.PLATFORM.i()]; + float platformWidth = platformLabel.getTextWidth(); + platformLabel.setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformWidth, 3 + index * LINE_HEIGHT); + components[LineComponent.DESTINATION.i()].setPos(x, 3 + index * LINE_HEIGHT); + components[LineComponent.DESTINATION.i()].setMaxWidth(blockEntity.getXSizeScaled() * 16 - 3 - x - platformWidth - 3, BoundsHitReaction.SCALE_SCROLL); + } + + private BERLabel[] createLine(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index) { + BERLabel[] components = new BERLabel[5]; + + components[LineComponent.TIME.i()] = new BERLabel() + .setYScale(0.4f) + .setMaxWidth(12, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + components[LineComponent.REAL_TIME.i()] = new BERLabel() + .setYScale(0.4f) + .setMaxWidth(12, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setBackground((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF), false) + .setColor(0xFF111111) + ; + components[LineComponent.TRAIN_NAME.i()] = new BERLabel() + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + + components[LineComponent.PLATFORM.i()] = new BERLabel() + .setYScale(0.4f) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; - public void setTimer(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason, float y) { - float displayWidth = blockEntity.getXSizeScaled() * 16 - 6; - timeLabel = new BERText(parent.getFontUtils(), () -> List.of(ELanguage.translate(keyTime, TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get()))), 0) - .withIsCentered(true) - .withMaxWidth(displayWidth, true) - .withStretchScale(0.4f, 0.4f) - .withStencil(0, displayWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withTicksPerPage(100) - .withRefreshRate(16) - .withPredefinedTextTransformation(new TextTransformation(3, y, 0.0f, 1, 0.4f)) - .build() + components[LineComponent.DESTINATION.i()] = new BERLabel() + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) ; + + return components; + } + + private static enum LineComponent { + TIME(0), + REAL_TIME(1), + TRAIN_NAME(2), + DESTINATION(3), + PLATFORM(4); + + int index; + LineComponent(int index) { + this.index = index; + } + public int i() { + return index; + } } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformInformative.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformInformative.java index 338197da..41a8d1df 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformInformative.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformInformative.java @@ -1,26 +1,29 @@ package de.mrjulsen.crn.client.ber.variants; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.data.train.portable.StationDisplayData; import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.client.util.BERUtils; import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; import net.minecraft.ChatFormatting; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.world.level.Level; @@ -28,320 +31,368 @@ import net.minecraft.world.level.block.state.BlockState; public class BERPlatformInformative implements IBERRenderSubtype { - - private List lastPredictions = new ArrayList<>(); - - private static final int TIME_LABEL_WIDTH = 16; - private static final int SPACING = 4; - - private static final String keyPlatform = "gui.createrailwaysnavigator.platform"; - private static final String keyLine = "gui.createrailwaysnavigator.line"; - private static final String keyDestination = "gui.createrailwaysnavigator.destination"; - private static final String keyDeparture = "gui.createrailwaysnavigator.departure"; + private static final String keyFollowingTrains = "gui.createrailwaysnavigator.following_trains"; - - // cache - private boolean wasPlatformFixed; + private static final float LINE_HEIGHT = 5.4f; - @Override - public boolean isSingleLined() { - return false; - } + private int maxLines = 0; + private boolean showInfoLine = false; + private Component infoLineText = TextUtils.empty(); + private BERLabel statusLabel; + private BERLabel[] focusArea; + private BERLabel[][] lines; + private final BERLabel followingTrainsLabel = new BERLabel(ELanguage.translate(keyFollowingTrains)).setPos(3, 16).setScale(0.2f, 0.2f).setYScale(0.2f); + @Override - public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent) { - + public void renderTick(float deltaTime) { + DLUtils.doIfNotNull(statusLabel, x -> x.renderTick()); + DLUtils.doIfNotNull(focusArea, x -> { + for (int i = 0; i < x.length; i++) { + DLUtils.doIfNotNull(x[i], y -> y.renderTick()); + } + }); + DLUtils.doIfNotNull(lines, x -> { + for (int i = 0; i < x.length; i++) { + DLUtils.doIfNotNull(x[i], y -> { + for (int j = 0; j < y.length; j++) { + DLUtils.doIfNotNull(y[j], z -> z.renderTick()); + } + }); + } + }); } - private boolean extendedDisplay(AdvancedDisplayBlockEntity blockEntity) { + private boolean isExtendedDisplay(AdvancedDisplayBlockEntity blockEntity) { return blockEntity.getYSize() > 1; } @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - boolean isPlatformFixed = pBlockEntity.isPlatformFixed(); - context.renderUtils().initRenderEngine(); - if (!isPlatformFixed) { - context.renderUtils().fillColor(pBufferSource, pBlockEntity, (0xFF << 24) | (pBlockEntity.getColor() & 0x00FFFFFF), pPoseStack, 2.5f, 6.0f, 0.0f, pBlockEntity.getXSizeScaled() * 16 - 5, 0.25f, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); - - int count = pBlockEntity.getPlatformInfoLinesCount(); - for (int i = 0; i < count; i += 2) { - context.renderUtils().fillColor(pBufferSource, pBlockEntity, 0x22FFFFFF, pPoseStack, 2, 4 + ((i + 1) * 5.34f) - 1f, 0.0f, pBlockEntity.getXSizeScaled() * 16 - 4, 5.34f, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); + public void render(BERGraphics graphics, float pPartialTicks, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + if (isExtendedDisplay(graphics.blockEntity())) { + BERUtils.fillColor(graphics, 2.5f, 15.5f, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); + followingTrainsLabel + .setColor((0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF)) + ; + followingTrainsLabel.render(graphics, light); + } + + DLUtils.doIfNotNull(focusArea, a -> { + for (int i = 0; i < a.length; i++) { + BERLabel label = a[i]; + if (label == null) continue; + + graphics.poseStack().pushPose(); + if (backSide) { + float maxWidth = graphics.blockEntity().getXSizeScaled() * 16; + if (i == LineComponent.TIME.i()) { + graphics.poseStack().translate(-label.getX() + maxWidth - 3 - label.getTextWidth(), 0, 0); + } else if (i == LineComponent.REAL_TIME.i()) { + graphics.poseStack().translate(-label.getX() + maxWidth - 3 - label.getTextWidth(), 0, 0); + } else if (i == LineComponent.TRAIN_NAME.i()) { + graphics.poseStack().translate(-label.getX() + maxWidth - 3 - label.getTextWidth(), 0, 0); + } else if (i == LineComponent.DESTINATION.i()) { + graphics.poseStack().translate(-label.getX() + 5 + a[LineComponent.PLATFORM.i()].getTextWidth(), 0, 0); + } else if (i == LineComponent.STOPOVERS.i()) { + graphics.poseStack().translate(-label.getX() + 5 + a[LineComponent.PLATFORM.i()].getTextWidth(), 0, 0); + } else if (i == LineComponent.PLATFORM.i()) { + graphics.poseStack().translate(-label.getX() + 3, 0, 0); + } + } + + label.render(graphics, light); + graphics.poseStack().popPose(); } - } else { - if (extendedDisplay(pBlockEntity)) { - context.renderUtils().fillColor(pBufferSource, pBlockEntity, (0xFF << 24) | (pBlockEntity.getColor() & 0x00FFFFFF), pPoseStack, 2.5f, 15.5f, 0.0f, pBlockEntity.getXSizeScaled() * 16 - 5, 0.25f, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); + }); + DLUtils.doIfNotNull(lines, x -> { + for (BERLabel[] line : x) { + if (line == null) continue; + for (BERLabel label : line) { + if (label == null) continue; + label.render(graphics, light); + } } + }); + + if (statusLabel != null && !statusLabel.getText().getString().isBlank()) { + graphics.poseStack().pushPose(); + if (backSide && focusArea != null && focusArea[LineComponent.PLATFORM.i()] != null) { + graphics.poseStack().translate(-statusLabel.getX() + 5 + focusArea[LineComponent.PLATFORM.i()].getTextWidth(), 0, 0); + } + DLUtils.doIfNotNull(statusLabel, x -> x.render(graphics, light)); + graphics.poseStack().popPose(); } } @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - List preds = blockEntity.getPredictions(); - boolean isPlatformFixed = blockEntity.isPlatformFixed(); - /* - if (preds.size() <= 0) { - parent.labels.clear(); + List preds = blockEntity.getStops().stream().filter(x -> !x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get()).toList(); + + if (preds.isEmpty()) { + lines = null; + focusArea = null; + statusLabel = null; return; } - */ - - int maxLines = blockEntity.getPlatformInfoLinesCount() - (isPlatformFixed ? 1 : 0) ; - boolean refreshAll = reason != EUpdateReason.DATA_CHANGED || - !DLUtils.compareCollections(lastPredictions, preds, (a, b) -> a.stationInfo().platform().equals(b.stationInfo().platform()) && a.trainId().equals(b.trainId())) || - wasPlatformFixed != isPlatformFixed - ; - - lastPredictions = preds; - wasPlatformFixed = blockEntity.isPlatformFixed(); + + if (reason == EUpdateReason.LAYOUT_CHANGED || this.lines == null || this.focusArea == null) { + updateLayout(blockEntity, preds); + } - if (refreshAll) { - parent.labels.clear(); - if (isPlatformFixed) { - addNextDeparture(level, pos, state, blockEntity, parent, reason, 0); + showInfoLine = preds.get(0).getStationData().isDepartureDelayed() && preds.get(0).getTrainData().hasStatusInfo(); + if (showInfoLine) { + // Update status label + Collection content = new ArrayList<>(); + if (preds.get(0).getTrainData().isCancelled()) { + content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled")); } else { - addHeader(level, pos, state, blockEntity, parent, reason); - } - - if (isPlatformFixed && preds.size() > 0) { - for (int i = 1; i < maxLines && i < preds.size(); i++) { - addLine(level, pos, state, blockEntity, parent, reason, i, 4 + ((i + 2) * 5.4f)); - } - } else { - for (int i = 0; i < maxLines && i < preds.size(); i++) { - addLine(level, pos, state, blockEntity, parent, reason, i, 4 + ((i + 1) * 5.34f)); + content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed", TimeUtils.formatToMinutes(preds.get(0).getStationData().getDepartureTimeDeviation()))); + for (CompiledTrainStatus status : preds.get(0).getTrainData().getStatus()) { + content.add(status.text()); } } + this.infoLineText = TextUtils.concat(TextUtils.text(" +++ "), content); + } else { + infoLineText = TextUtils.empty(); + } + + updateFocusContent(blockEntity, preds.get(0)); + for (int i = 1; i < this.lines.length && i < preds.size(); i++) { + StationDisplayData stop = preds.get(i); + updateTableContent(blockEntity, stop, i); } } - private void addLine(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason, int predictionIdx, float y) { - float displayWidth = blockEntity.getXSizeScaled() * 16 - 4; - - // PLATFORM - Component label = blockEntity.isPlatformFixed() ? TextUtils.empty() : TextUtils.text(lastPredictions.get(predictionIdx).stationInfo().platform()); - float labelWidth = blockEntity.getPlatformWidth() < 0 ? parent.getFontUtils().font.width(label) * 0.4f : Math.min(parent.getFontUtils().font.width(label) * 0.4f, blockEntity.getPlatformWidth() - 2); - int platformMaxWidth = blockEntity.getPlatformWidth() < 0 ? (int)(displayWidth - 6) : blockEntity.getPlatformWidth() - 2; - - BERText lastLabel = new BERText(parent.getFontUtils(), label, 0) - .withIsCentered(false) - .withMaxWidth(platformMaxWidth, true) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, platformMaxWidth) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(blockEntity.getXSizeScaled() * 16 - 3 - labelWidth, y, 0.01f, 1, 0.4f)) - .build(); - parent.labels.add(lastLabel); - - // TIME - parent.labels.add(new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - switch (blockEntity.getTimeDisplay()) { - case ETA: - texts.add(TextUtils.text(ModUtils.timeRemainingString(lastPredictions.get(predictionIdx).departureTicks()))); - break; - default: - int rawTime = (int)(blockEntity.getLastRefreshedTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT + lastPredictions.get(predictionIdx).departureTicks()); - texts.add(TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get()))); - break; - } - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(TIME_LABEL_WIDTH - 4, true) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, TIME_LABEL_WIDTH - 4) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withRefreshRate(100) - .withPredefinedTextTransformation(new TextTransformation(3, y, 0.01f, 1, 0.4f)) - .build() - ); - - float platformWidth = blockEntity.getPlatformWidth() < 0 ? lastLabel.getScaledTextWidth() + 2 : blockEntity.getPlatformWidth(); - int trainNameWidth = blockEntity.getTrainNameWidth(); - - // TRAIN NAME - lastLabel = new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - texts.add(TextUtils.text(lastPredictions.get(predictionIdx).trainName())); - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(Math.min(trainNameWidth - 1, displayWidth - TIME_LABEL_WIDTH - platformWidth - 1), false) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, Math.min(trainNameWidth - 1, displayWidth - TIME_LABEL_WIDTH - platformWidth - 1)) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH, y, 0.01f, 1, 0.4f)) - .build() - ; - parent.labels.add(lastLabel); - - // DESTINATION - lastLabel = new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - texts.add(TextUtils.text(lastPredictions.get(predictionIdx).scheduleTitle())); - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(displayWidth - TIME_LABEL_WIDTH - trainNameWidth - platformWidth + 1 - SPACING, true) - .withStretchScale(0.25f, 0.4f) - .withStencil(0, displayWidth - TIME_LABEL_WIDTH - trainNameWidth - platformWidth + 1 - SPACING) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH + trainNameWidth + SPACING, y, 0.01f, 1, 0.4f)) - .build() + private void updateLayout(AdvancedDisplayBlockEntity blockEntity, List preds) { + this.focusArea = new BERLabel[7]; + this.lines = new BERLabel[0][]; + + followingTrainsLabel + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) ; - - parent.labels.add(lastLabel); - } - private void addNextDeparture(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason, int predictionIdx) { - float displayWidth = blockEntity.getXSizeScaled() * 16 - 6; - int timeLabelWidth = TIME_LABEL_WIDTH - 3; - - // PLATFORM - Component label = TextUtils.text(blockEntity.getStationInfo().platform()).withStyle(ChatFormatting.BOLD); - float labelWidth = parent.getFontUtils().font.width(label) * 0.6f; - BERText lastLabel = new BERText(parent.getFontUtils(), label, 0) - .withIsCentered(false) - .withMaxWidth(TIME_LABEL_WIDTH, true) - .withStretchScale(0.6f, 0.6f) - .withStencil(0, displayWidth) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(displayWidth - labelWidth + 3, 3, 0.0f, 1, 1f)) - .build(); - parent.labels.add(lastLabel); - - if (lastPredictions.size() <= 0) { - return; - } + BERLabel timeLabel = new BERLabel() + .setPos(3, 3) + .setYScale(0.4f) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + focusArea[LineComponent.TIME.i()] = timeLabel; + + BERLabel realTimeLabel = new BERLabel() + .setPos(3, 7) + .setYScale(0.4f) + .setScale(0.4f, 0.2f) + .setBackground((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF), false) + .setColor(0xFF111111) + ; + focusArea[LineComponent.REAL_TIME.i()] = realTimeLabel; + + BERLabel trainNameLabel = new BERLabel() + .setPos(3, 7) + .setYScale(0.3f) + .setMaxWidth(12, BoundsHitReaction.IGNORE) + .setScale(0.3f, 0.15f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + focusArea[LineComponent.TRAIN_NAME.i()] = trainNameLabel; - float platformWidth = lastLabel.getScaledTextWidth(); - - // TIME - parent.labels.add(new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - switch (blockEntity.getTimeDisplay()) { - case ETA: - texts.add(TextUtils.text(ModUtils.timeRemainingString(lastPredictions.get(predictionIdx).departureTicks()))); - break; - default: - int rawTime = (int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT + lastPredictions.get(predictionIdx).departureTicks()); - texts.add(TextUtils.text(TimeUtils.parseTime(rawTime - rawTime % ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(), ModClientConfig.TIME_FORMAT.get()))); - break; + BERLabel platformLabel = new BERLabel() + .setYScale(0.8f) + .setScale(0.6f, 0.5f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + focusArea[LineComponent.PLATFORM.i()] = platformLabel; + + BERLabel destinationLabel = new BERLabel() + .setYScale(0.6f) + .setScale(0.6f, 0.4f) + .setScrollingSpeed(2) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + focusArea[LineComponent.DESTINATION.i()] = destinationLabel; + + BERLabel stopoversLabel = new BERLabel() + .setYScale(0.2f) + .setScale(0.2f, 0.1f) + .setScrollingSpeed(2) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + focusArea[LineComponent.STOPOVERS.i()] = stopoversLabel; + + statusLabel = new BERLabel() + .setText(infoLineText) + .setYScale(0.3f) + .setScale(0.3f, 0.3f) + .setScrollingSpeed(2) + .setBackground((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF), true) + .setColor(0xFF111111) + ; + + if (isExtendedDisplay(blockEntity)) { + maxLines = (blockEntity.getYSizeScaled() - 1) * 3; + int maxIndices = Math.min(this.maxLines, preds.size()); + this.lines = new BERLabel[Math.max(maxIndices, 0)][]; + for (int i = 1; i < this.lines.length; i++) { + StationDisplayData stop = preds.get(i); + this.lines[i] = createTableLine(blockEntity, stop, i); } - return texts; - }, 0) - .withIsCentered(false) - .withMaxWidth(timeLabelWidth, true) - .withStretchScale(0.2f, 0.4f) - .withStencil(0, timeLabelWidth) - .withRefreshRate(100) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3, 3, 0.0f, 1, 0.4f)) - .build() - ); - - parent.labels.add(new BERText(parent.getFontUtils(), TextUtils.text(lastPredictions.get(predictionIdx).trainName()), 0) - .withIsCentered(false) - .withMaxWidth(timeLabelWidth, true) - .withStretchScale(0.15f, 0.3f) - .withStencil(0, timeLabelWidth) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3, 7, 0.0f, 1, 0.3f)) - .build() - ); - - parent.labels.add(new BERText(parent.getFontUtils(), TextUtils.text(lastPredictions.get(predictionIdx).scheduleTitle()).withStyle(ChatFormatting.BOLD), 0) - .withIsCentered(false) - .withMaxWidth(displayWidth - timeLabelWidth - platformWidth - 5, true) - .withStretchScale(0.3f, 0.6f) - .withStencil(0, displayWidth - timeLabelWidth - platformWidth - 5) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3 + timeLabelWidth + 1, 8, 0.0f, 1, 0.6f)) - .build() - ); - - parent.labels.add(new BERText(parent.getFontUtils(), parent.getStopoversString(blockEntity), 0) - .withIsCentered(false) - .withMaxWidth(displayWidth - TIME_LABEL_WIDTH - platformWidth - 2, true) - .withStretchScale(0.2f, 0.25f) - .withStencil(0, displayWidth - TIME_LABEL_WIDTH - platformWidth - 2) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH + 1, 5, 0.0f, 1, 0.25f)) - .build() - ); - - if (extendedDisplay(blockEntity)) { - parent.labels.add(new BERText(parent.getFontUtils(), ELanguage.translate(keyFollowingTrains), 0) - .withIsCentered(false) - .withMaxWidth(displayWidth, true) - .withStretchScale(0.15f, 0.2f) - .withStencil(0, displayWidth) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3, 17, 0.0f, 1, 0.2f)) - .build() - ); } } - private void addHeader(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - float displayWidth = blockEntity.getXSizeScaled() * 16 - 4; - float TIME_LABEL_WIDTH = 16; - - // TIME - parent.labels.add(new BERText(parent.getFontUtils(), ELanguage.translate(keyDeparture).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC), 0) - .withIsCentered(false) - .withMaxWidth(TIME_LABEL_WIDTH - 4, true) - .withStretchScale(0.15f, 0.3f) - .withStencil(0, Math.min(TIME_LABEL_WIDTH - 4, displayWidth - 2)) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3, 3, 0.0f, 1, 0.3f)) - .build() - ); - - // PLATFORM - Component label = ELanguage.translate(keyPlatform).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC); - float labelWidth = blockEntity.getPlatformWidth() < 0 ? parent.getFontUtils().font.width(label) * 0.3f : Math.min(parent.getFontUtils().font.width(label) * 0.4f, blockEntity.getPlatformWidth() - 2); - int platformMaxWidth = blockEntity.getPlatformWidth() < 0 ? (int)(displayWidth - 6) : blockEntity.getPlatformWidth() - 2; + private BERLabel[] createTableLine(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index) { + BERLabel[] components = new BERLabel[5]; + + components[LineComponent.TIME.i()] = new BERLabel() + .setYScale(0.4f) + .setMaxWidth(12, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + components[LineComponent.REAL_TIME.i()] = new BERLabel() + .setYScale(0.4f) + .setMaxWidth(12, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setBackground((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF), false) + .setColor(0xFF111111) + ; + components[LineComponent.TRAIN_NAME.i()] = new BERLabel() + .setYScale(0.4f) + //.setMaxWidth(14, BoundsHitReaction.SCALE_SCROLL) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; - BERText lastLabel = new BERText(parent.getFontUtils(), label, 0) - .withIsCentered(false) - .withMaxWidth(platformMaxWidth, true) - .withStretchScale(0.15f, 0.3f) - .withStencil(0, labelWidth) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(blockEntity.getXSizeScaled() * 16 - 3 - labelWidth, 3, 0.0f, 1, 0.3f)) - .build(); - parent.labels.add(lastLabel); - - float platformWidth = blockEntity.getPlatformWidth() < 0 ? lastLabel.getScaledTextWidth() + 2 : blockEntity.getPlatformWidth(); - int trainNameWidth = blockEntity.getTrainNameWidth(); - - lastLabel = new BERText(parent.getFontUtils(), ELanguage.translate(keyLine).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC), 0) - .withIsCentered(false) - .withMaxWidth(Math.min(trainNameWidth - 1, displayWidth - TIME_LABEL_WIDTH - platformWidth - 1), true) - .withStretchScale(0.15f, 0.3f) - .withStencil(0, Math.min(trainNameWidth - 1, displayWidth - TIME_LABEL_WIDTH - platformWidth - 1)) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH, 3, 0.0f, 1, 0.3f)) - .build() - ; - parent.labels.add(lastLabel); - - lastLabel = new BERText(parent.getFontUtils(), ELanguage.translate(keyDestination).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC), 0) - .withIsCentered(false) - .withMaxWidth(displayWidth - TIME_LABEL_WIDTH - trainNameWidth - platformWidth + 1, true) - .withStretchScale(0.15f, 0.3f) - .withStencil(0, displayWidth - TIME_LABEL_WIDTH - trainNameWidth - platformWidth + 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(TIME_LABEL_WIDTH + trainNameWidth, 3, 0.0f, 1, 0.3f)) - .build() + components[LineComponent.PLATFORM.i()] = new BERLabel() + .setYScale(0.4f) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + + components[LineComponent.DESTINATION.i()] = new BERLabel() + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + + return components; + } + + private void updateFocusContent(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop) { + + followingTrainsLabel + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + + BERLabel timeLabel = focusArea[LineComponent.TIME.i()] + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + ; + + BERLabel realTimeLabel = focusArea[LineComponent.REAL_TIME.i()] + .setText(TextUtils.text(stop.isDelayed() ? ModUtils.formatTime(stop.getRealTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA) : "")) + ; + + BERLabel trainNameLabel = focusArea[LineComponent.TRAIN_NAME.i()] + .setText(TextUtils.text(stop.getTrainData().getName())) + .setPos(3, 7 + (stop.isDelayed() ? 4.5f : 0)) + .setMaxWidth(blockEntity.getTrainNameWidth(), BoundsHitReaction.SCALE_SCROLL) ; + + BERLabel platformLabel = focusArea[LineComponent.PLATFORM.i()]; + platformLabel + .setText(TextUtils.text(stop.getStationData().getStationInfo().platform()).withStyle(ChatFormatting.BOLD)) + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformLabel.getTextWidth(), 3); + ; + float platformWidth = platformLabel.getTextWidth(); + + float x = 5 + Math.max(trainNameLabel.getTextWidth(), Math.max(timeLabel.getTextWidth(), realTimeLabel.getTextWidth())); + float w = blockEntity.getXSizeScaled() * 16 - 5 - platformWidth - x; + focusArea[LineComponent.DESTINATION.i()] + .setText(stop.isLastStop() ? + ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.arrival") : + TextUtils.text(stop.getStationData().getDestination())) + .setPos(x, 8.5f) + .setMaxWidth(w, BoundsHitReaction.SCALE_SCROLL) + ; + + focusArea[LineComponent.STOPOVERS.i()] + .setText(stop.isLastStop() ? + ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", stop.getFirstStopName()) : + TextUtils.concat(TextUtils.text(" \u25CF "), stop.getStopovers().stream().map(a -> (Component)TextUtils.text(a)).toList())) + .setPos(x, 6) + .setMaxWidth(w, BoundsHitReaction.SCALE_SCROLL) + ; + + statusLabel + .setText(infoLineText) + .setPos(x, 2.5f) + .setMaxWidth(w, BoundsHitReaction.SCALE_SCROLL) + ; + } + + private void updateTableContent(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index) { + boolean isLast = stop.isLastStop(); + BERLabel[] components = lines[index]; + components[LineComponent.TIME.i()] + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + ; + components[LineComponent.REAL_TIME.i()] + .setText(TextUtils.text(stop.getTrainData().isCancelled() ? + " \u274C " : // X + (stop.getStationData().isDepartureDelayed() ? + (ModUtils.formatTime(isLast ? stop.getStationData().getRealTimeArrivalTime() : stop.getStationData().getRealTimeDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)) : + ""))) // Nothing (not delayed) + ; + components[LineComponent.TRAIN_NAME.i()] + .setText(TextUtils.text(stop.getTrainData().getName())) + ; + components[LineComponent.PLATFORM.i()] + .setText(blockEntity.isPlatformFixed() ? + TextUtils.empty() : + TextUtils.text(stop.getStationData().getStationInfo().platform())) + ; + components[LineComponent.DESTINATION.i()] + .setText(isLast ? + ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", stop.getFirstStopName()) : + TextUtils.text(stop.getStationData().getDestination())) + ; + + int x = 3; + components[LineComponent.TIME.i()].setPos(x, 11 + 3 + index * LINE_HEIGHT); + x += components[LineComponent.TIME.i()].getTextWidth() + 2; + components[LineComponent.REAL_TIME.i()].setPos(x, 11 + 3 + index * LINE_HEIGHT); + x += components[LineComponent.REAL_TIME.i()].getTextWidth() + 2 + (!components[LineComponent.REAL_TIME.i()].getText().getString().isEmpty() ? 2 : 0); + + BERLabel trainNameLabel = components[LineComponent.TRAIN_NAME.i()] + .setPos(x, 11 + 3 + index * LINE_HEIGHT) + .setMaxWidth(blockEntity.getTrainNameWidth(), BoundsHitReaction.SCALE_SCROLL) + ; + x += trainNameLabel.getMaxWidth() + 2; - parent.labels.add(lastLabel); + BERLabel platformLabel = components[LineComponent.PLATFORM.i()]; + float platformWidth = platformLabel.getTextWidth(); + platformLabel.setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformWidth, 11 + 3 + index * LINE_HEIGHT); + components[LineComponent.DESTINATION.i()].setPos(x, 11 + 3 + index * LINE_HEIGHT); + components[LineComponent.DESTINATION.i()].setMaxWidth(blockEntity.getXSizeScaled() * 16 - 3 - x - platformWidth - 3, BoundsHitReaction.SCALE_SCROLL); + } + + private static enum LineComponent { + TIME(0), + REAL_TIME(1), + TRAIN_NAME(2), + DESTINATION(3), + PLATFORM(4), + STOPOVERS(5); + + int index; + LineComponent(int index) { + this.index = index; + } + public int i() { + return index; + } } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java index f529fe8e..9c12a23e 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java @@ -1,28 +1,27 @@ package de.mrjulsen.crn.client.ber.variants; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.UUID; -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; +import de.mrjulsen.crn.data.train.portable.StationDisplayData; import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; @@ -32,69 +31,75 @@ public class BERPlatformSimple implements IBERRenderSubtype lastTrainOrder = new ArrayList<>(); + private final BERLabel label = new BERLabel() + .setPos(3, 5.5f) + .setYScale(0.75f) + .setScale(0.75f, 0.75f) + .setCentered(true) + .setScrollingSpeed(2) + ; + private List texts; + boolean updateLabel = false; + + @Override - public boolean isSingleLined() { - return true; + public void renderTick(float deltaTime) { + label.renderTick(); } @Override - public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent) { - + public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { + List textContent = new ArrayList<>(texts); + textContent.add(0, ELanguage.translate(keyTime, TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get()))); + MutableComponent txt = TextUtils.concat(textContent); + label + .setText(txt) + ; } @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - + public void render(BERGraphics graphics, float pPartialTicks, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + label.render(graphics, light); } @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - Collection preds = blockEntity.getPredictions().stream().filter(x -> x.departureTicks() < ModClientConfig.DISPLAY_LEAD_TIME.get()).toList(); - Collection uuidOrder = preds.stream().map(x -> x.trainId()).toList(); - - if (reason == EUpdateReason.DATA_CHANGED && lastTrainOrder.equals(uuidOrder)) { - return; - } + List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getScheduledArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); + + label + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) + ; - lastTrainOrder = uuidOrder; - parent.labels.clear(); + texts = new ArrayList<>(); + texts.addAll(preds.stream().map(x -> { + String timeString; + switch (blockEntity.getTimeDisplay()) { + case ETA: + timeString = ModUtils.timeRemainingString(x.getStationData().getScheduledDepartureTime()); + break; + default: + timeString = ModUtils.formatTime(x.getStationData().getScheduledDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA); + break; + } - int displayWidth = blockEntity.getXSizeScaled(); - float maxWidth = displayWidth * 16 - 6; - parent.labels.add(new BERText(parent.getFontUtils(), () -> { - List texts = new ArrayList<>(); - texts.add(ELanguage.translate(keyTime, TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get()))); - texts.addAll(preds.stream().map(x -> { - String timeString; - switch (blockEntity.getTimeDisplay()) { - case ETA: - timeString = ModUtils.timeRemainingString(x.departureTicks()); - break; - default: - timeString = TimeUtils.parseTime((int)(blockEntity.getLastRefreshedTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT + x.departureTicks()), ModClientConfig.TIME_FORMAT.get()); - break; - } + MutableComponent text = TextUtils.empty(); + if (x.getStationData().getStationInfo().platform() == null || x.getStationData().getStationInfo().platform().isBlank()) { + text.append(ELanguage.translate(keyTrainDeparture, x.getTrainData().getName(), x.getStationData().getDestination(), timeString)); + } else { + text.append(ELanguage.translate(keyTrainDepartureWithPlatform, x.getTrainData().getName(), x.getStationData().getDestination(), timeString, x.getStationData().getStationInfo().platform())); + } - if (x.stationInfo().platform() == null || x.stationInfo().platform().isBlank()) { - return ELanguage.translate(keyTrainDeparture, x.trainName(), x.scheduleTitle(), timeString); + if (x.getTrainData().isCancelled()) { + text.append(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled2").getString()); + } else if (x.getStationData().isDepartureDelayed()) { + text.append(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed2", TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation())).getString()); + if (x.getTrainData().hasStatusInfo()) { + text.append(" ").append(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.reason").getString()).append(x.getTrainData().getStatus().get(0).text()); } - return ELanguage.translate(keyTrainDepartureWithPlatform, x.trainName(), x.scheduleTitle(), timeString, x.stationInfo().platform()); - }).toList()); - - return List.of(TextUtils.concatWithStarChars(texts.toArray(Component[]::new))); - }, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.75f, 0.75f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withTicksPerPage(100) - .withRefreshRate(16) - .withPredefinedTextTransformation(new TextTransformation(3, 5.5f, 0.0f, 1, 0.75f)) - .build() - ); + } + return text; + }).toList()); } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java index 00f01449..7505ad10 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java @@ -1,96 +1,107 @@ package de.mrjulsen.crn.client.ber.variants; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; -import de.mrjulsen.mcdragonlib.util.ColorUtils; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; import de.mrjulsen.mcdragonlib.util.TextUtils; import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Component; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; public class BERTrainDestinationDetailed implements IBERRenderSubtype { + private final BERLabel outOfServiceLabel = new BERLabel(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service")) + .setPos(3, 6) + .setScale(0.5f, 0.25f) + .setYScale(0.5f) + .setCentered(true) + .setScrollingSpeed(2) + ; + + private final BERLabel trainLineLabel = new BERLabel() + .setScale(0.5f, 0.3f) + .setYScale(0.5f) + .setMaxWidth(12, BoundsHitReaction.IGNORE) + ; + private final BERLabel destinationLabel = new BERLabel() + .setScale(0.5f, 0.25f) + .setYScale(0.5f) + .setCentered(true) + .setScrollingSpeed(2) + ; + private final BERLabel viaLabel = new BERLabel(ELanguage.translate("gui.createrailwaysnavigator.via").withStyle(ChatFormatting.ITALIC)) + .setScale(0.35f, 0.35f) + .setYScale(0.35f) + ; + private final BERLabel stopoversLabel = new BERLabel() + .setScale(0.35f, 0.35f) + .setYScale(0.35f) + .setScrollingSpeed(2) + ; + + @Override - public boolean isSingleLined() { - return true; + public void renderTick(float deltaTime) { + outOfServiceLabel.renderTick(); + trainLineLabel.renderTick(); + destinationLabel.renderTick(); + viaLabel.renderTick(); + stopoversLabel.renderTick(); } @Override - public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - if (blockEntity.getTrainData() == null) { + public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { + outOfServiceLabel.render(graphics, light); return; } - parent.labels.clear(); - - int displayWidth = blockEntity.getXSizeScaled(); - boolean isSingleBlock = blockEntity.getXSizeScaled() <= 1; - - // TRAIN NAME - float maxWidth = isSingleBlock ? 11.0f : 12.0f; - MutableComponent line = TextUtils.text(blockEntity.getTrainData().trainName()).withStyle(ChatFormatting.BOLD); - BERText lastLabel = new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(isSingleBlock) - .withMaxWidth(maxWidth, isSingleBlock) - .withStretchScale(0.3f, 0.5f) - .withStencil(0, displayWidth * 16 - 5) - .withCanScroll(isSingleBlock, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) - .withPredefinedTextTransformation(new TextTransformation(isSingleBlock ? 2.5f : 3.0f, 4, 0.0f, 1, 0.5f)) - .build(); - parent.labels.add(lastLabel); + trainLineLabel.render(graphics, light); + destinationLabel.render(graphics, light); + viaLabel.render(graphics, light); + stopoversLabel.render(graphics, light); + } - // DESTINATION - float startX = lastLabel.getScaledTextWidth(); - if (blockEntity.getTrainData().getNextStop().isPresent()) { - line = TextUtils.text(blockEntity.getTrainData().getNextStop().get().scheduleTitle()); - maxWidth = displayWidth * 16 - 7 - startX; - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.25f, 0.5f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(2 + startX + 2, 4, 0.0f, 1, 0.5f)) - .build() - ); + @Override + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { + if (blockEntity.getTrainData() == null || blockEntity.getTrainData().isEmpty()) { + outOfServiceLabel + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + return; } + updateContent(blockEntity); + } - - maxWidth = isSingleBlock ? 11.0f : 12.0f; - line = TextUtils.translate("gui.createrailwaysnavigator.via").withStyle(ChatFormatting.ITALIC); - maxWidth = displayWidth * 16 - 8; - maxWidth /= 0.75f; - lastLabel = new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.35f, 0.35f) - .withColor(ColorUtils.darkenColor(blockEntity.getColor(), -0.75f)) - .withPredefinedTextTransformation(new TextTransformation(isSingleBlock ? 2.5f : 3.0f, 10, 0.0f, 0.75f, 0.3f)) - .build(); - - parent.labels.add(lastLabel); - - startX = lastLabel.getScaledTextWidth(); - startX *= 0.75f; - line = parent.getStopoversString(blockEntity); - maxWidth = displayWidth * 16 - 7 - startX; - maxWidth /= 0.75f; - - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.35f, 0.35f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor(ColorUtils.darkenColor(blockEntity.getColor(), -0.75f)) - .withPredefinedTextTransformation(new TextTransformation(2 + startX + 2, 10, 0.0f, 0.75f, 0.3f)) - .build() - ); - } + private void updateContent(AdvancedDisplayBlockEntity blockEntity) { + trainLineLabel + .setPos(3, 4) + .setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName()).withStyle(ChatFormatting.BOLD)) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + destinationLabel + .setPos(trainLineLabel.getTextWidth() + 5, 4) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - destinationLabel.getX() - 3, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.text(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().getDestination() : "")) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + viaLabel + .setPos(3, 10) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + stopoversLabel + .setPos(Math.max(trainLineLabel.getTextWidth(), viaLabel.getTextWidth()) + 5, 10) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - stopoversLabel.getX() - 3, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.concat(TextUtils.text(" \u25CF "), blockEntity.getTrainData().getStopovers().stream().map(x -> (Component)TextUtils.text(x.getName())).toList())) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java index 443e5d73..dd67781d 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java @@ -1,18 +1,18 @@ package de.mrjulsen.crn.client.ber.variants; -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.client.util.BERUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import net.minecraft.ChatFormatting; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.HorizontalDirectionalBlock; @@ -20,119 +20,134 @@ public class BERTrainDestinationInformative implements IBERRenderSubtype { + private static final ResourceLocation CARRIAGE_ICON = new ResourceLocation("create:textures/gui/assemble.png"); + private static final ResourceLocation ICONS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/icons.png"); + + + private final BERLabel carriageIndexLabel = new BERLabel() + .setScale(0.25f, 0.25f) + .setYScale(0.25f) + ; + private final BERLabel trainLineLabel = new BERLabel() + .setScale(0.25f, 0.15f) + .setYScale(0.25f) + ; + private final BERLabel fromLabel = new BERLabel() + .setScale(0.25f, 0.15f) + .setYScale(0.25f) + .setScrollingSpeed(2) + ; + private final BERLabel stopoversLabel = new BERLabel() + .setScale(0.2f, 0.15f) + .setYScale(0.2f) + .setScrollingSpeed(2) + ; + private final BERLabel destinationLabel = new BERLabel() + .setScale(0.25f, 0.15f) + .setYScale(0.25f) + .setScrollingSpeed(2) + ; + + @Override - public boolean isSingleLined() { - return true; + public void renderTick(float deltaTime) { + carriageIndexLabel.renderTick(); + trainLineLabel.renderTick(); + fromLabel.renderTick(); + stopoversLabel.renderTick(); + destinationLabel.renderTick(); } @Override - public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - if (blockEntity.getTrainData() == null) { + public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + float uv = 1.0f / 256.0f; + BERUtils.fillColor(graphics, 2.5f, 5.0f, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); + BERUtils.renderTexture(CARRIAGE_ICON, graphics, false, graphics.blockEntity().getXSizeScaled() * 16 - 7 - carriageIndexLabel.getTextWidth(), 2.5f, 0, 3, 2, uv * 22, uv * 231, uv * 22 + uv * 13, uv * 231 + uv * 5, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING).getOpposite(), (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), light); + carriageIndexLabel.render(graphics, light); + + if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { return; } - parent.labels.clear(); - - int displayWidth = blockEntity.getXSizeScaled(); - boolean isSingleBlock = blockEntity.getXSizeScaled() <= 1; - - float maxWidth = displayWidth * 16 - 6; - maxWidth /= 0.5f; - // TRAIN NAME - MutableComponent line = TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1)).withStyle(ChatFormatting.BOLD); - float textWidth = parent.getFontUtils().font.width(line) * 0.25f; - BERText lastLabel = parent.carriageIndexLabel = new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, false) - .withStretchScale(0.5f, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) - .withPredefinedTextTransformation(new TextTransformation(displayWidth * 16 - 2.5f - textWidth, 2.5f, 0.0f, 0.5f, 0.3f)) - .build(); - parent.labels.add(lastLabel); + trainLineLabel.render(graphics, light); + fromLabel.render(graphics, light); + stopoversLabel.render(graphics, light); + destinationLabel.render(graphics, light); - line = TextUtils.text(blockEntity.getTrainData().trainName()).withStyle(ChatFormatting.BOLD); - lastLabel = new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(isSingleBlock) - .withMaxWidth(maxWidth - 10f - textWidth, true) - .withStretchScale(0.3f, 0.6f) - .withStencil(0, maxWidth - 10f - textWidth) - .withCanScroll(true, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) - .withPredefinedTextTransformation(new TextTransformation(3.0f, 2.5f, 0.0f, 0.5f, 0.3f)) - .build(); - parent.labels.add(lastLabel); - - // DESTINATION - if (blockEntity.getTrainData().getNextStop().isPresent()) { - line = TextUtils.text(blockEntity.getTrainData().getNextStop().get().stationTagName()).withStyle(ChatFormatting.BOLD); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.25f, 0.5f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, 6.0f, 0.0f, 0.5f, 0.25f)) - .build() - ); - } - - // STOPOVERS - line = parent.getStopoversString(blockEntity); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.25f, 0.4f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, 8.75f, 0.0f, 0.5f, 0.2f)) - .build() + BERUtils.renderTexture( + ICONS, + graphics, + false, + 3, + 6, + 0.0f, + 2, + 2, + uv * 195, + uv * 19, + uv * (195 + 10), + uv * (19 + 10), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light ); - - // DESTINATION - if (blockEntity.getTrainData().getNextStop().isPresent()) { - line = TextUtils.text(blockEntity.getTrainData().getNextStop().get().scheduleTitle()).withStyle(ChatFormatting.BOLD); - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(false) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.25f, 0.5f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(3.0f, 11.0f, 0.0f, 0.5f, 0.25f)) - .build() - ); - } - } - - @Override - public void renderAdditional(BlockEntityRendererContext context, AdvancedDisplayBlockEntity pBlockEntity, AdvancedDisplayRenderInstance parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, Boolean backSide) { - context.renderUtils().initRenderEngine(); - float uv = 1.0f / 256.0f; - context.renderUtils().renderTexture( - new ResourceLocation("create:textures/gui/assemble.png"), - pBufferSource, - pBlockEntity, - pPoseStack, + + BERUtils.renderTexture( + ICONS, + graphics, false, - pBlockEntity.getXSizeScaled() * 16 - 6f - (parent.carriageIndexLabel == null ? 0 : parent.carriageIndexLabel.getScaledTextWidth() * 0.5f), - 2.5f, + 3, + 11, 0.0f, - 3.0f, - 2.0f, - uv * 22, - uv * 231, - uv * 22 + uv * 13, - uv * 231 + uv * 5, - pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (pBlockEntity.getColor() & 0x00FFFFFF), - pPackedLight + 2, + 2, + uv * 211, + uv * 19, + uv * (211 + 10), + uv * (19 + 10), + graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), + (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + light ); + } - - context.renderUtils().fillColor(pBufferSource, pBlockEntity, (0xFF << 24) | (pBlockEntity.getColor() & 0x00FFFFFF), pPoseStack, 2.5f, 5.0f, 0.0f, pBlockEntity.getXSizeScaled() * 16 - 5, 0.25f, pBlockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING), pPackedLight); + @Override + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { + if (blockEntity.getTrainData() == null || blockEntity.getTrainData().isEmpty()) { + return; + } + updateContent(blockEntity); } - + private void updateContent(AdvancedDisplayBlockEntity blockEntity) { + carriageIndexLabel + .setText(TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1)).withStyle(ChatFormatting.BOLD)) + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - carriageIndexLabel.getTextWidth(), 2.5f) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + trainLineLabel + .setPos(3, 2.5f) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6 - carriageIndexLabel.getTextWidth() - 5, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName()).withStyle(ChatFormatting.BOLD)) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + fromLabel + .setPos(6, 6) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 9, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.text(!blockEntity.getTrainData().getAllStops().isEmpty() ? blockEntity.getTrainData().getAllStops().get(0).getName() : "")) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + stopoversLabel + .setPos(6, 8.75f) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 9, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.concat(TextUtils.text(" \u25CF "), blockEntity.getTrainData().getStopovers().stream().map(x -> (Component)TextUtils.text(x.getName())).toList())) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + destinationLabel + .setPos(6, 11) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 9, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.text(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().getDestination() : "").withStyle(ChatFormatting.BOLD)) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java index 0eaa0d57..23881277 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java @@ -1,64 +1,86 @@ package de.mrjulsen.crn.client.ber.variants; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.base.BERText; -import de.mrjulsen.crn.client.ber.base.BERText.TextTransformation; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; public class BERTrainDestinationSimple implements IBERRenderSubtype { + + private final BERLabel outOfServiceLabel = new BERLabel(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service")) + .setPos(3, 6) + .setScale(0.5f, 0.25f) + .setYScale(0.5f) + .setCentered(true) + .setScrollingSpeed(2) + ; + + private final BERLabel trainLineLabel = new BERLabel() + .setScale(0.6f, 0.3f) + .setYScale(0.8f) + .setMaxWidth(12, BoundsHitReaction.IGNORE) + ; + private final BERLabel destinationLabel = new BERLabel() + .setScale(0.5f, 0.25f) + .setYScale(0.5f) + .setCentered(true) + .setScrollingSpeed(2) + ; + + @Override - public boolean isSingleLined() { - return true; + public void renderTick(float deltaTime) { + trainLineLabel.renderTick(); + destinationLabel.renderTick(); + outOfServiceLabel.renderTick(); + } + + @Override + public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { + outOfServiceLabel.render(graphics, light); + return; + } + DLUtils.doIfNotNull(trainLineLabel, x -> x.render(graphics, light)); + DLUtils.doIfNotNull(destinationLabel, x -> x.render(graphics, light)); } @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - if (blockEntity.getTrainData() == null) { + if (blockEntity.getTrainData() == null || blockEntity.getTrainData().isEmpty()) { + outOfServiceLabel + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; return; } + updateContent(blockEntity); + } - parent.labels.clear(); + private void updateContent(AdvancedDisplayBlockEntity blockEntity) { + trainLineLabel + .setPos(3, 5) + .setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName()).withStyle(ChatFormatting.BOLD)) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; + destinationLabel + .setPos(trainLineLabel.getTextWidth() + 5, 6) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - destinationLabel.getX() - 3, BoundsHitReaction.SCALE_SCROLL) + .setText(TextUtils.text(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().getDestination() : "")) + .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + ; - int displayWidth = blockEntity.getXSizeScaled(); - boolean isSingleBlock = blockEntity.getXSizeScaled() <= 1; - - // TRAIN NAME - float maxWidth = isSingleBlock ? 11.0f : 12.0f; - MutableComponent line = TextUtils.text(blockEntity.getTrainData().trainName()).withStyle(ChatFormatting.BOLD); - BERText lastLabel = new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(isSingleBlock) - .withMaxWidth(maxWidth, isSingleBlock) - .withStretchScale(0.3f, 0.6f) - .withStencil(0, displayWidth * 16 - 5) - .withCanScroll(isSingleBlock, 0.5f) - .withColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) - .withPredefinedTextTransformation(new TextTransformation(isSingleBlock ? 2.5f : 3.0f, 5f, 0.0f, 1, 0.8f)) - .build(); - parent.labels.add(lastLabel); - - if (!isSingleBlock && blockEntity.getTrainData().getNextStop().isPresent()) { - // DESTINATION - float startX = lastLabel.getScaledTextWidth(); - line = TextUtils.text(blockEntity.getTrainData().getNextStop().get().scheduleTitle()); - maxWidth = displayWidth * 16 - 7 - startX; - parent.labels.add(new BERText(parent.getFontUtils(), line, 0) - .withIsCentered(true) - .withMaxWidth(maxWidth, true) - .withStretchScale(0.25f, 0.5f) - .withStencil(0, maxWidth) - .withCanScroll(true, 1) - .withColor((0xFF << 24) | (blockEntity.getColor())) - .withPredefinedTextTransformation(new TextTransformation(2 + startX + 2, 6, 0.0f, 1, 0.5f)) - .build() - ); - } - } + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/IBERRenderSubtype.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/IBERRenderSubtype.java deleted file mode 100644 index a7a27ffd..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/IBERRenderSubtype.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.mrjulsen.crn.client.ber.variants; - -import com.mojang.blaze3d.vertex.PoseStack; - -import de.mrjulsen.mcdragonlib.client.ber.AbstractBlockEntityRenderInstance; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.BlockEntityRendererContext; -import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance.EUpdateReason; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; - -public interface IBERRenderSubtype, U> { - void update(Level level, BlockPos pos, BlockState state, T blockEntity, S parent, EUpdateReason reason); - boolean isSingleLined(); - default void renderAdditional(BlockEntityRendererContext context, T pBlockEntity, S parent, float pPartialTicks, PoseStack pPoseStack, MultiBufferSource pBufferSource, int pPackedLight, int pOverlay, U renderData) {} - default void tick(Level level, BlockPos pos, BlockState state, T pBlockEntity, S parent) {} -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/Animator.java b/common/src/main/java/de/mrjulsen/crn/client/gui/Animator.java new file mode 100644 index 00000000..b1205e65 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/Animator.java @@ -0,0 +1,93 @@ +package de.mrjulsen.crn.client.gui; + +import com.mojang.blaze3d.vertex.PoseStack; + +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import net.minecraft.client.Minecraft; + +public class Animator extends DLRenderable { + + private int maxTicks; + private int currentTicks; + private float currentTicksSmooth; + private boolean running; + + private IAnimatorRenderCallback onAnimateRender; + private IAnimatorTickCallback onAnimateTick; + private Runnable onCompleted; + + public Animator() { + super(0, 0, 0, 0); + } + + public boolean isRunning() { + return running; + } + + public int getTotalTicks() { + return maxTicks; + } + + public int getCurrentTicks() { + return currentTicks; + } + + public float getCurrentTicksSmooth() { + return currentTicksSmooth; + } + + public float getPercentage() { + return 1F / (float)maxTicks * currentTicksSmooth; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + partialTicks = Minecraft.getInstance().getFrameTime(); + currentTicksSmooth += partialTicks; + if (running) { + DLUtils.doIfNotNull(onAnimateRender, x -> x.execute(graphics.poseStack(), getCurrentTicks(), getTotalTicks(), getPercentage())); + } + } + + @Override + public void tick() { + if (running) { + DLUtils.doIfNotNull(onAnimateTick, x -> x.execute(getCurrentTicks(), getTotalTicks(), getPercentage())); + currentTicks++; + currentTicksSmooth = currentTicks; + if (currentTicks >= maxTicks) { + stop(); + DLUtils.doIfNotNull(onCompleted, x -> x.run()); + } + } + } + + public void start(int ticks, IAnimatorRenderCallback renderCallback, IAnimatorTickCallback tickCallback, Runnable onCompleted) { + this.currentTicks = 0; + this.currentTicksSmooth = 0; + this.maxTicks = ticks; + this.onAnimateRender = renderCallback; + this.onAnimateTick = tickCallback; + this.onCompleted = onCompleted; + this.running = true; + } + + public void stop() { + this.running = false; + this.currentTicks = 1; + this.currentTicksSmooth = 1; + this.maxTicks = 1; + } + + @FunctionalInterface + public static interface IAnimatorRenderCallback { + void execute(PoseStack poseStack, int currentTicks, int totalTicks, double percentage); + } + + @FunctionalInterface + public static interface IAnimatorTickCallback { + void execute(int currentTicks, int totalTicks, double percentage); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/CreateDynamicWidgets.java b/common/src/main/java/de/mrjulsen/crn/client/gui/CreateDynamicWidgets.java index 13410e83..d2ef0910 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/CreateDynamicWidgets.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/CreateDynamicWidgets.java @@ -1,10 +1,25 @@ package de.mrjulsen.crn.client.gui; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat.Mode; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.CRNGui; +import de.mrjulsen.crn.client.ModGuiUtils; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; public class CreateDynamicWidgets { - private static final int BORDER_WEIGHT = 2; + + private static final int BORDER_HEIGHT = 2; private static final int TEXTBOX_HEIGHT = 18; private static final int COLOR_BORDER = 0xFF393939; private static final int COLOR_3D_SHADOW = 0xFF373737; @@ -30,19 +45,21 @@ public static void renderWidgetInner(Graphics graphics, int x, int y, int w, int } public static void renderWidgetTopBorder(Graphics graphics, int x, int y, int w) { - GuiUtils.fill(graphics, x, y, w, BORDER_WEIGHT, ColorShade.LIGHT.getColor()); // left border - GuiUtils.fill(graphics, x + 1, y + 1, w - 2, BORDER_WEIGHT - 1, COLOR_BORDER); // left border + GuiUtils.fill(graphics, x + 1, y, w - 2, 1, ColorShade.LIGHT.getColor()); // left border + GuiUtils.fill(graphics, x, y + 1, w, 1, ColorShade.LIGHT.getColor()); // left border + GuiUtils.fill(graphics, x + 1, y + 1, w - 2, BORDER_HEIGHT - 1, COLOR_BORDER); // left border } public static void renderWidgetBottomBorder(Graphics graphics, int x, int y, int w) { - GuiUtils.fill(graphics, x, y, w, BORDER_WEIGHT, ColorShade.LIGHT.getColor()); // left border + GuiUtils.fill(graphics, x, y, w, 1, ColorShade.LIGHT.getColor()); // left border + GuiUtils.fill(graphics, x + 1, y + 1, w - 2, 1, ColorShade.LIGHT.getColor()); // left border GuiUtils.fill(graphics, x + 1, y, w - 2, 1, COLOR_BORDER); // left border } public static void renderSingleShadeWidget(Graphics graphics, int x, int y, int w, int h, int color) { - renderWidgetInner(graphics, x, y, w, h, color); + renderWidgetInner(graphics, x, y + 2, w, h - 4, color); renderWidgetTopBorder(graphics, x, y, w); - renderWidgetBottomBorder(graphics, x, y + h - BORDER_WEIGHT, w); + renderWidgetBottomBorder(graphics, x, y + h - BORDER_HEIGHT, w); } public static void renderSingleShadeWidget(Graphics graphics, int x, int y, int w, int h, ColorShade color) { @@ -50,10 +67,10 @@ public static void renderSingleShadeWidget(Graphics graphics, int x, int y, int } public static void renderDuoShadeWidget(Graphics graphics, int x, int y, int w, int h1, int color1, int h2, int color2) { - renderWidgetInner(graphics, x, y, w, h1, color1); - renderWidgetInner(graphics, x, y + h1, w, h2, color2); + renderWidgetInner(graphics, x, y + 2, w, h1 - 2, color1); + renderWidgetInner(graphics, x, y + h1, w, h2 - 2, color2); renderWidgetTopBorder(graphics, x, y, w); - renderWidgetBottomBorder(graphics, x, y + h1 + h2 - BORDER_WEIGHT, w); + renderWidgetBottomBorder(graphics, x, y + h1 + h2 - BORDER_HEIGHT, w); } public static void renderDuoShadeWidget(Graphics graphics, int x, int y, int w, int h1, ColorShade color1, int h2, ColorShade color2) { @@ -90,11 +107,74 @@ public static void renderHorizontalSeparator(Graphics graphics, int x, int y, in GuiUtils.fill(graphics, x, y + 1, w - 1, 1, COLOR_BORDER); } + public static void renderContainerBackground(Graphics graphics, int x, int y, int w, int h, ContainerColor color) { + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferbuilder = tesselator.getBuilder(); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + RenderSystem.setShaderTexture(0, color.res); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + float f = 2f; + bufferbuilder.begin(Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + bufferbuilder.vertex(x, y + (double)h, 0.0).uv(0.0F, (float)h / f).color(1f, 1f, 1f, 1f).endVertex(); + bufferbuilder.vertex(x + (double)w, y + (double)h, 0.0).uv((float)w / f, (float)h / f).color(1f, 1f, 1f, 1f).endVertex(); + bufferbuilder.vertex(x + (double)w, y, 0.0).uv((float)w / f, 0).color(1f, 1f, 1f, 1f).endVertex(); + bufferbuilder.vertex(x, y, 0.0).uv(0.0F, 0).color(1f, 1f, 1f, 1f).endVertex(); + tesselator.end(); + } + + protected static void renderNineSliced(Graphics graphics, int x, int y, int w, int h, int u, int v, int textureWidth, int textureHeight, int cornerSliceSize, ResourceLocation location, boolean renderCenter) { + GuiUtils.drawTexture(location, graphics, x, y, cornerSliceSize, cornerSliceSize, u, v, cornerSliceSize, cornerSliceSize, textureWidth, textureHeight); // Top left + GuiUtils.drawTexture(location, graphics, x + w - cornerSliceSize, y, cornerSliceSize, cornerSliceSize, u + 1 + cornerSliceSize, v, cornerSliceSize, cornerSliceSize, textureWidth, textureHeight); // Top right + GuiUtils.drawTexture(location, graphics, x, y + h - cornerSliceSize, cornerSliceSize, cornerSliceSize, u, v + 1 + cornerSliceSize, cornerSliceSize, cornerSliceSize, textureWidth, textureHeight); // bottom left + GuiUtils.drawTexture(location, graphics, x + w - cornerSliceSize, y + h - cornerSliceSize, cornerSliceSize, cornerSliceSize, u + 1 + cornerSliceSize, v + 1 + cornerSliceSize, cornerSliceSize, cornerSliceSize, textureWidth, textureHeight); // bottom right + + GuiUtils.drawTexture(location, graphics, x + cornerSliceSize, y, w - cornerSliceSize * 2, cornerSliceSize, u + cornerSliceSize, v, 1, cornerSliceSize, textureWidth, textureHeight); // top + GuiUtils.drawTexture(location, graphics, x + cornerSliceSize, y + h - cornerSliceSize, w - cornerSliceSize * 2, cornerSliceSize, u + cornerSliceSize, v + 1 + cornerSliceSize, 1, cornerSliceSize, textureWidth, textureHeight); // bottom + GuiUtils.drawTexture(location, graphics, x, y + cornerSliceSize, cornerSliceSize, h - cornerSliceSize * 2, u, v + cornerSliceSize, cornerSliceSize, 1, textureWidth, textureHeight); // left + GuiUtils.drawTexture(location, graphics, x + w - cornerSliceSize, y + cornerSliceSize, cornerSliceSize, h - cornerSliceSize * 2, u + 1 + cornerSliceSize, v + cornerSliceSize, cornerSliceSize, 1, textureWidth, textureHeight); // right + + if (renderCenter) { + GuiUtils.drawTexture(location, graphics, x + cornerSliceSize, y + cornerSliceSize, w - cornerSliceSize * 2, h - cornerSliceSize * 2, u + cornerSliceSize, v + cornerSliceSize, 1, 1, textureWidth, textureHeight); + } + } + + public static void renderContainer(Graphics graphics, int x, int y, int w, int h, ContainerColor color) { + renderContainerBackground(graphics, x + 2, y + 2, w - 4, h - 4, color); + renderNineSliced(graphics, x, y, w, h, 0, 7, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT, 2, CRNGui.GUI, false); + } + + public static void renderTitleBar(Graphics graphics, int x, int y, int w, int h, BarColor color) { + renderNineSliced(graphics, x, y, w, h, color.u, color.v, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT, 3, CRNGui.GUI, true); + } + + + public static void renderWindow(Graphics graphics, int x, int y, int w, int h, ContainerColor color, BarColor bar, int headerSize, int footerSize, boolean renderContent) { + renderTitleBar(graphics, x, y, w, headerSize, bar); + renderTitleBar(graphics, x, y + h - footerSize, w, footerSize, bar); + + if (renderContent) { + renderContainer(graphics, x + 1, y + headerSize - 1, w - 2, h - headerSize - footerSize + 2, color); + } + } + + public static void renderShadow(Graphics graphics, int x, int y, int w, int h) { + renderNineSliced(graphics, x - 5, y - 5, w + 10, h + 10, 0, 0, 11, 11, 5, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/shadow.png"), true); + } + + public static void renderTextHighlighted(Graphics graphics, int x, int y, Font font, Component text, int color) { + int width = font.width(text) + 6; + int height = font.lineHeight + 6; + GuiUtils.fill(graphics, x, y + 1, width, height - 3, color); + GuiUtils.fill(graphics, x + 1, y, width - 2, 1, color); + GuiUtils.fill(graphics, x + 1, y + height - 2, width - 2, 1, color); + GuiUtils.drawString(graphics, font, x + 3, y + 3, text, ModGuiUtils.useWhiteOrBlackForeColor(color) ? 0xFFFFFFFF : 0xFF000000, EAlignment.LEFT, false); + } + public static enum ColorShade { LIGHT(0xFF6f6f6f), DARK(0xFF575757); - private int color; + private final int color; ColorShade(int color) { this.color = color; @@ -105,4 +185,50 @@ public int getColor() { } } + + public static enum ContainerColor { + GRAY(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/container_gray.png")), + PURPLE(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/container_purple.png")), + BLUE(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/container_blue.png")), + GOLD(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/container_gold.png")); + + private ResourceLocation res; + + ContainerColor(ResourceLocation res) { + this.res = res; + } + } + + public static enum BarColor { + GRAY(7, 0), + PURPLE(14, 0), + GOLD(0, 0); + + private final int u; + private final int v; + + BarColor(int u, int v) { + this.u = u; + this.v = v; + } + } + + + + public static enum FooterSize { + DEFAULT(15), + SMALL(30), + EXTENDED(36); + + private final int size; + + FooterSize(int size) { + this.size = size; + } + + public int size() { + return size; + } + } + } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/CustomIconScreenElement.java b/common/src/main/java/de/mrjulsen/crn/client/gui/CustomIconScreenElement.java similarity index 95% rename from common/src/main/java/de/mrjulsen/crn/client/gui/screen/CustomIconScreenElement.java rename to common/src/main/java/de/mrjulsen/crn/client/gui/CustomIconScreenElement.java index 672a5930..85e111e8 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/CustomIconScreenElement.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/CustomIconScreenElement.java @@ -1,4 +1,4 @@ -package de.mrjulsen.crn.client.gui.screen; +package de.mrjulsen.crn.client.gui; import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.foundation.gui.element.ScreenElement; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/ModGuiIcons.java b/common/src/main/java/de/mrjulsen/crn/client/gui/ModGuiIcons.java index 002b24c0..bef0e660 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/ModGuiIcons.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/ModGuiIcons.java @@ -6,6 +6,7 @@ import com.simibubi.create.foundation.gui.AllIcons; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.mcdragonlib.client.render.Sprite; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import net.minecraft.resources.ResourceLocation; @@ -15,6 +16,7 @@ public enum ModGuiIcons { CHECK("check", 1, 0), CROSS("cross", 2, 0), WARN("warn", 3, 0), + IMPORTANT("important", 4, 0), SETTINGS("settings", 0, 1), FILTER("filter", 1, 1), @@ -46,14 +48,29 @@ public enum ModGuiIcons { DETAILED("detailed", 10, 2), VERY_DETAILED("very_detailed", 11, 2), ARROW_RIGHT("arrow_right", 12, 2), - ARROW_LEFT("arrow_left", 13, 2); + ARROW_LEFT("arrow_left", 13, 2), + REFRESH("refresh", 14, 2), + POPUP("refresh", 15, 2), + + USER("user", 0, 3), + MAP_PATH("map_path", 1, 3), + BOOKMARK_FILLED("bookmark_filled", 2, 3), + HELP("help", 3, 3), + ROUTE_START("route_start", 4, 3), + ROUTE_END("route_end", 5, 3), + CALENDAR("calendar", 6, 3), + BELL("bell", 7, 3), + COLOR_PALETTE("color_palette", 8, 3), + TRAIN("train", 9, 3), + X("x", 10, 3), + CHECKMARK("checkmark", 11, 3); private String id; private int u; private int v; public static final int ICON_SIZE = 16; - public static final ResourceLocation ICON_LOCATION = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/icons.png");; + public static final ResourceLocation ICON_LOCATION = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/icons.png"); ModGuiIcons(String id, int u, int v) { this.id = id; @@ -92,6 +109,10 @@ public AllIcons getAsCreateIcon() { public void render(Graphics graphics, int x, int y) { GuiUtils.drawTexture(ModGuiIcons.ICON_LOCATION, graphics, x, y, getU(), getV(), ICON_SIZE, ICON_SIZE); } + + public Sprite getAsSprite(int renderWidth, int renderHeight) { + return new Sprite(ICON_LOCATION, 256, 256, getU(), getV(), ICON_SIZE, ICON_SIZE, renderWidth, renderHeight); + } public static class ModAllIcons extends AllIcons { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlay.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlay.java new file mode 100644 index 00000000..c209148c --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlay.java @@ -0,0 +1,296 @@ +package de.mrjulsen.crn.client.gui.overlay; + +import java.util.Set; +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import com.simibubi.create.foundation.gui.UIRenderHelper; +import com.simibubi.create.foundation.utility.animation.LerpedFloat; +import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ModGuiUtils; +import de.mrjulsen.crn.client.gui.overlay.pages.AbstractRouteDetailsPage; +import de.mrjulsen.crn.client.gui.overlay.pages.ConnectionMissedPage; +import de.mrjulsen.crn.client.gui.overlay.pages.JourneyCompletedPage; +import de.mrjulsen.crn.client.gui.overlay.pages.NextConnectionsPage; +import de.mrjulsen.crn.client.gui.overlay.pages.RouteOverviewPage; +import de.mrjulsen.crn.client.gui.overlay.pages.TrainCancelledInfo; +import de.mrjulsen.crn.client.gui.overlay.pages.TransferPage; +import de.mrjulsen.crn.client.gui.overlay.pages.WelcomePage; +import de.mrjulsen.crn.client.gui.screen.RouteOverlaySettingsScreen; +import de.mrjulsen.crn.client.input.ModKeys; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.TransferConnection; +import de.mrjulsen.crn.registry.ModItems; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLOverlayScreen; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +public class RouteDetailsOverlay extends DLOverlayScreen { + + private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/overview.png"); + private static final Component title = TextUtils.translate("gui.createrailwaysnavigator.route_overview.title"); + private static final int GUI_WIDTH = 226; + private static final int GUI_HEIGHT = 118; + private static final int SLIDING_TEXT_AREA_WIDTH = 220; + + private final Level level; + + private Component slidingText = TextUtils.empty(); + private float slidingTextOffset = 0; + private int slidingTextWidth = 0; + + private LerpedFloat xPos; + private LerpedFloat yPos; + + private static final String keyTrainDetails = "gui.createrailwaysnavigator.route_overview.train_details"; + private static final String keyTransfer = "gui.createrailwaysnavigator.route_overview.transfer"; + private static final String keyTransferWithPlatform = "gui.createrailwaysnavigator.route_overview.transfer_with_platform"; + private static final String keyAfterJourney = "gui.createrailwaysnavigator.route_overview.after_journey"; + private static final String keyOptionsText = "gui.createrailwaysnavigator.route_overview.options"; + private static final String keyKeybindOptions = "key.createrailwaysnavigator.route_overlay_options"; + private static final String keyJourneyBegins = "gui.createrailwaysnavigator.route_overview.journey_begins"; + private static final String keyJourneyBeginsWithPlatform = "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform"; + private static final String keyNextStop = "gui.createrailwaysnavigator.route_overview.next_stop"; + private static final String keyConnectionMissedInfo = "gui.createrailwaysnavigator.route_overview.connection_missed_info"; + private static final String keyTrainCancelledInfo = "gui.createrailwaysnavigator.route_overview.train_cancelled_info"; + + + private final Font font = Minecraft.getInstance().font; + private final ClientRoute route; + private boolean journeyCompleted = false; + + private AbstractRouteDetailsPage currentPage; + + public RouteDetailsOverlay(Level level, ClientRoute route, int width, int height) { + this.level = level; + this.route = route; + route.addListener(); + + { + currentPage = new WelcomePage(this.route); + String terminus = route.getStart().getDisplayTitle(); + StationInfo info = route.getStart().getRealTimeStationTag().info(); + setSlidingText(info.platform().isEmpty() ? ELanguage.translate(keyJourneyBegins, route.getStart().getTrainDisplayName(), terminus, TimeUtils.parseTime(route.getStart().getScheduledDepartureTime(), ModClientConfig.TIME_FORMAT.get())) : ELanguage.translate(keyJourneyBeginsWithPlatform, route.getStart().getTrainDisplayName(), terminus, TimeUtils.parseTime(route.getStart().getScheduledDepartureTime(), ModClientConfig.TIME_FORMAT.get()), info.platform())); + } + + xPos = LerpedFloat.linear().startWithValue(width / 2 - (ModClientConfig.OVERLAY_SCALE.get() * (GUI_WIDTH / 2))); + yPos = LerpedFloat.linear().startWithValue(height / 2 - (ModClientConfig.OVERLAY_SCALE.get() * (GUI_HEIGHT / 2))); + + if (route.isClosed()) return; + + route.listen(ClientRoute.EVENT_DEPARTURE_FROM_ANY_STOP, this, x -> { + currentPage = new RouteOverviewPage(this.route); + String terminus = x.part().getNextStop().getTerminusText(); + setSlidingText(ELanguage.translate(keyTrainDetails, x.part().getNextStop().getTrainDisplayName(), terminus == null || terminus.isEmpty() ? x.part().getNextStop().getScheduleTitle() : terminus)); + }); + route.listen(ClientRoute.EVENT_FIRST_STOP_STATION_CHANGED, this, x -> { + setSlidingText(x.trainStop().getRealTimeStationTag().info().platform().isEmpty() ? ELanguage.translate(keyJourneyBegins) : ELanguage.translate(keyJourneyBeginsWithPlatform, x.trainStop().getRealTimeStationTag().info().platform())); + }); + route.listen(ClientRoute.EVENT_ARRIVAL_AT_ANY_STOP, this, x -> { + setSlidingText(TextUtils.text(x.trainStop().getClientTag().tagName())); + }); + route.listen(ClientRoute.EVENT_ANY_STOP_ANNOUNCED, this, x -> { + NextConnectionsPage page = new NextConnectionsPage(this.route, null); + if (page.hasConnections()) { + currentPage = page; + } + }); + route.listen(ClientRoute.EVENT_ANNOUNCE_STOPOVER, this, x -> { + setSlidingText(ELanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName())); + }); + route.listen(ClientRoute.EVENT_ANNOUNCE_LAST_STOP, this, x -> { + setSlidingText(ELanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName())); + }); + route.listen(ClientRoute.EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION, this, x -> { + if (x.connection().isConnectionMissed()) { + connectionMissed(); + return; + } + setSlidingText(ELanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName()).append(" *** ").append(getTransferSlidingText(x.connection()))); + currentPage = new TransferPage(this.route, x.connection()); + }); + route.listen(ClientRoute.EVENT_PART_CHANGED, this, x -> { + if (x.connection().isConnectionMissed()) { + connectionMissed(); + } + }); + route.listen(ClientRoute.EVENT_DEPARTURE_FROM_TRANSFER_ARRIVAL_STATION, this, x -> { + setSlidingText(TextUtils.text(x.connection().getArrivalStation().getClientTag().tagName()).append(" *** ").append(getTransferSlidingText(x.connection()))); + currentPage = new TransferPage(this.route, x.connection()); + }); + route.listen(ClientRoute.EVENT_ARRIVAL_AT_LAST_STOP, this, x -> { + setSlidingText(ELanguage.translate(keyAfterJourney, x.trainStop().getClientTag().tagName())); + currentPage = new JourneyCompletedPage(this.route, () -> currentPage = new NextConnectionsPage(route, () -> {} /*InstanceManager::removeRouteOverlay*/)); + route.close(); + }); + route.listen(ClientRoute.EVENT_DEPARTURE_FROM_LAST_STOP, this, x -> { + if (journeyCompleted) { + return; + } + setSlidingText(ELanguage.translate(keyAfterJourney, x.trainStop().getClientTag().tagName())); + currentPage = new JourneyCompletedPage(this.route, () -> currentPage = new NextConnectionsPage(route, () -> {} /*InstanceManager::removeRouteOverlay*/)); + route.close(); + }); + route.listen(ClientRoute.EVENT_ANY_TRANSFER_MISSED, this, x -> { + connectionMissed(); + }); + route.listen(ClientRoute.EVENT_ANY_TRAIN_CANCELLED, this, x -> { + trainCancelled(x.part().getLastStop().getTrainDisplayName()); + }); + } + + private Component getTransferSlidingText(TransferConnection connection) { + StationInfo info = connection.getDepartureStation().getRealTimeStationTag().info(); + String terminus = connection.getDepartureStation().getDisplayTitle(); + return (info == null || info.platform().isBlank() ? ELanguage.translate(keyTransfer, connection.getDepartureStation().getTrainDisplayName(), terminus) : ELanguage.translate(keyTransferWithPlatform, connection.getDepartureStation().getTrainDisplayName(), terminus, info.platform())); + } + + private void connectionMissed() { + setSlidingText(ELanguage.translate(keyConnectionMissedInfo)); + currentPage = new ConnectionMissedPage(this.route); + route.close(); + } + + private void trainCancelled(String trainName) { + setSlidingText(ELanguage.translate(keyTrainCancelledInfo, trainName)); + currentPage = new TrainCancelledInfo(this.route, trainName); + route.close(); + } + + private float getUIScale() { + return (float)ModClientConfig.OVERLAY_SCALE.get().doubleValue(); + } + + @Override + public void onClose() { + route.close(); + journeyCompleted = true; + } + + + @SuppressWarnings("resource") + @Override + public void tick() { + if (Screen.hasControlDown() && ModKeys.KEY_OVERLAY_SETTINGS.isDown() && Minecraft.getInstance().player.getInventory().hasAnyOf(Set.of(ModItems.NAVIGATOR.get()))) { + DLScreen.setScreen(new RouteOverlaySettingsScreen(this)); + } + + xPos.tickChaser(); + yPos.tickChaser(); + + DLUtils.doIfNotNull(currentPage, x -> x.tick()); + } + + protected void tickSlidingText(float delta) { + // Sliding text + if (slidingTextWidth > SLIDING_TEXT_AREA_WIDTH * 0.75f) { + slidingTextOffset -= delta; + if (slidingTextOffset < -(slidingTextWidth / 2)) { + slidingTextOffset = (int)((SLIDING_TEXT_AREA_WIDTH + slidingTextWidth / 2) + 20); + } + } + } + + //#region FUNCTIONS + + private void startStencil(Graphics graphics, int x, int y, int w, int h) { + UIRenderHelper.swapAndBlitColor(Minecraft.getInstance().getMainRenderTarget(), UIRenderHelper.framebuffer); + ModGuiUtils.startStencil(graphics, x, y, w, h); + } + + private void endStencil() { + ModGuiUtils.endStencil(); + UIRenderHelper.swapAndBlitColor(UIRenderHelper.framebuffer, Minecraft.getInstance().getMainRenderTarget()); + } + + private void setSlidingText(Component component) { + slidingText = component; + slidingTextWidth = font.width(component); + + if (slidingTextWidth > SLIDING_TEXT_AREA_WIDTH * 0.75f) { + slidingTextOffset = (int)((SLIDING_TEXT_AREA_WIDTH + slidingTextWidth / 2) + 20); + } else { + slidingTextOffset = (int)(SLIDING_TEXT_AREA_WIDTH * 0.75f / 2); + } + } + //#endregion + + //#region RENDERING + @Override + public void render(Graphics graphics, float partialTicks, int width, int height) { + width = Minecraft.getInstance().getWindow().getGuiScaledWidth(); + height = Minecraft.getInstance().getWindow().getGuiScaledHeight(); + partialTicks = Minecraft.getInstance().getFrameTime(); + OverlayPosition pos = ModClientConfig.ROUTE_OVERLAY_POSITION.get(); + final int x = pos == OverlayPosition.TOP_LEFT || pos == OverlayPosition.BOTTOM_LEFT ? 8 : (int)(width - GUI_WIDTH * getUIScale() - 10); + final int y = pos == OverlayPosition.TOP_LEFT || pos == OverlayPosition.TOP_RIGHT ? 8 : (int)(height - GUI_HEIGHT * getUIScale() - 10); + + xPos.chase(x, 0.2f, Chaser.EXP); + yPos.chase(y, 0.2f, Chaser.EXP); + + graphics.poseStack().pushPose(); + graphics.poseStack().translate((int)xPos.getValue(partialTicks), (int)yPos.getValue(partialTicks), 0); + renderInternal(graphics, 0, 0, width, height, partialTicks, (int)xPos.getValue(partialTicks), (int)yPos.getValue(partialTicks)); + graphics.poseStack().popPose(); + + tickSlidingText(2 * Minecraft.getInstance().getDeltaFrameTime()); + } + + public void renderSlidingText(Graphics graphics, int x, int y, int transX, int transY) { + startStencil(graphics, x + 3, y + 14, 220, 21); + graphics.poseStack().pushPose(); + graphics.poseStack().scale(1.0f / 0.75f, 1.0f / 0.75f, 1.0f / 0.75f); + GuiUtils.drawString(graphics, font, (int)((x + 3) + slidingTextOffset), y + 14, slidingText, 0xFF9900, EAlignment.CENTER, false); + graphics.poseStack().popPose(); + endStencil(); + } + + private void renderInternal(Graphics graphics, int x, int y, int width, int height, float partialTicks, int transX, int transY) { + graphics.poseStack().pushPose(); + graphics.poseStack().scale(getUIScale(), getUIScale(), getUIScale()); + RenderSystem.setShaderTexture(0, GUI); + GuiUtils.drawTexture(GUI, graphics, x, y, GUI_WIDTH, GUI_HEIGHT, 0, currentPage != null && currentPage.isImportant() ? 138 : 0, 256, 256); + + GuiUtils.drawString(graphics, font, x + 6, y + 4, title, 0x4F4F4F, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, x + 6, y + GUI_HEIGHT - 2 - font.lineHeight, TextUtils.translate(keyOptionsText, TextUtils.translate(InputConstants.getKey(Minecraft.ON_OSX ? InputConstants.KEY_LWIN : InputConstants.KEY_LCONTROL, 0).getName()).append(" + ").append(Component.keybind(keyKeybindOptions)).withStyle(ChatFormatting.BOLD)), 0x4F4F4F, EAlignment.LEFT, false); + + String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + GuiUtils.drawString(graphics, font, x + GUI_WIDTH - 4 - font.width(timeString), y + 4, timeString, 0x4F4F4F, EAlignment.LEFT, false); + + renderSlidingText(graphics, x, y + 2, transX, transY); + + startStencil(graphics, x + 3, y + 40, 220, 62); + graphics.poseStack().pushPose(); + graphics.poseStack().translate(3, 40, 0); + DLUtils.doIfNotNull(currentPage, a -> { + a.renderBackLayer(graphics, 0, 0, partialTicks); + a.renderMainLayer(graphics, 0, 0, partialTicks); + }); + graphics.poseStack().popPose(); + endStencil(); + DLUtils.doIfNotNull(currentPage, a -> a.renderFrontLayer(graphics, 0, 0, partialTicks)); + if (CreateRailwaysNavigator.isDebug()) GuiUtils.drawString(graphics, font, 5, GUI_HEIGHT + 10, "State: " + route.getState() + ", " + route.getCurrentPartIndex() + ", " + route.getCurrentPart().getNextStop().getClientTag().tagName(), 0xFFFF0000, EAlignment.LEFT, false); + graphics.poseStack().popPose(); + } + + public ClientRoute getRoute() { + return route; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlayScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlayScreen.java deleted file mode 100644 index 76371718..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/RouteDetailsOverlayScreen.java +++ /dev/null @@ -1,787 +0,0 @@ -package de.mrjulsen.crn.client.gui.overlay; - -import java.util.Collection; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.text2speech.Narrator; -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; -import com.simibubi.create.foundation.gui.UIRenderHelper; -import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.ClientWrapper; -import de.mrjulsen.crn.client.ModGuiUtils; -import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.gui.NavigatorToast; -import de.mrjulsen.crn.client.input.ModKeys; -import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.SimpleTrainConnection; -import de.mrjulsen.crn.data.SimpleRoute.StationEntry; -import de.mrjulsen.crn.data.SimpleRoute.StationTag; -import de.mrjulsen.crn.event.listeners.IJourneyListenerClient; -import de.mrjulsen.crn.event.listeners.JourneyListener; -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; -import de.mrjulsen.crn.event.listeners.JourneyListener.AnnounceNextStopData; -import de.mrjulsen.crn.event.listeners.JourneyListener.ContinueData; -import de.mrjulsen.crn.event.listeners.JourneyListener.FinishJourneyData; -import de.mrjulsen.crn.event.listeners.JourneyListener.JourneyBeginData; -import de.mrjulsen.crn.event.listeners.JourneyListener.JourneyInterruptData; -import de.mrjulsen.crn.event.listeners.JourneyListener.NotificationData; -import de.mrjulsen.crn.event.listeners.JourneyListener.ReachNextStopData; -import de.mrjulsen.crn.event.listeners.JourneyListener.State; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.NextConnectionsRequestPacket; -import de.mrjulsen.crn.network.packets.cts.TrainDataRequestPacket; -import de.mrjulsen.crn.registry.ModItems; -import de.mrjulsen.crn.util.ModUtils; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.DLOverlayScreen; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils.TimeFormat; -import net.minecraft.ChatFormatting; -import net.minecraft.Util; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; -import net.minecraft.world.level.Level; - -public class RouteDetailsOverlayScreen extends DLOverlayScreen implements IJourneyListenerClient { - - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/overview.png"); - private static final Component title = TextUtils.translate("gui.createrailwaysnavigator.route_overview.title"); - private static final int GUI_WIDTH = 226; - private static final int GUI_HEIGHT = 118; - private static final int SLIDING_TEXT_AREA_WIDTH = 220; - - private static final int ON_TIME = 0x1AEA5F; - private static final int DELAYED = 0xFF4242; - private static final int COLOR_WARN = 16755200; - - private long fadeStart = 0L; - private boolean fading = false; - private boolean fadeInvert = true; - private Runnable fadeDoneAction; - - private static final int ROUTE_LINE_HEIGHT = 14; - - private final Level level; - - private static final int MAX_STATION_PER_PAGE = 4; - - private Page currentPage = Page.NONE; - - private Collection connections; - private long connectionsRefreshTime; - private static final int CONNECTION_ENTRIES_PER_PAGE = 3; - private static final int TIME_PER_CONNECTIONS_SUBPAGE = 200; - private int connectionsSubPageTime = 0; - private int connectionsSubPageIndex = 0; - private int connectionsSubPagesCount = 0; - - private static final int TIME_PER_TRAIN_DATA_SUBPAGE = 200; - private static final int TRAIN_DATA_PAGES = 2; - private int trainDataSubPageTime = 0; - private int trainDataSubPageIndex = 0; - - private final Font shadowlessFont; - - private Component slidingText = TextUtils.empty(); - private float slidingTextOffset = 0; - private int slidingTextWidth = 0; - - private MultiLineLabel messageLabel; - private MutableComponent interruptedText; - - private LerpedFloat xPos; - private LerpedFloat yPos; - - private static final String keyTrainDetails = "gui.createrailwaysnavigator.route_overview.train_details"; - private static final String keyTransfer = "gui.createrailwaysnavigator.route_overview.transfer"; - private static final String keyTransferWithPlatform = "gui.createrailwaysnavigator.route_overview.transfer_with_platform"; - private static final String keyTransferCount = "gui.createrailwaysnavigator.navigator.route_entry.transfer"; - private static final String keyTrainCanceled = "gui.createrailwaysnavigator.route_overview.stop_canceled"; - private static final String keyAfterJourney = "gui.createrailwaysnavigator.route_overview.after_journey"; - private static final String keyJourneyCompleted = "gui.createrailwaysnavigator.route_overview.journey_completed"; - private static final String keyNextConnections = "gui.createrailwaysnavigator.route_overview.next_connections"; - private static final String keyScheduleTransfer = "gui.createrailwaysnavigator.route_overview.schedule_transfer"; - private static final String keyConnectionEndangered = "gui.createrailwaysnavigator.route_overview.connection_endangered"; - private static final String keyConnectionMissed = "gui.createrailwaysnavigator.route_overview.connection_missed"; - private static final String keyConnectionCanceled = "gui.createrailwaysnavigator.route_overview.connection_canceled"; - private static final String keyConnectionMissedPageText = "gui.createrailwaysnavigator.route_overview.journey_interrupted_info"; - private static final String keyDepartureIn = "gui.createrailwaysnavigator.route_details.departure"; - private static final String keyTimeNow = "gui.createrailwaysnavigator.time.now"; - private static final String keyOptionsText = "gui.createrailwaysnavigator.route_overview.options"; - private static final String keyKeybindOptions = "key.createrailwaysnavigator.route_overlay_options"; - - private final UUID listenerId; - private JourneyListener listener; - private final UUID clientId = UUID.randomUUID(); - - - - @SuppressWarnings("resource") - public RouteDetailsOverlayScreen(Level level, SimpleRoute route, int width, int height) { - this.level = level; - this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); - this.listenerId = route.getListenerId(); - - xPos = LerpedFloat.linear().startWithValue(width / 2 - (ModClientConfig.OVERLAY_SCALE.get() * (GUI_WIDTH / 2))); - yPos = LerpedFloat.linear().startWithValue(height / 2 - (ModClientConfig.OVERLAY_SCALE.get() * (GUI_HEIGHT / 2))); - - getListener() - .registerOnNarratorAnnounce(this, this::narratorAnnouncement) - .registerOnAnnounceNextStop(this, this::announceNextStop) - .registerOnContinueWithJourneyAfterStop(this, this::nextStop) - .registerOnFinishJourney(this, this::finishJourney) - .registerOnReachNextStop(this, this::reachNextStop) - .registerOnJourneyBegin(this, this::journeyBegin) - .registerOnNotification(this, this::notificationReceive) - .registerOnJourneyInterrupt(this, this::journeyInterrupt) - ; - - setSlidingText(getListener().getLastInfoText()); - if (getListener().getCurrentState() == State.BEFORE_JOURNEY) { - if (getListener().getLastNotification() != null) { - notificationReceive(getListener().getLastNotification()); - } - if (getListener().lastNarratorText() != null) { - narratorAnnouncement(getListener().lastNarratorText()); - } - setPageJourneyStart(); - } else { - setPageRouteOverview(); - } - } - - private float getUIScale() { - return (float)ModClientConfig.OVERLAY_SCALE.get().doubleValue(); - } - - public JourneyListener getListener() { - return listener == null ? listener = JourneyListenerManager.getInstance().get(listenerId, this) : listener; - } - - public UUID getListenerId() { - return listenerId; - } - - @Override - public UUID getJourneyListenerClientId() { - return clientId; - } - - @Override - public void onClose() { - getListener().unregister(this); - JourneyListenerManager.getInstance().removeClientListenerForAll(this); - } - - - @SuppressWarnings("resource") - @Override - public void tick() { - if (Screen.hasControlDown() && ModKeys.KEY_OVERLAY_SETTINGS.isDown() && Minecraft.getInstance().player.getInventory().hasAnyOf(Set.of(ModItems.NAVIGATOR.get()))) { - ClientWrapper.showRouteOverlaySettingsGui(this); - } - - xPos.tickChaser(); - yPos.tickChaser(); - - //tickSlidingText(2); - - // train info while traveling - if (getListener() != null && getListener().getCurrentState() == JourneyListener.State.WHILE_TRAVELING) { - trainDataSubPageTime++; - if ((slidingTextWidth <= SLIDING_TEXT_AREA_WIDTH && trainDataSubPageTime > TIME_PER_TRAIN_DATA_SUBPAGE) || slidingTextOffset < -(slidingTextWidth / 2)) { - setTrainDataSubPage(false); - } - } - - switch (currentPage) { - case NEXT_CONNECTIONS: // Next connections animation - if (fading) { - break; - } - - connectionsSubPageTime++; - if (connectionsSubPageTime > TIME_PER_CONNECTIONS_SUBPAGE) { - setNextConnectionsSubPage(); - } - break; - case ROUTE_OVERVIEW: - default: - break; - } - } - - public void tickSlidingText(float delta) { - // Sliding text - if (slidingTextWidth > SLIDING_TEXT_AREA_WIDTH * 0.75f) { - slidingTextOffset -= delta; - if (slidingTextOffset < -(slidingTextWidth / 2)) { - slidingTextOffset = (int)((SLIDING_TEXT_AREA_WIDTH + slidingTextWidth / 2) + 20); - } - } - } - - //#region FUNCTIONS - - private void fadeIn(Runnable andThen) { - fadeInternal(andThen, false); - } - - private void fadeOut(Runnable andThen) { - fadeInternal(andThen, true); - } - - private void fadeInternal(Runnable andThen, boolean fadeOut) { - fadeDoneAction = andThen; - fadeInvert = fadeOut; - this.fadeStart = Util.getMillis(); - fading = true; - } - - private void setSlidingText(Component component) { - slidingText = component; - slidingTextWidth = shadowlessFont.width(component); - - if (slidingTextWidth > SLIDING_TEXT_AREA_WIDTH * 0.75f) { - slidingTextOffset = (int)((SLIDING_TEXT_AREA_WIDTH + slidingTextWidth / 2) + 20); - } else { - slidingTextOffset = (int)(SLIDING_TEXT_AREA_WIDTH * 0.75f / 2); - } - } - - private void startStencil(Graphics graphics, int x, int y, int w, int h) { - UIRenderHelper.swapAndBlitColor(Minecraft.getInstance().getMainRenderTarget(), UIRenderHelper.framebuffer); - ModGuiUtils.startStencil(graphics, x, y, w, h); - } - - private void endStencil() { - ModGuiUtils.endStencil(); - UIRenderHelper.swapAndBlitColor(UIRenderHelper.framebuffer, Minecraft.getInstance().getMainRenderTarget()); - } - //#endregion - - //#region ACTIONS - - private void notificationReceive(NotificationData data) { - if (ModClientConfig.ROUTE_NOTIFICATIONS.get()) { - Minecraft.getInstance().getToasts().addToast(NavigatorToast.multiline(data.title(), data.text())); - } - } - - private void journeyBegin(JourneyBeginData data) { - setSlidingText(data.infoText()); - setPageJourneyStart(); - } - - private void nextStop(ContinueData data) { - setTrainDataSubPage(true); - setPageRouteOverview(); - } - - private void finishJourney(FinishJourneyData data) { - setSlidingText(data.infoText()); - setPageJourneyCompleted(); - } - - private void announceNextStop(AnnounceNextStopData data) { - setSlidingText(data.infoText()); - setPageNextConnections(); - } - - private void reachNextStop(ReachNextStopData data) { - setSlidingText(data.infoText()); - - switch (data.transferState()) { - case CONNECTION_MISSED: - setPageConnectionMissed(); - break; - case DEFAULT: - setPageTransfer(); - break; - default: - break; - } - } - - private void journeyInterrupt(JourneyInterruptData data) { - setSlidingText(data.text()); - setPageJourneyInterrupted(data); - } - - private void narratorAnnouncement(String text) { - if (ModClientConfig.ROUTE_NARRATOR.get()) { - Narrator.getNarrator().say(text, true); - } - } - - //#endregion - - //#region PAGE MANAGEMENT - private void setPageRouteOverview() { - fadeOut(() -> { - currentPage = Page.ROUTE_OVERVIEW; - fadeIn(null); - }); - } - - private void setPageJourneyStart() { - fadeOut(() -> { - currentPage = Page.JOURNEY_START; - fadeIn(null); - }); - } - - private void setPageTransfer() { - fadeOut(() -> { - currentPage = Page.TRANSFER; - StationEntry station = getListener().currentStation(); - if (station != null) { - this.messageLabel = MultiLineLabel.create(shadowlessFont, - station.getInfo().platform() == null || station.getInfo().platform().isBlank() ? - ELanguage.translate(keyTransfer, - station.getTrain().trainName(), - station.getTrain().scheduleTitle() - ) : - ELanguage.translate(keyTransferWithPlatform, - station.getTrain().trainName(), - station.getTrain().scheduleTitle(), - station.getInfo().platform() - ), SLIDING_TEXT_AREA_WIDTH - (15 + ModGuiIcons.ICON_SIZE)); - } - fadeIn(null); - }); - } - - private void setPageConnectionMissed() { - fadeOut(() -> { - currentPage = Page.JOURNEY_INTERRUPTED; - StationEntry station = getListener().lastStation(); - if (station != null) { - Component text = TextUtils.translate(keyConnectionMissedPageText, station.getStationName()); - this.messageLabel = MultiLineLabel.create(shadowlessFont, text, SLIDING_TEXT_AREA_WIDTH - 10); - interruptedText = TextUtils.translate(keyConnectionMissed); - } - fadeIn(null); - }); - } - - private void setPageJourneyInterrupted(JourneyInterruptData data) { - fadeOut(() -> { - currentPage = Page.JOURNEY_INTERRUPTED; - this.messageLabel = MultiLineLabel.create(shadowlessFont, data.text(), SLIDING_TEXT_AREA_WIDTH - 10); - interruptedText = TextUtils.text(data.title().getString()); - fadeIn(null); - }); - } - - private void setPageJourneyCompleted() { - fadeOut(() -> { - currentPage = Page.JOURNEY_END; - StationEntry station = getListener().currentStation(); - if (station != null) { - this.messageLabel = MultiLineLabel.create(shadowlessFont, TextUtils.translate(keyAfterJourney, station.getStationName()), SLIDING_TEXT_AREA_WIDTH - 10); - } - fadeIn(null); - }); - } - - private void setPageNextConnections() { - long id = InstanceManager.registerClientNextConnectionsResponseAction((connections, time) -> { - this.connections = connections; - this.connectionsRefreshTime = time; - if (!connections.isEmpty()) { - fadeOut(() -> { - currentPage = Page.NEXT_CONNECTIONS; - connectionsSubPagesCount = connections.size() / CONNECTION_ENTRIES_PER_PAGE + (connections.size() % CONNECTION_ENTRIES_PER_PAGE == 0 ? 0 : 1); - connectionsSubPageIndex = 0; - connectionsSubPageTime = 0; - fadeIn(null); - }); - } - }); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new NextConnectionsRequestPacket(id, getListener().currentStation().getTrain().trainId(), getListener().currentStation().getStationName(), getListener().currentStation().getCurrentTicks() + ModClientConfig.TRANSFER_TIME.get())); - } - - private void setNextConnectionsSubPage() { - if (connectionsSubPagesCount > 1) { - fadeOut(() -> { - connectionsSubPageTime = 0; - connectionsSubPageIndex++; - if (connectionsSubPageIndex >= connectionsSubPagesCount) { - connectionsSubPageIndex = 0; - } - fadeIn(null); - }); - } else { - connectionsSubPageTime = 0; - } - } - - private void setTrainDataSubPage(boolean reset) { - if (reset || trainDataSubPageIndex >= TRAIN_DATA_PAGES) { - trainDataSubPageIndex = 0; - } else { - trainDataSubPageIndex++; - } - - switch (trainDataSubPageIndex) { - default: - case 0: - setSlidingText(ELanguage.translate(keyTrainDetails, - getListener().currentStation().getTrain().trainName(), - getListener().currentStation().getTrain().scheduleTitle() - )); - trainDataSubPageTime = 0; - break; - case 1: - long id = InstanceManager.registerClientTrainDataResponseAction((data, time) -> { - setSlidingText(ModUtils.calcSpeedString(data.speed(), ModClientConfig.SPEED_UNIT.get())); - trainDataSubPageTime = 0; - }); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new TrainDataRequestPacket(id, getListener().currentStation().getTrain().trainId(), false)); - break; - } - } - //#endregion - - //#region RENDERING - @Override - public void render(Graphics graphics, float partialTicks, int width, int height) { - width = Minecraft.getInstance().getWindow().getGuiScaledWidth(); - height = Minecraft.getInstance().getWindow().getGuiScaledHeight(); - partialTicks = Minecraft.getInstance().getFrameTime(); - OverlayPosition pos = ModClientConfig.ROUTE_OVERLAY_POSITION.get(); - final int x = pos == OverlayPosition.TOP_LEFT || pos == OverlayPosition.BOTTOM_LEFT ? 8 : (int)(width - GUI_WIDTH * getUIScale() - 10); - final int y = pos == OverlayPosition.TOP_LEFT || pos == OverlayPosition.TOP_RIGHT ? 8 : (int)(height - GUI_HEIGHT * getUIScale() - 10); - - xPos.chase(x, 0.2f, Chaser.EXP); - yPos.chase(y, 0.2f, Chaser.EXP); - - graphics.poseStack().pushPose(); - graphics.poseStack().translate((int)xPos.getValue(partialTicks), (int)yPos.getValue(partialTicks), 0); - renderInternal(graphics, 0, 0, width, height, partialTicks); - graphics.poseStack().popPose(); - - tickSlidingText(2 * Minecraft.getInstance().getDeltaFrameTime()); - } - - private void renderInternal(Graphics graphics, int x, int y, int width, int height, float partialTicks) { - graphics.poseStack().pushPose(); - float fadePercentage = this.fading ? Mth.clamp((float)(Util.getMillis() - this.fadeStart) / 500.0F, 0.0F, 1.0F) : 1.0F; - float alpha = fadeInvert ? Mth.clamp(1.0f - fadePercentage, 0, 1) : Mth.clamp(fadePercentage, 0, 1); - int fontAlpha = Mth.ceil(alpha * 255.0F) << 24; // | fontAlpha - - graphics.poseStack().scale(getUIScale(), getUIScale(), getUIScale()); - RenderSystem.setShaderTexture(0, GUI); - GuiUtils.drawTexture(GUI, graphics, x, y, GUI_WIDTH, GUI_HEIGHT, 0, getListener().getCurrentState().important() ? 138 : 0, 256, 256); - - GuiUtils.drawString(graphics, shadowlessFont, x + 6, y + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x + 6, y + GUI_HEIGHT - 2 - shadowlessFont.lineHeight, TextUtils.translate(keyOptionsText, TextUtils.translate(InputConstants.getKey(Minecraft.ON_OSX ? InputConstants.KEY_LWIN : InputConstants.KEY_LCONTROL, 0).getName()).append(" + ").append(TextUtils.keybind(keyKeybindOptions)).withStyle(ChatFormatting.BOLD)), 0x4F4F4F, EAlignment.LEFT, false); - - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, x + GUI_WIDTH - 4 - shadowlessFont.width(timeString), y + 4, timeString, 0x4F4F4F, EAlignment.LEFT, false); - - // Test - renderSlidingText(graphics, x, y + 2); - - startStencil(graphics, x + 3, y + 40, 220, 62); - graphics.poseStack().pushPose(); - - graphics.poseStack().translate((fadeInvert ? 0 : -20) + fadePercentage * 20, 0, 0); - if (alpha > 0.1f && (fontAlpha & -67108864) != 0) { - - switch (currentPage) { - case JOURNEY_START: - renderPageJourneyStart(graphics, x, y + 40, fadePercentage, fontAlpha); - break; - case NEXT_CONNECTIONS: - renderNextConnections(graphics, x, y + 40, fadePercentage, fontAlpha, null); - break; - case TRANSFER: - renderPageTransfer(graphics, x, y + 40, fadePercentage, fontAlpha, null); - break; - case JOURNEY_INTERRUPTED: - renderPageJourneyInterrupted(graphics, x, y + 40, fadePercentage, fontAlpha); - break; - case JOURNEY_END: - renderPageJourneyCompleted(graphics, x, y + 40, fadePercentage, fontAlpha); - break; - case ROUTE_OVERVIEW: - final int[] yOffset = new int[] { y + 40 - 1 }; - for (int i = getListener().getIndex(); i < Math.min(getListener().getIndex() + MAX_STATION_PER_PAGE, getListener().getListeningRoute().getStationCount(true)); i++) { - final int k = i; - yOffset[0] += renderRouteOverview(graphics, k, x, yOffset[0], alpha, fontAlpha); - } - break; - default: - break; - } - } - - graphics.poseStack().popPose(); - endStencil(); - graphics.poseStack().popPose(); - - if (fadePercentage >= 1.0f) { - fading = false; - if (fadeDoneAction != null) { - fadeDoneAction.run(); - } - } - } - - public void renderSlidingText(Graphics graphics, int x, int y) { - startStencil(graphics, x + 3, y + 16, 220, 21); - graphics.poseStack().pushPose(); - graphics.poseStack().scale(1.0f / 0.75f, 1.0f / 0.75f, 1.0f / 0.75f); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 3) + slidingTextOffset), y + 14, slidingText, 0xFF9900, EAlignment.CENTER, false); - graphics.poseStack().popPose(); - endStencil(); - } - - public int renderRouteOverview(Graphics graphics, int index, int x, int y, float alphaPercentage, int fontAlpha) { - RenderSystem.setShaderTexture(0, GUI); - RenderSystem.setShaderColor(1, 1, 1, alphaPercentage); - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - RenderSystem.enableDepthTest(); - - Optional stationOptional = getListener().getEntryAt(index); - //Optional lastStation = index > getListener().getIndex() ? getListener().getEntryAt(index - 1) : Optional.empty(); - Optional nextStation = getListener().getEntryAt(index + 1); - - if (!stationOptional.isPresent()) { - return y; - } - StationEntry station = stationOptional.get(); - - boolean reachable = station.reachable(false); - - // Icon - int dY = index <= 0 ? 0 : ROUTE_LINE_HEIGHT; - final int transferY = ROUTE_LINE_HEIGHT * 3; - switch (station.getTag()) { - case PART_START: - dY = ROUTE_LINE_HEIGHT * 2; - break; - case START: - dY = ROUTE_LINE_HEIGHT * 4; - break; - default: - break; - } - GuiUtils.drawTexture(GUI, graphics, x + 75, y, 7, ROUTE_LINE_HEIGHT, 226, dY, 256, 256); - if (index >= getListener().getIndex() + MAX_STATION_PER_PAGE - 1 && station.getTag() != StationTag.END) { - GuiUtils.drawTexture(GUI, graphics, x + 75, y + ROUTE_LINE_HEIGHT, 7, ROUTE_LINE_HEIGHT, 226, ROUTE_LINE_HEIGHT, 256, 256); - } - - // time display - if (station.isTrainCanceled()) { - GuiUtils.drawString(graphics, shadowlessFont, x + 10, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, ELanguage.translate(keyTrainCanceled), DELAYED | fontAlpha, EAlignment.LEFT, false); - } else { - long timeDiff = station.getDifferenceTime(); - MutableComponent timeText = TextUtils.text(TimeUtils.parseTime((int)((station.getScheduleTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get())).withStyle(reachable ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH); - - float scale = shadowlessFont.width(timeText) >= 30 ? 0.7F : 1; - graphics.poseStack().pushPose(); - graphics.poseStack().scale(scale, 1, 1); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 10) / scale), y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, timeText, reachable ? (index <= getListener().getIndex() ? 0xFFFFFF | fontAlpha : 0xDBDBDB | fontAlpha) : DELAYED | fontAlpha, EAlignment.LEFT, false); - graphics.poseStack().popPose(); - - if (station.reachable(false) && station.shouldRenderRealtime()) { - MutableComponent realtimeText = TextUtils.text(TimeUtils.parseTime((int)((station.getEstimatedTimeWithThreshold() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get())).withStyle(reachable ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH); - - float realtimeScale = shadowlessFont.width(realtimeText) >= 30 ? 0.7F : 1; - graphics.poseStack().pushPose(); - graphics.poseStack().scale(realtimeScale, 1, 1); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 40) / realtimeScale), y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, realtimeText, timeDiff < ModClientConfig.DEVIATION_THRESHOLD.get() && reachable ? ON_TIME | fontAlpha : DELAYED | fontAlpha, EAlignment.LEFT, false); - graphics.poseStack().popPose(); - } - } - - // station name display - Component platformText = TextUtils.text(station.getUpdatedInfo().platform()); - int platformTextWidth = shadowlessFont.width(platformText); - - final int maxStationNameWidth = SLIDING_TEXT_AREA_WIDTH - platformTextWidth - 105; - MutableComponent stationText = TextUtils.text(station.getStationName()); - if (index == getListener().getIndex()) stationText = stationText.withStyle(ChatFormatting.BOLD); - if (!reachable) stationText = stationText.withStyle(ChatFormatting.STRIKETHROUGH); - if (shadowlessFont.width(stationText) > maxStationNameWidth) { - stationText = TextUtils.text(shadowlessFont.substrByWidth(stationText, maxStationNameWidth).getString()).append(TextUtils.text("...")).withStyle(stationText.getStyle()); - } - - GuiUtils.drawString(graphics, shadowlessFont, x + 90, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, stationText, reachable ? (index <= getListener().getIndex() ? 0xFFFFFF | fontAlpha : 0xDBDBDB | fontAlpha) : DELAYED | fontAlpha, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x + SLIDING_TEXT_AREA_WIDTH - platformTextWidth, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, platformText, reachable && !station.stationInfoChanged() ? (index <= getListener().getIndex() ? 0xFFFFFF | fontAlpha : 0xDBDBDB | fontAlpha) : DELAYED | fontAlpha, EAlignment.LEFT, false); - - // render transfer - if (station.getTag() == StationTag.PART_END) { - y += ROUTE_LINE_HEIGHT; - RenderSystem.setShaderColor(1, 1, 1, alphaPercentage); - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - RenderSystem.enableDepthTest(); - GuiUtils.drawTexture(GUI, graphics, x + 75, y, 7, ROUTE_LINE_HEIGHT, 226, transferY,256, 256); - if (nextStation.isPresent() && !nextStation.get().reachable(true)) { - if (nextStation.get().isDeparted() || nextStation.get().isTrainCanceled()) { - ModGuiIcons.CROSS.render(graphics, x + 10, y + ROUTE_LINE_HEIGHT - 2 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + 90, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, ELanguage.translate(nextStation.get().isTrainCanceled() ? keyConnectionCanceled : keyConnectionMissed).withStyle(ChatFormatting.BOLD), DELAYED | fontAlpha, EAlignment.LEFT, false); - } else { - ModGuiIcons.WARN.render(graphics, x + 10, y + ROUTE_LINE_HEIGHT - 2 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + 90, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, ELanguage.translate(keyConnectionEndangered).withStyle(ChatFormatting.BOLD), COLOR_WARN | fontAlpha, EAlignment.LEFT, false); - } - } else { - GuiUtils.drawString(graphics, shadowlessFont, x + 10, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, TextUtils.text(TimeUtils.parseDurationShort((int)(getListener().getTransferTime(index)))).withStyle(ChatFormatting.ITALIC), 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x + 90, y + ROUTE_LINE_HEIGHT - 2 - shadowlessFont.lineHeight / 2, ELanguage.translate(keyScheduleTransfer).withStyle(ChatFormatting.ITALIC), 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - } - - return ROUTE_LINE_HEIGHT * 2; - } - return ROUTE_LINE_HEIGHT; - - } - - public void renderNextConnections(Graphics graphics, int x, int y, float alphaPercentage, int fontAlpha, StationEntry station) { - GuiUtils.drawString(graphics, shadowlessFont, x + 10, y + 4, ELanguage.translate(keyNextConnections).withStyle(ChatFormatting.BOLD), 0xFFFFFF | fontAlpha, EAlignment.LEFT, false); - - SimpleTrainConnection[] conns = connections.toArray(SimpleTrainConnection[]::new); - for (int i = connectionsSubPageIndex * CONNECTION_ENTRIES_PER_PAGE, k = 0; i < Math.min((connectionsSubPageIndex + 1) * CONNECTION_ENTRIES_PER_PAGE, connections.size()); i++, k++) { - MutableComponent time = TextUtils.text(TimeUtils.parseTime((int)((connectionsRefreshTime + conns[i].ticks() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get())); - MutableComponent platform = TextUtils.text(conns[i].stationDetails().platform()); - - int x1 = x + 10; - int x2 = x + 55; - int x3 = x + 100; - int x4 = x + SLIDING_TEXT_AREA_WIDTH - shadowlessFont.width(platform); - - final int maxTrainNameWidth = x3 - x2 - 5; - MutableComponent trainName = TextUtils.text(conns[i].trainName()); - if (shadowlessFont.width(trainName) > maxTrainNameWidth) { - trainName = TextUtils.text(shadowlessFont.substrByWidth(trainName, maxTrainNameWidth).getString()).append(TextUtils.text("...")); - } - - final int maxDestinationWidth = x4 - x3 - 5; - MutableComponent destination = TextUtils.text(conns[i].scheduleTitle()); - if (shadowlessFont.width(destination) > maxDestinationWidth) { - destination = TextUtils.text(shadowlessFont.substrByWidth(destination, maxDestinationWidth).getString()).append(TextUtils.text("...")); - } - - GuiUtils.drawString(graphics, shadowlessFont, x1, y + 15 + 12 * k, time, 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x2, y + 15 + 12 * k, trainName, 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x3, y + 15 + 12 * k, destination, 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x4, y + 15 + 12 * k, platform, 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - } - - // page - final int dotSize = 4; - final int dotY = y + 62 - 10; - final int startX = x + GUI_WIDTH / 2 - connectionsSubPagesCount * dotSize - dotSize; - - for (int i = 0; i < connectionsSubPagesCount; i++) { - int s = dotSize + (i == connectionsSubPageIndex ? 2 : 0); - int dX = startX + i * dotSize * 3 - (i == connectionsSubPageIndex ? 1 : 0); - int dY = dotY - (i == connectionsSubPageIndex ? 1 : 0); - GuiUtils.fill(graphics, dX, dY, s, s, i == connectionsSubPageIndex ? 0xFFFFFF | fontAlpha : 0xDBDBDB | fontAlpha); - GuiUtils.fill(graphics, dX + 1, dY + 1, s - 2, s - 2, i == connectionsSubPageIndex ? 0xAAAAAA | fontAlpha : 0x888888 | fontAlpha); - } - - } - - public void renderPageJourneyStart(Graphics graphics, int x, int y, float alphaPercentage, int fontAlpha) { - y += 3 + renderRouteOverview(graphics, getListener().getIndex(), x, y - 3, alphaPercentage, fontAlpha); - GuiUtils.fill(graphics, x + 3, y, SLIDING_TEXT_AREA_WIDTH, 1, 0xDBDBDB | fontAlpha); - - // Title - ModGuiIcons.TIME.render(graphics, x + 10, y + 3); - long time = getListener().currentStation().getEstimatedTimeWithThreshold() - level.getDayTime(); - GuiUtils.drawString(graphics, shadowlessFont, x + 15 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - shadowlessFont.lineHeight / 2, ELanguage.translate(keyDepartureIn).append(" ").append(time > 0 ? TextUtils.text(TimeUtils.parseTime((int)(time % DragonLib.TICKS_PER_DAY), TimeFormat.HOURS_24)) : ELanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFF | fontAlpha, EAlignment.LEFT, false); - y += 5 + ModGuiIcons.ICON_SIZE; - - // Details - final int detailsLineHeight = 12; - //StationEntry station = taggedRoute[0]; - StationEntry endStation = getListener().lastStation(); - - Component platformText = TextUtils.text(endStation.getInfo().platform()); - int platformTextWidth = shadowlessFont.width(platformText); - final int maxStationNameWidth = SLIDING_TEXT_AREA_WIDTH - platformTextWidth - 10 - 5; - MutableComponent stationText = TextUtils.text(TimeUtils.parseTime((int)((endStation.getEstimatedTimeWithThreshold() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get())).append(TextUtils.text(" " + endStation.getStationName())); - if (shadowlessFont.width(stationText) > maxStationNameWidth) { - stationText = TextUtils.text(shadowlessFont.substrByWidth(stationText, maxStationNameWidth).getString()).append(TextUtils.text("...")).withStyle(stationText.getStyle()); - } - - ModGuiIcons.TARGET.render(graphics, x + 10, y + shadowlessFont.lineHeight / 2 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + 15 + ModGuiIcons.ICON_SIZE, y, stationText, 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, x + SLIDING_TEXT_AREA_WIDTH - platformTextWidth, y, platformText, endStation.stationInfoChanged() ? DELAYED | fontAlpha : 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - ModGuiIcons.INFO.render(graphics, x + 10, y + detailsLineHeight + shadowlessFont.lineHeight / 2 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + 15 + ModGuiIcons.ICON_SIZE, y + detailsLineHeight, TextUtils.text(String.format("%s %s | %s", - getListener().getListeningRoute().getTransferCount(), - ELanguage.translate(keyTransferCount).getString(), - TimeUtils.parseDurationShort(getListener().getListeningRoute().getTotalDuration()) - )), 0xDBDBDB | fontAlpha, EAlignment.LEFT, false); - } - - public void renderPageTransfer(Graphics graphics, int x, int y, float alphaPercentage, int fontAlpha, StationEntry station) { - y += 3 + renderRouteOverview(graphics, getListener().getIndex(), x, y - 3, alphaPercentage, fontAlpha); - GuiUtils.fill(graphics, x + 3, y, SLIDING_TEXT_AREA_WIDTH, 1, 0xDBDBDB | fontAlpha); - - // Title - ModGuiIcons.WALK.render(graphics, x + 10, y + 3); - long transferTime = getListener().currentStation().getEstimatedTimeWithThreshold() - level.getDayTime();//getListener().getTransferTime(getListener().getIndex()); - GuiUtils.drawString(graphics, shadowlessFont, x + 15 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - shadowlessFont.lineHeight / 2, ELanguage.translate(keyScheduleTransfer).append(" ").append(transferTime > 0 ? TextUtils.text(TimeUtils.parseTime((int)(transferTime % DragonLib.TICKS_PER_DAY), TimeFormat.HOURS_24)) : ELanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFF | fontAlpha, EAlignment.LEFT, false); - y += 5 + ModGuiIcons.ICON_SIZE; - - // Details - this.messageLabel.renderLeftAligned(graphics.poseStack(), x + 15 + ModGuiIcons.ICON_SIZE, y, 12, 0xDBDBDB | fontAlpha); - } - - public void renderPageJourneyInterrupted(Graphics graphics, int x, int y, float alphaPercentage, int fontAlpha) { - // Title - ModGuiIcons.CROSS.render(graphics, x + 10, y + 3); - GuiUtils.drawString(graphics, shadowlessFont, x + 15 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - shadowlessFont.lineHeight / 2, interruptedText.withStyle(ChatFormatting.BOLD), DELAYED | fontAlpha, EAlignment.LEFT, false); - y += 5 + ModGuiIcons.ICON_SIZE; - - // Details - this.messageLabel.renderLeftAligned(graphics.poseStack(), x + 10, y, 10, 0xDBDBDB | fontAlpha); - } - - public void renderPageJourneyCompleted(Graphics graphics, int x, int y, float alphaPercentage, int fontAlpha) { - // Title - ModGuiIcons.CHECK.render(graphics, x + 10, y + 3); - GuiUtils.drawString(graphics, shadowlessFont, x + 15 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - shadowlessFont.lineHeight / 2, TextUtils.translate(keyJourneyCompleted).withStyle(ChatFormatting.BOLD), ON_TIME | fontAlpha, EAlignment.LEFT, false); - y += 5 + ModGuiIcons.ICON_SIZE; - - // Details - this.messageLabel.renderLeftAligned(graphics.poseStack(), x + 10, y, 10, 0xDBDBDB | fontAlpha); - } - //#endregion - - protected static enum Page { - NONE, - ROUTE_OVERVIEW, - NEXT_CONNECTIONS, - JOURNEY_START, - JOURNEY_END, - TRANSFER, - JOURNEY_INTERRUPTED; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/AbstractRouteDetailsPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/AbstractRouteDetailsPage.java new file mode 100644 index 00000000..27b46901 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/AbstractRouteDetailsPage.java @@ -0,0 +1,20 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; + +public abstract class AbstractRouteDetailsPage extends DLRenderable { + + protected final Font font = Minecraft.getInstance().font; + protected final ClientRoute route; + + public AbstractRouteDetailsPage(ClientRoute route) { + super(0, 0, 220, 62); + this.route = route; + } + + public abstract boolean isImportant(); + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/ConnectionMissedPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/ConnectionMissedPage.java new file mode 100644 index 00000000..71d947da --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/ConnectionMissedPage.java @@ -0,0 +1,42 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.MultiLineLabel; + +public class ConnectionMissedPage extends AbstractRouteDetailsPage { + + private static final String keyConnectionMissed = "gui.createrailwaysnavigator.route_overview.connection_missed"; + private static final String keyConnectionMissedPageText = "gui.createrailwaysnavigator.route_overview.journey_interrupted_info"; + + private MultiLineLabel messageLabel; + + public ConnectionMissedPage(ClientRoute route) { + super(route); + this.messageLabel = MultiLineLabel.create(font, TextUtils.translate(keyConnectionMissedPageText, route.getEnd().getClientTag().tagName()), width() - 10); + } + + @Override + public boolean isImportant() { + return false; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + int y = 3; + // Title + ModGuiIcons.CROSS.render(graphics, 5, y); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, TextUtils.translate(keyConnectionMissed).withStyle(ChatFormatting.BOLD), Constants.COLOR_DELAYED, EAlignment.LEFT, false); + y += 5 + ModGuiIcons.ICON_SIZE; + + // Details + this.messageLabel.renderLeftAligned(graphics.poseStack(), 10, y, font.lineHeight, 0xFFDBDBDB); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/JourneyCompletedPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/JourneyCompletedPage.java new file mode 100644 index 00000000..41ae88b7 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/JourneyCompletedPage.java @@ -0,0 +1,58 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.MultiLineLabel; + +public class JourneyCompletedPage extends AbstractRouteDetailsPage { + + private static final String keyAfterJourney = "gui.createrailwaysnavigator.route_overview.after_journey"; + private static final String keyJourneyCompleted = "gui.createrailwaysnavigator.route_overview.journey_completed"; + + private static final int MAX_TICK_TIME = 200; + private int ticks; + private final Runnable after; + + private MultiLineLabel messageLabel; + + public JourneyCompletedPage(ClientRoute route, Runnable after) { + super(route); + this.messageLabel = MultiLineLabel.create(font, TextUtils.translate(keyAfterJourney, route.getEnd().getClientTag().tagName()), width() - 10); + this.after = after; + } + + @Override + public boolean isImportant() { + return false; + } + + @Override + public void tick() { + super.tick(); + ticks++; + + if (ticks == MAX_TICK_TIME) { + DLUtils.doIfNotNull(after, x -> x.run()); + } + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + int y = 3; + // Title + ModGuiIcons.CHECK.render(graphics, 5, y); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, TextUtils.translate(keyJourneyCompleted).withStyle(ChatFormatting.BOLD), Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + y += 5 + ModGuiIcons.ICON_SIZE; + + // Details + this.messageLabel.renderLeftAligned(graphics.poseStack(), 10, y, font.lineHeight, 0xFFDBDBDB); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java new file mode 100644 index 00000000..3a62722c --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java @@ -0,0 +1,108 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import java.util.ArrayList; +import java.util.List; + +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.crn.registry.ModAccessorTypes.DeparturesData; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.ChatFormatting; + +public class NextConnectionsPage extends AbstractRouteDetailsPage { + + private final List nextConnections = new ArrayList<>(); + private static final String keyNextConnections = "gui.createrailwaysnavigator.route_overview.next_connections"; + + private static final int CONNECTION_ENTRIES_PER_PAGE = 3; + private static final int TIME_PER_CONNECTIONS_SUBPAGE = 200; + private int connectionsSubPageTime = 0; + private int connectionsSubPageIndex = 0; + private int connectionsSubPagesCount = 0; + private final Runnable afterFirstCycle; + private int cycles; + + public NextConnectionsPage(ClientRoute route, Runnable afterFirstCycle) { + super(route); + this.afterFirstCycle = afterFirstCycle; + + DataAccessor.getFromServer(new DeparturesData(route.getCurrentPart().getNextStop().getClientTag().tagId(), route.getCurrentPart().getNextStop().getTrainId()), ModAccessorTypes.GET_DEPARTURES_AT, (stops) -> { + if (stops.isEmpty()) { + afterFirstCycle.run(); + return; + } + nextConnections.addAll(stops); + connectionsSubPagesCount = nextConnections.size() / CONNECTION_ENTRIES_PER_PAGE + (nextConnections.size() % CONNECTION_ENTRIES_PER_PAGE == 0 ? 0 : 1); + }); + } + + @Override + public boolean isImportant() { + return false; + } + + public boolean hasConnections() { + return !nextConnections.isEmpty(); + } + + @Override + public void tick() { + super.tick(); + if (nextConnections.isEmpty()) { + return; + } + connectionsSubPageTime++; + if ((connectionsSubPageTime %= TIME_PER_CONNECTIONS_SUBPAGE) == 0) { + connectionsSubPageIndex++; + if ((connectionsSubPageIndex %= connectionsSubPagesCount) == 0) { + cycles++; + if (cycles == 1) { + DLUtils.doIfNotNull(afterFirstCycle, x -> x.run()); + } + } + } + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + GuiUtils.drawString(graphics, font, 5, 4, ELanguage.translate(keyNextConnections).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + + int y = 16; + final int spacing = 5; + final int timeWidth = 30; + final int trainNameWidth = 40; + for (int i = connectionsSubPageIndex * CONNECTION_ENTRIES_PER_PAGE; i < (connectionsSubPageIndex + 1) * CONNECTION_ENTRIES_PER_PAGE && i < nextConnections.size(); i++) { + TrainStop stop = nextConnections.get(i); + String terminus = stop.getDisplayTitle(); + GuiUtils.drawString(graphics, font, 5, y, TimeUtils.parseTime(stop.getScheduledDepartureTime(), ModClientConfig.TIME_FORMAT.get()), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 5 + timeWidth + spacing, y, GuiUtils.ellipsisString(font, TextUtils.text(stop.getTrainName()), trainNameWidth), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, width() - 5, y, stop.getRealTimeStationTag().info().platform(), 0xFFDBDBDB, EAlignment.RIGHT, false); + int terminusWidth = width() - 10 + timeWidth + trainNameWidth + spacing * 3 - font.width(stop.getRealTimeStationTag().info().platform()); + GuiUtils.drawString(graphics, font, 5 + timeWidth + trainNameWidth + spacing * 2, y, GuiUtils.ellipsisString(font, TextUtils.text(terminus), terminusWidth), 0xFFDBDBDB, EAlignment.LEFT, false); + y += 12; + } + + y = 52; + final int dotSize = 4; + final int center = width() / 2 - dotSize / 2; + final int startX = center - (dotSize * 2) * (connectionsSubPagesCount - 1); + + for (int i = 0; i < connectionsSubPagesCount; i++) { + if (connectionsSubPageIndex == i) { + GuiUtils.drawBox(graphics, new GuiAreaDefinition((startX + (dotSize * 2 * i)) - 1, y - 1, dotSize + 2, dotSize + 2), 0xFFAAAAAA, 0xFFFFFFFF); + } else { + GuiUtils.drawBox(graphics, new GuiAreaDefinition((startX + (dotSize * 2 * i)), y, dotSize, dotSize), 0xFF888888, 0xFFDBDBDB); + } + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java new file mode 100644 index 00000000..89c2899e --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java @@ -0,0 +1,122 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.CRNGui; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.crn.data.navigation.TransferConnection; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.MutableComponent; + +public class RouteOverviewPage extends AbstractRouteDetailsPage { + + public static final int ENTRY_HEIGHT = 14; + + private static final MutableComponent textTransfer = TextUtils.translate("gui.createrailwaysnavigator.route_overview.schedule_transfer"); + private static final MutableComponent textConnectionEndangered = TextUtils.translate("gui.createrailwaysnavigator.route_overview.connection_endangered"); + private static final MutableComponent textConnectionMissed = TextUtils.translate("gui.createrailwaysnavigator.route_overview.connection_missed"); + + public RouteOverviewPage(ClientRoute route) { + super(route); + } + + @Override + public boolean isImportant() { + return false; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + int y = -2; + int n = 0; + List parts = route.getClientParts().stream().skip(Math.max(route.getCurrentPartIndex(), 0)).toList(); + for (int i = 0; i < parts.size(); i++) { + ClientRoutePart part = parts.get(i); + int toSkip = Math.max(part.getNextStopIndex(), 0); + List stops = part.getAllStops().stream().skip(toSkip).toList(); + for (int k = 0; k < stops.size() && n < 5; k++, n++) { + TrainStop stop = stops.get(k); + renderStation(graphics, y, width(), font, stop, toSkip <= 0 && i > 0 && k == 0 ? RoutePathIcons.TRANSFER_STOP : RoutePathIcons.STOP, stop == part.getFirstStop(), !route.isPartReachable(part)); + y += RoutePathIcons.SPRITE_HEIGHT; + if (i < parts.size() - 1 && k >= stops.size() - 1) { + Optional connection = route.getConnectionWith(stop); + if (connection.isPresent()) { + renderTransfer(graphics, y, width(), font, connection.get()); + } + y += RoutePathIcons.SPRITE_HEIGHT; + } + } + } + } + + public static void renderStation(Graphics graphics, int y, int width, Font font, TrainStop stop, RoutePathIcons icon, boolean isStart, boolean isMissed) { + final int precision = ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(); + GuiUtils.drawString(graphics, font, 7, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TextUtils.text(TimeUtils.parseTime((isStart ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime()) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(isMissed ? ChatFormatting.STRIKETHROUGH : ChatFormatting.RESET), isMissed ? Constants.COLOR_DELAYED : 0xFFDBDBDB, EAlignment.LEFT, false); + if (stop.shouldRenderRealTime() && !isMissed) { + GuiUtils.drawString(graphics, font, 7 + 32, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TimeUtils.parseTime((isStart ? stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) : stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision)) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + } + icon.getAsSprite().render(graphics, 10 + 64, y); + GuiUtils.drawString(graphics, font, 17 + 64 + RoutePathIcons.SPRITE_WIDTH, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, GuiUtils.ellipsisString(font, TextUtils.text(stop.getClientTag().tagName()), width - (17 + 64 + RoutePathIcons.SPRITE_WIDTH) - font.width(stop.getRealTimeStationTag().info().platform()) - 10), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, width - 4, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, stop.getRealTimeStationTag().info().platform(), stop.isStationInfoChanged() ? Constants.COLOR_DELAYED : 0xFFDBDBDB, EAlignment.RIGHT, false); + + } + + public static void renderTransfer(Graphics graphics, int y, int width, Font font, TransferConnection connection) { + if (connection.isConnectionMissed()) { + ModGuiIcons.CROSS.getAsSprite(16, 16).render(graphics, 5, y + ENTRY_HEIGHT - 2 - ModGuiIcons.ICON_SIZE / 2); + GuiUtils.drawString(graphics, font, 17 + 64 + RoutePathIcons.SPRITE_WIDTH, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, textConnectionMissed.withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.RED), 0xFFFFFFFF, EAlignment.LEFT, false); + } else if (connection.isConnectionEndangered()) { + ModGuiIcons.WARN.getAsSprite(16, 16).render(graphics, 5, y + ENTRY_HEIGHT - 2 - ModGuiIcons.ICON_SIZE / 2); + GuiUtils.drawString(graphics, font, 17 + 64 + RoutePathIcons.SPRITE_WIDTH, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, textConnectionEndangered.withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.GOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + } else { + GuiUtils.drawString(graphics, font, 7, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TextUtils.text(TimeUtils.parseDurationShort((int)connection.getRealTimeTransferTime())).withStyle(ChatFormatting.ITALIC), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 17 + 64 + RoutePathIcons.SPRITE_WIDTH, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, textTransfer.withStyle(ChatFormatting.ITALIC), 0xFFDBDBDB, EAlignment.LEFT, false); + } + RoutePathIcons.TRANSFER.getAsSprite().render(graphics, 10 + 64, y); + } + + public static enum RoutePathIcons { + CURRENT(0), + STOP(1), + TRANSFER_STOP(2), + TRANSFER(3), + START(4); + + private static final int V = 30; + private static final int START_U = 21; + private static final int SPRITE_WIDTH = 7; + private static final int SPRITE_HEIGHT = 14; + private int index; + + private RoutePathIcons(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static RoutePathIcons getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(START); + } + + public Sprite getAsSprite() { + return new Sprite(CRNGui.GUI, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT, START_U + getIndex() * SPRITE_WIDTH, V, SPRITE_WIDTH, SPRITE_HEIGHT, SPRITE_WIDTH, SPRITE_HEIGHT, 0, 0); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TrainCancelledInfo.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TrainCancelledInfo.java new file mode 100644 index 00000000..e86eb570 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TrainCancelledInfo.java @@ -0,0 +1,42 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.MultiLineLabel; + +public class TrainCancelledInfo extends AbstractRouteDetailsPage { + + private static final String keyTrainCancelled = "gui.createrailwaysnavigator.route_overview.train_cancelled_title"; + private static final String keyTrainCancelledInfoText = "gui.createrailwaysnavigator.route_overview.train_cancelled_info"; + + private MultiLineLabel messageLabel; + + public TrainCancelledInfo(ClientRoute route, String trainName) { + super(route); + this.messageLabel = MultiLineLabel.create(font, TextUtils.translate(keyTrainCancelledInfoText, trainName), width() - 10); + } + + @Override + public boolean isImportant() { + return false; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + int y = 3; + // Title + ModGuiIcons.CROSS.render(graphics, 5, y); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, TextUtils.translate(keyTrainCancelled).withStyle(ChatFormatting.BOLD), Constants.COLOR_DELAYED, EAlignment.LEFT, false); + y += 5 + ModGuiIcons.ICON_SIZE; + + // Details + this.messageLabel.renderLeftAligned(graphics.poseStack(), 10, y, font.lineHeight, 0xFFDBDBDB); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java new file mode 100644 index 00000000..3ff0b2ea --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java @@ -0,0 +1,69 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.overlay.pages.RouteOverviewPage.RoutePathIcons; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.TransferConnection; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.MultiLineLabel; + +public class TransferPage extends AbstractRouteDetailsPage { + + private static final String keyScheduleTransfer = "gui.createrailwaysnavigator.route_overview.schedule_transfer"; + private static final String keyTransfer = "gui.createrailwaysnavigator.route_overview.transfer"; + private static final String keyTransferWithPlatform = "gui.createrailwaysnavigator.route_overview.transfer_with_platform"; + private static final String keyTimeNow = "gui.createrailwaysnavigator.time.now"; + + private final TransferConnection connection; + private MultiLineLabel messageLabel; + + public TransferPage(ClientRoute route, TransferConnection connection) { + super(route); + this.connection = connection; + + String terminus = connection.getDepartureStation().getDisplayTitle(); + StationInfo info = connection.getDepartureStation().getRealTimeStationTag().info(); + this.messageLabel = MultiLineLabel.create(font, + info.platform() == null || info.platform().isBlank() ? + ELanguage.translate(keyTransfer, + connection.getDepartureStation().getTrainName(), + terminus + ) : + ELanguage.translate(keyTransferWithPlatform, + connection.getDepartureStation().getTrainName(), + terminus, + info.platform() + ), width - (15 + ModGuiIcons.ICON_SIZE)); + } + + @Override + public boolean isImportant() { + return true; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + int y = 0; + RouteOverviewPage.renderStation(graphics, -4, width(), font, connection.getDepartureStation(), RoutePathIcons.START, true, connection.isConnectionMissed()); + y += 16; + GuiUtils.fill(graphics, 0, y, width(), 1, 0xFFDBDBDB); + + // Title + ModGuiIcons.WALK.render(graphics, 5, y + 3); + long transferTime = connection.getDepartureStation().getRealTimeDepartureTime() - DragonLib.getCurrentWorldTime(); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, ELanguage.translate(keyScheduleTransfer).append(" ").append(transferTime > 0 ? TextUtils.text(TimeUtils.parseDurationShort((int)transferTime)) : ELanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + y += 5 + ModGuiIcons.ICON_SIZE; + + // Details + this.messageLabel.renderLeftAligned(graphics.poseStack(), 10 + ModGuiIcons.ICON_SIZE, y, font.lineHeight, 0xFFDBDBDB); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java new file mode 100644 index 00000000..b70b04ef --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java @@ -0,0 +1,69 @@ +package de.mrjulsen.crn.client.gui.overlay.pages; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.overlay.pages.RouteOverviewPage.RoutePathIcons; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class WelcomePage extends AbstractRouteDetailsPage { + + private static final String keyDepartureIn = "gui.createrailwaysnavigator.route_details.departure"; + private static final String keyTimeNow = "gui.createrailwaysnavigator.time.now"; + private static final String keyTransferCount = "gui.createrailwaysnavigator.navigator.route_entry.transfer"; + + public WelcomePage(ClientRoute route) { + super(route); + } + + @Override + public boolean isImportant() { + return true; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + int y = 16; + RouteOverviewPage.renderStation(graphics, -4, width(), font, route.getStart(), RoutePathIcons.START, true, false); + GuiUtils.fill(graphics, 0, y, width(), 1, 0xFFDBDBDB); + + // Title + ModGuiIcons.TIME.render(graphics, 5, y + 3); + long time = route.getCurrentPart().departureIn(); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, ELanguage.translate(keyDepartureIn).append(" ").append(time > 0 ? TextUtils.text(TimeUtils.parseDurationShort((int)time)) : ELanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + y += 5 + ModGuiIcons.ICON_SIZE; + + // Details + final int detailsLineHeight = 12; + //StationEntry station = taggedRoute[0]; + ClientTrainStop endStation = route.getLastClientPart().getLastClientStop(); + + Component platformText = TextUtils.text(endStation.getRealTimeStationTag().info().platform()); + int platformTextWidth = font.width(platformText); + final int maxStationNameWidth = width() - platformTextWidth - 10 - 5; + MutableComponent stationText = TextUtils.text(ModUtils.formatTime(endStation.getRoundedRealTimeArrivalTime(), false)).append(TextUtils.text(" " + endStation.getClientTag().tagName())); + if (font.width(stationText) > maxStationNameWidth) { + stationText = TextUtils.text(font.substrByWidth(stationText, maxStationNameWidth).getString()).append(TextUtils.text("...")).withStyle(stationText.getStyle()); + } + + ModGuiIcons.TARGET.render(graphics, 5, y + font.lineHeight / 2 - ModGuiIcons.ICON_SIZE / 2); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y, stationText, 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, width() - 5, y, platformText, endStation.isStationInfoChanged() ? Constants.COLOR_DELAYED : 0xFFDBDBDB, EAlignment.RIGHT, false); + ModGuiIcons.INFO.render(graphics, 5, y + detailsLineHeight + font.lineHeight / 2 - ModGuiIcons.ICON_SIZE / 2); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + detailsLineHeight, TextUtils.text(String.format("%s %s | %s", + route.getTransferCount(), + ELanguage.translate(keyTransferCount).getString(), + TimeUtils.parseDurationShort((int)route.travelTime()) + )), 0xFFDBDBDB, EAlignment.LEFT, false); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractBlacklistScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractBlacklistScreen.java deleted file mode 100644 index 1230317a..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractBlacklistScreen.java +++ /dev/null @@ -1,357 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; -import com.simibubi.create.foundation.gui.AllIcons; -import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; -import de.mrjulsen.crn.client.gui.widgets.ModStationSuggestions; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; -import net.minecraft.world.level.Level; - -public abstract class AbstractBlacklistScreen extends DLScreen { - - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings.png"); - public static final ResourceLocation WIDGETS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings_widgets.png"); - private static final int GUI_WIDTH = 255; - private static final int GUI_HEIGHT = 247; - - private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; - private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; - private static final int ENTRIES_START_Y_OFFSET = 10; - private static final int ENTRY_HEIGHT = 20; - - private final int AREA_X = 16; - private final int AREA_Y = 54 + 18; - private final int AREA_W = 220; - private final int AREA_H = 156 - 18; - - - private int guiLeft, guiTop; - private LerpedFloat scroll = LerpedFloat.linear().startWithValue(0); - - // Data - private final Level level; - private final Font shadowlessFont; - private final Screen lastScreen; - - private boolean initialized; - - // Controls - private DLCreateIconButton backButton; - private DLEditBox newEntryBox; - private DLEditBox searchBox; - private GuiAreaDefinition addButton; - private ModStationSuggestions suggestions; - private final Map blacklistEntryButton = new HashMap<>(); - private final Map entryAreas = new HashMap<>(); - - // Tooltips - private final MutableComponent tooltipAdd = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".blacklist.add.tooltip"); - private final MutableComponent tooltipRemove = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".blacklist.delete.tooltip"); - - @SuppressWarnings("resource") - public AbstractBlacklistScreen(Level level, Screen lastScreen, Component title) { - super(title); - this.level = level; - this.lastScreen = lastScreen; - this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); - } - - protected abstract Collection getSuggestions(); - protected abstract boolean checkIsBlacklisted(String entry); - protected abstract String[] getBlacklistedNames(String searchText); - protected abstract void addToBlacklist(String name, Runnable andThen); - protected abstract void removeFromBlacklist(String name, Runnable andThen); - - @Override - protected void init() { - initialized = false; - super.init(); - guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; - - backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIG_BACK) { - @Override - public void onClick(double mouseX, double mouseY) { - super.onClick(mouseX, mouseY); - onClose(); - } - }); - addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_BACK).assignedTo(backButton)); - - newEntryBox = addEditBox(guiLeft + AREA_X + 5 + 35, guiTop + AREA_Y - 28 + 10, 129, 12, "", TextUtils.empty(), false, (s) -> { - updateEditorSubwidgets(newEntryBox); - }, (box, focus) -> { - if (!focus) { - clearSuggestions(); - } - }, null); - newEntryBox.setMaxLength(25); - newEntryBox.setTextColor(0xFFFFFF); - - addButton = new GuiAreaDefinition(guiLeft + AREA_X + 165 + 10, guiTop + AREA_Y - 28 + 6, 16, 16); - addTooltip(DLTooltip.of(tooltipAdd).assignedTo(addButton)); - - searchBox = addEditBox(guiLeft + AREA_X + 1, guiTop + 16 + 1, AREA_W - 2, 16, "", Constants.TEXT_SEARCH, true, (s) -> { - initStationDeleteButtons(); - }, null, null); - - initialized = true; - initStationDeleteButtons(); - } - - private void initStationDeleteButtons() { - if (!initialized) { - return; - } - - blacklistEntryButton.clear(); - entryAreas.clear(); - - String[] names = getBlacklistedNames(searchBox.getValue()); - for (int i = 0; i < names.length; i++) { - String name = names[i]; - blacklistEntryButton.put(name, new GuiAreaDefinition(guiLeft + AREA_X + 165 + 10, guiTop + AREA_Y + (i * ENTRY_HEIGHT) + 2, 16, 16)); - entryAreas.put(name, new GuiAreaDefinition(guiLeft + AREA_X + 35, guiTop + AREA_Y + (i * ENTRY_HEIGHT) + 2, 129, 16)); - } - } - - private void addToBlacklistInternal() { - String value = newEntryBox.getValue(); - newEntryBox.setValue(""); - - if (value.isBlank()) { - return; - } - - addToBlacklist(value, this::initStationDeleteButtons); - } - - private int getMaxScrollHeight() { - return ENTRIES_START_Y_OFFSET + ENTRY_HEIGHT * getBlacklistedNames(searchBox.getValue()).length; - } - - private void clearSuggestions() { - if (suggestions != null) { - suggestions.getEditBox().setSuggestion(""); - } - suggestions = null; - } - - protected void updateEditorSubwidgets(DLEditBox field) { - if (!initialized) { - return; - } - - clearSuggestions(); - - suggestions = new ModStationSuggestions(minecraft, this, field, minecraft.font, getViableStations(field), field.getHeight() + 2 + field.y); - suggestions.setAllowSuggestions(true); - suggestions.updateCommandInfo(); - } - - - private List getViableStations(DLEditBox field) { - return getSuggestions().stream() - .distinct() - .filter(x -> !checkIsBlacklisted(x)) - .sorted((a, b) -> a.compareTo(b)) - .toList(); - } - - @Override - public void onClose() { - minecraft.setScreen(lastScreen); - } - - @Override - public void tick() { - super.tick(); - scroll.tickChaser(); - - float scrollMax = getMaxScrollHeight(); - if (scroll.getValue() > 0 && scroll.getValue() > scrollMax) { - scroll.chase(Math.max(0, scrollMax), 0.7f, Chaser.EXP); - } - - if (suggestions != null) { - suggestions.tick(); - - if (!newEntryBox.canConsumeInput()) { - clearSuggestions(); - } - } - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - pPartialTick = Minecraft.getInstance().getFrameTime(); - float scrollOffset = -scroll.getValue(pPartialTick); - - renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - - GuiUtils.drawTexture(WIDGETS, graphics, guiLeft + AREA_X + 10, guiTop + AREA_Y - 28, 0, 110, 200, 28); - GuiUtils.drawTexture(WIDGETS, graphics, guiLeft + AREA_X + 35, guiTop + AREA_Y - 28 + 5, 0, 92, 139, 18); - GuiUtils.drawTexture(WIDGETS, graphics, addButton.getX(), addButton.getY(), 200, 16, 16, 16); // add button - if (addButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, addButton.getX(), addButton.getY(), addButton.getWidth(), addButton.getHeight(), 0x1AFFFFFF); - } - - GuiUtils.enableScissor(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - graphics.poseStack().translate(0, scrollOffset, 0); - - String[] blacklist = getBlacklistedNames(searchBox.getValue()); - for (int i = 0; i < blacklist.length; i++) { - GuiUtils.drawTexture(WIDGETS, graphics, guiLeft + AREA_X + 10, guiTop + AREA_Y + (i * ENTRY_HEIGHT), 0, 4, 200, ENTRY_HEIGHT); - } - GuiUtils.drawTexture(WIDGETS, graphics, guiLeft + AREA_X + 10, guiTop + AREA_Y + (blacklist.length * ENTRY_HEIGHT), 0, 23, 200, 3); - GuiUtils.drawTexture(WIDGETS, graphics, guiLeft + AREA_X + 10, guiTop + AREA_Y + (blacklist.length * ENTRY_HEIGHT) + 3, 0, 46, 200, 2); - - for (GuiAreaDefinition def : blacklistEntryButton.values()) { - GuiUtils.drawTexture(WIDGETS, graphics, def.getX(), def.getY(), 232, 0, 16, 16); // delete button - - if (def.isInBounds(pMouseX, pMouseY - scrollOffset)) { - GuiUtils.fill(graphics, def.getX(), def.getY(), def.getWidth(), def.getHeight(), 0x1AFFFFFF); - } - } - - for (int i = 0; i < blacklist.length; i++) { - MutableComponent name = TextUtils.text(blacklist[i]); - int maxTextWidth = 129; - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + AREA_X + 40, guiTop + AREA_Y + (i * ENTRY_HEIGHT) + 6, name, 0xFFFFFF, EAlignment.LEFT, false); - } - - GuiUtils.disableScissor(graphics); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y - 38, 0, AREA_W, 10, 0x77000000, 0x00000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y + AREA_H - 10, 0, AREA_W, 10, 0x00000000, 0x77000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X + 10, guiTop + AREA_Y, 0, AREA_W - 20, 10, 0x77000000, 0x00000000); - - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - - // Scrollbar - double maxHeight = getMaxScrollHeight(); - double aH = AREA_H + 1; - if (aH / maxHeight < 1) { - int scrollerHeight = Math.max(10, (int)(aH * (aH / maxHeight))); - int startY = guiTop + AREA_Y + (int)((AREA_H) * (Math.abs(scrollOffset) / maxHeight)); - - GuiUtils.fill(graphics, guiLeft + AREA_X + AREA_W - 3, startY, 3, scrollerHeight, 0x7FFFFFFF); - } - - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 19, guiTop + 4, title, 0xFF4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + GUI_WIDTH - 22 - shadowlessFont.width(timeString), guiTop + 4, TextUtils.text(timeString), 0xFF4F4F4F, EAlignment.LEFT, false); - } - - - @Override - public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { - if (suggestions != null) { - graphics.poseStack().pushPose(); - graphics.poseStack().translate(0, 0, 500); - suggestions.render(graphics.poseStack(), mouseX, mouseY); - graphics.poseStack().popPose(); - } - - float scrollOffset = scroll.getValue(partialTick); - - if (mouseX > guiLeft + AREA_X && mouseX < guiLeft + AREA_X + AREA_W && mouseY > guiTop + AREA_Y && mouseY < guiTop + AREA_Y + AREA_H) { - for (Entry entry : blacklistEntryButton.entrySet()) { - if (GuiUtils.renderTooltipWithOffset(this, entry.getValue(), List.of(tooltipRemove), width, graphics, mouseX, mouseY, 0, (int)scrollOffset)) { - break; - } - } - - for (Entry entry : entryAreas.entrySet()) { - if (shadowlessFont.width(entry.getKey()) > 129 && GuiUtils.renderTooltipAt(this, entry.getValue(), List.of(TextUtils.text(entry.getKey())), width, graphics, entry.getValue().getLeft() + 1, (int)(entry.getValue().getTop() - scrollOffset), mouseX, mouseY, 0, (int)scrollOffset)) { - break; - } - } - } - super.renderFrontLayer(graphics, mouseX, mouseY, partialTick); - } - - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - - if (suggestions != null && suggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton)) { - GuiUtils.playButtonSound(); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - float scrollOffset = scroll.getValue(); - - if (addButton.isInBounds(pMouseX, pMouseY) && !newEntryBox.getValue().isBlank()) { - addToBlacklistInternal(); - GuiUtils.playButtonSound(); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (pMouseX > guiLeft + AREA_X && pMouseX < guiLeft + AREA_X + AREA_W && pMouseY > guiTop + AREA_Y && pMouseY < guiTop + AREA_Y + AREA_H && blacklistEntryButton.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY + scrollOffset))) { - for (Entry entry : blacklistEntryButton.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY + scrollOffset)) { - removeFromBlacklist(entry.getKey(), this::initStationDeleteButtons); - GuiUtils.playButtonSound(); - return super.mouseClicked(pMouseX, pMouseY + scrollOffset, pButton); - } - } - } - - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - @Override - public boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) { - if (suggestions != null && suggestions.keyPressed(pKeyCode, pScanCode, pModifiers)) - return true; - - return super.keyPressed(pKeyCode, pScanCode, pModifiers); - } - - @Override - public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { - if (suggestions != null && suggestions.mouseScrolled(pMouseX, pMouseY, Mth.clamp(pDelta, -1.0D, 1.0D))) - return true; - - float chaseTarget = scroll.getChaseTarget(); - float max = -AREA_H + getMaxScrollHeight(); - - if (max > 0) { - chaseTarget -= pDelta * 12; - chaseTarget = Mth.clamp(chaseTarget, 0, max); - scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP); - } else - scroll.chase(0, 0.7f, Chaser.EXP); - - return super.mouseScrolled(pMouseX, pMouseY, pDelta); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractEntryListSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractEntryListSettingsScreen.java deleted file mode 100644 index 343f763f..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractEntryListSettingsScreen.java +++ /dev/null @@ -1,355 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.ArrayList; -import java.util.List; - -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; -import com.simibubi.create.foundation.gui.AllIcons; -import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.widgets.AbstractEntryListOptionWidget; -import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; -import de.mrjulsen.crn.client.gui.widgets.WidgetContainerCollection; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.client.util.WidgetsCollection; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.MathUtils; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.Level; - -public abstract class AbstractEntryListSettingsScreen extends DLScreen { - - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings.png"); - private static final int GUI_WIDTH = 255; - private static final int GUI_HEIGHT = 247; - - private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; - private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; - - private static final int ENTRIES_START_Y_OFFSET = 10; - private final int ENTRY_SPACING = 4; - - private final int AREA_X = 16; - private final int AREA_Y = 16 + 18; // + searchbar height - private final int AREA_W = 220; - private final int AREA_H = 194 - 18; // + searchbar height - - private int guiLeft, guiTop; - - private boolean initialized = false; - - // Data - private final Level level; - private final Font shadowlessFont; - private final Screen lastScreen; - private boolean initEntries = false; - private boolean renderingEntries = false; - - // Widgets - private final WidgetContainerCollection entriesCollection = new WidgetContainerCollection(); - private final WidgetsCollection newEntryCollection = new WidgetsCollection(); - private DLEditBox newEntryInputBox; - private DLCreateIconButton backButton; - private DLCreateIconButton addButton; - private DLEditBox searchBox; - private LerpedFloat scroll = LerpedFloat.linear().startWithValue(0); - - // Tooltips - private final MutableComponent tooltipAdd = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.add.tooltip"); - private final MutableComponent textEnterName = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.enter_name"); - - public static record WidgetCreationData>(W parent, int x, int y, List previousEntries) {} - - @SuppressWarnings("resource") - public AbstractEntryListSettingsScreen(Level level, Screen lastScreen, Component title) { - super(title); - this.level = level; - this.lastScreen = lastScreen; - this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); - } - - protected abstract D[] getData(String searchText); - protected abstract W createWidget(WidgetCreationData> widgetData, D data); - - @Override - protected void init() { - initialized = false; - super.init(); - guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; - - - backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIG_BACK) - .withCallback((x, y) -> { - onClose(); - })); - addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_BACK).assignedTo(backButton)); - - addButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 43, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_ADD)); - addButton.withCallback((x, y) -> { - addButton.visible = false; - newEntryCollection.setVisible(true); - newEntryInputBox.setValue(""); - }); - addTooltip(DLTooltip.of(tooltipAdd).assignedTo(addButton)); - - searchBox = addEditBox(guiLeft + AREA_X + 1, guiTop + 16 + 1, AREA_W - 2, 16, "", Constants.TEXT_SEARCH, true, (b) -> { - refreshEntries(); - }, NO_EDIT_BOX_FOCUS_CHANGE_ACTION, null); - - // Add new Entry - newEntryCollection.add(addRenderableWidget(new DLCreateIconButton(guiLeft + 145, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIRM)) - .withCallback((x, y) -> { - if (newEntryInputBox.getValue().isBlank()) { - return; - } - - onCreateNewEntry(newEntryInputBox.getValue(), () -> { - refreshEntries(); - scroll.chase(getScrollMax(), 0.7f, Chaser.EXP); - }); - - cancelNewEntryCreation(null); - })); - - newEntryCollection.add(addRenderableWidget(new DLCreateIconButton(guiLeft + 145 + DEFAULT_ICON_BUTTON_WIDTH, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_DISABLE)) - .withCallback((x, y) -> { - cancelNewEntryCreation(null); - })); - - newEntryCollection.add(newEntryInputBox = addEditBox(guiLeft + 44, guiTop + 223, 100, 16, "", textEnterName, true, (b) -> {}, (box, focus) -> {}, null)); - - initialized = true; - newEntryCollection.setVisible(false); - refreshEntries(); - } - - protected abstract void onCreateNewEntry(String value, Runnable refreshAction); - - private boolean cancelFocus = false; - private void cancelNewEntryCreation(DLEditBox excluded) { - if (cancelFocus) { - return; - } - cancelFocus = true; - addButton.visible = true; - newEntryCollection.setVisible(false); - //renderables.stream().filter(x -> x instanceof DLEditBox && excluded != x).forEach(x -> ((DLEditBox)x).setFocus(false)); - cancelFocus = false; - } - - protected void refreshEntries() { - if (!initialized) { - return; - } - initEntries = true; - - while (renderingEntries) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - int startY = guiTop + AREA_X + ENTRIES_START_Y_OFFSET; - List previousComponents = new ArrayList<>(entriesCollection.components); - entriesCollection.components.forEach(x -> removeWidget(x)); - entriesCollection.components.clear(); - - // Entries - D[] data = getData(searchBox.getValue()); - for (int i = 0; i < data.length; i++) { - W w = createWidget(new WidgetCreationData>(this, guiLeft + AREA_X + 10, startY, previousComponents), data[i]); - entriesCollection.components.add(w); - w.calcHeight(); - } - initEntries = false; - } - - @Override - public void onClose() { - minecraft.setScreen(lastScreen); - } - - @Override - public void tick() { - searchBox.tick(); - scroll.tickChaser(); - - float scrollMax = getScrollMax(); - if (scroll.getValue() > 0 && scroll.getValue() > scrollMax) { - scroll.chase(Math.max(0, getScrollMax()), 0.7f, Chaser.EXP); - } - - entriesCollection.performForEachOfType(AbstractEntryListOptionWidget.class, x -> x.tick()); - newEntryCollection.performForEachOfType(DLEditBox.class, x -> x.tick()); - } - - public int getScrollOffset(float pPartialTicks) { - return (int)scroll.getValue(pPartialTicks); - } - - private float getScrollMax() { - float max = -AREA_H + (ENTRIES_START_Y_OFFSET * 2); - for (WidgetContainer w : entriesCollection.components) { - max += 4 + w.getHeight(); - } - return max; - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - pPartialTick = Minecraft.getInstance().getFrameTime(); - float scrollOffset = -scroll.getValue(pPartialTick); - - renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 19, guiTop + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + GUI_WIDTH - 22 - shadowlessFont.width(timeString), guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.LEFT, false); - - GuiUtils.enableScissor(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - graphics.poseStack().translate(0, scrollOffset, 0); - - renderingEntries = true; - while (initEntries) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - // render entries - int currentY = guiTop + AREA_Y + ENTRIES_START_Y_OFFSET; - for (int i = 0; i < entriesCollection.components.size(); i++) { - - if (entriesCollection.components.get(i) instanceof AbstractEntryListOptionWidget widget) { - widget.setYPos(currentY); - widget.calcHeight(); - if (currentY < guiTop + AREA_Y + AREA_H - scrollOffset && currentY + widget.getHeight() > guiTop + AREA_Y - scrollOffset) { - widget.render(graphics.poseStack(), pMouseX, (int)(pMouseY - scrollOffset), pPartialTick); - } - currentY += widget.getHeight() + ENTRY_SPACING; - } - - } - - //super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - renderingEntries = false; - - GuiUtils.disableScissor(graphics); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y, 0, AREA_W, 10, 0x77000000, 0x00000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y + AREA_H - 10, 0, AREA_W, 10, 0x00000000, 0x77000000); - - // Scrollbar - int componentHeight = entriesCollection.components.stream().mapToInt(x -> x.getHeight() + ENTRY_SPACING).sum(); - double maxHeight = ENTRIES_START_Y_OFFSET * 2 + componentHeight; - double aH = AREA_H + 1; - if (aH / maxHeight < 1) { - int scrollerHeight = Math.max(10, (int)(aH * (aH / maxHeight))); - int startY = guiTop + AREA_Y + (int)((AREA_H) * (Math.abs(scrollOffset) / maxHeight)); - - GuiUtils.fill(graphics, guiLeft + AREA_X + AREA_W - 3, startY, 3, scrollerHeight, 0x7FFFFFFF); - } - - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - } - - @Override - public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTicks) { - entriesCollection.performForEachOfType(AbstractEntryListOptionWidget.class, x -> { - if (pMouseX > guiLeft + AREA_X && pMouseX < guiLeft + AREA_X + AREA_W && pMouseY > guiTop + AREA_Y && pMouseY < guiTop + AREA_Y + AREA_H) { - x.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTicks); - } - x.renderSuggestions(graphics.poseStack(), pMouseX, pMouseY, pPartialTicks); - }); - - super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTicks); - } - - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - float scrollOffset = scroll.getValue(); - for (WidgetContainer w : entriesCollection.components) { - if (pMouseX > guiLeft + AREA_X && pMouseX < guiLeft + AREA_X + AREA_W && pMouseY > guiTop + AREA_Y && pMouseY < guiTop + AREA_Y + AREA_H) { - if (w.mouseClicked(pMouseX, pMouseY + scrollOffset, pButton)) { - break; - } - } - - if (w instanceof AbstractEntryListOptionWidget entry && entry.mouseClickedLoop(pMouseX, pMouseY + scrollOffset, pButton)) { - break; - } - } - - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - @Override - public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { - float scrollOffset = scroll.getValue(); - - for (WidgetContainer w : entriesCollection.components) { - if (w instanceof AbstractEntryListOptionWidget entry && entry.mouseScrolledLoop(pMouseX, pMouseY + scrollOffset, pDelta)) { - return true; - } - - if (pMouseX > guiLeft + AREA_X && pMouseX < guiLeft + AREA_X + AREA_W && pMouseY > guiTop + AREA_Y && pMouseY < guiTop + AREA_Y + AREA_H) { - if (w.mouseScrolled(pMouseX, pMouseY + scrollOffset, pDelta)) { - return true; - } - } - } - - float chaseTarget = scroll.getChaseTarget(); - float max = getScrollMax(); - - if (max > 0) { - chaseTarget -= pDelta * 12; - chaseTarget = MathUtils.clamp(chaseTarget, 0, max); - scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP); - } else - scroll.chase(0, 0.7f, Chaser.EXP); - - return super.mouseScrolled(pMouseX, pMouseY, pDelta); - } - - @Override - public boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) { - for (WidgetContainer w : entriesCollection.components) { - if (w.keyPressed(pKeyCode, pScanCode, pModifiers)) { - return true; - } - } - return super.keyPressed(pKeyCode, pScanCode, pModifiers); - } - - @Override - public boolean charTyped(char pCodePoint, int pModifiers) { - for (WidgetContainer w : entriesCollection.components) { - if (w.charTyped(pCodePoint, pModifiers)) { - return true; - } - } - return super.charTyped(pCodePoint, pModifiers); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java new file mode 100644 index 00000000..b94e1931 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java @@ -0,0 +1,68 @@ +package de.mrjulsen.crn.client.gui.screen; + +import com.simibubi.create.foundation.gui.AllIcons; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public abstract class AbstractNavigatorScreen extends DLScreen { + + protected static final int GUI_WIDTH = 240;//255; + protected static final int GUI_HEIGHT = 247; + + protected BarColor primaryColoring; + protected int guiLeft, guiTop; + + protected DLCreateIconButton backButton; + + protected final Screen lastScreen; + + protected AbstractNavigatorScreen(Screen lastScreen, Component title, BarColor primaryColoring) { + super(title); + this.lastScreen = lastScreen; + this.primaryColoring = primaryColoring; + } + + @Override + protected void init() { + super.init(); + guiLeft = this.width / 2 - GUI_WIDTH / 2; + guiTop = this.height / 2 - GUI_HEIGHT / 2; + + backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 8, guiTop + 223, DLIconButton.DEFAULT_BUTTON_WIDTH, DLIconButton.DEFAULT_BUTTON_HEIGHT, AllIcons.I_CONFIG_BACK) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + onClose(); + } + }); + addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_BACK).assignedTo(backButton)); + } + + @SuppressWarnings("resource") + public void renderNavigatorBackground(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderScreenBackground(graphics); + CreateDynamicWidgets.renderWindow(graphics, guiLeft, guiTop, GUI_WIDTH, GUI_HEIGHT, ContainerColor.GRAY, primaryColoring, FooterSize.DEFAULT.size(), FooterSize.SMALL.size(), false); + GuiUtils.drawString(graphics, font, guiLeft + 6, guiTop + 4, getTitle(), 0x4F4F4F, EAlignment.LEFT, false); + String timeString = TimeUtils.parseTime((int)((Minecraft.getInstance().level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH - 6, guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.RIGHT, false); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java index e4fbd1e4..7dc93f06 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java @@ -11,19 +11,19 @@ import com.simibubi.create.foundation.gui.widget.ScrollInput; import com.simibubi.create.foundation.utility.Components; +import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.AbstractAdvancedSidedDisplayBlock; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.properties.EDisplayType; +import de.mrjulsen.crn.block.properties.ESide; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; import de.mrjulsen.crn.client.gui.widgets.DLCreateLabel; import de.mrjulsen.crn.client.gui.widgets.DLCreateSelectionScrollInput; import de.mrjulsen.crn.config.ModCommonConfig; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.EDisplayInfo; -import de.mrjulsen.crn.data.EDisplayType; -import de.mrjulsen.crn.data.ESide; -import de.mrjulsen.crn.data.GlobalSettingsManager; import de.mrjulsen.crn.network.packets.cts.AdvancedDisplayUpdatePacket; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; @@ -32,7 +32,9 @@ import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Cache; import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.components.Widget; @@ -40,6 +42,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; @@ -59,7 +62,7 @@ public class AdvancedDisplaySettingsScreen extends DLScreen { // Settings private final Level level; private final BlockPos pos; - private EDisplayInfo info; + private DisplayTypeResourceKey typeKey; private EDisplayType type; private final boolean canBeDoubleSided; private boolean doubleSided; @@ -72,14 +75,14 @@ public class AdvancedDisplaySettingsScreen extends DLScreen { private DLCreateIconButton globalSettingsButton; private final MutableComponent tooltipGlobalSettings = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.global_settings.tooltip"); private final MutableComponent tooltipDisplayType = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.display_type"); - private final MutableComponent tooltipDisplayTypeDescription = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.display_type.description"); private final MutableComponent tooltipInfoType = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.info_type"); - private final MutableComponent tooltipInfoTypeDescription = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.info_type.description"); private final MutableComponent textDoubleSided = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.double_sided"); private int guiLeft, guiTop; private DLCreateIconButton backButton; + + private final Cache> displayTypes = new Cache<>(() -> AdvancedDisplaysRegistry.getAllOfTypeAsKey(type)); @SuppressWarnings("resource") public AdvancedDisplaySettingsScreen(AdvancedDisplayBlockEntity blockEntity) { @@ -87,8 +90,8 @@ public AdvancedDisplaySettingsScreen(AdvancedDisplayBlockEntity blockEntity) { this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); this.pos = blockEntity.getBlockPos(); this.level = blockEntity.getLevel(); - this.info = blockEntity.getInfoType(); - this.type = blockEntity.getDisplayType(); + this.type = blockEntity.getDisplayTypeKey().category(); + this.typeKey = blockEntity.getDisplayTypeKey(); this.renderedItem = new ItemStack(blockEntity.getBlockState().getBlock()); this.canBeDoubleSided = blockEntity.getBlockState().getBlock() instanceof AbstractAdvancedSidedDisplayBlock; this.doubleSided = !canBeDoubleSided || blockEntity.getBlockState().getValue(AbstractAdvancedSidedDisplayBlock.SIDE) == ESide.BOTH; @@ -96,7 +99,7 @@ public AdvancedDisplaySettingsScreen(AdvancedDisplayBlockEntity blockEntity) { @Override public void onClose() { - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new AdvancedDisplayUpdatePacket(level, pos, type, info, doubleSided)); + CreateRailwaysNavigator.net().CHANNEL.sendToServer(new AdvancedDisplayUpdatePacket(level, pos, typeKey, doubleSided)); super.onClose(); } @@ -110,6 +113,15 @@ protected void init() { backButton.withCallback(() -> { onClose(); }); + + DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 179 - DEFAULT_ICON_BUTTON_WIDTH - 10, guiTop + 99, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + Util.getPlatform().openUri(Constants.HELP_PAGE_ADVANCED_DISPLAYS); + } + }); + addTooltip(DLTooltip.of(Constants.TEXT_HELP).assignedTo(helpButton)); displayTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 23 + 5, Components.immutableEmpty()).withShadow()); displayTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 23, 138, 18) @@ -118,23 +130,16 @@ protected void init() { .writingTo(displayTypeLabel) .calling((i) -> { type = EDisplayType.getTypeById(i); + displayTypes.clear(); + createDisplayBrowser(); + displayTypeInput.addHint(displayTypeHint()); }) - .addHint(tooltipDisplayTypeDescription) + .addHint(displayTypeHint()) .setState(type.getId())); displayTypeInput.onChanged(); infoTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 45 + 5, Components.immutableEmpty()).withShadow()); - infoTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 45, 138, 18) - .forOptions(Arrays.stream(EDisplayInfo.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) - .titled(tooltipInfoType) - .writingTo(infoTypeLabel) - .calling((i) -> { - info = EDisplayInfo.getTypeById(i); - }) - .addHint(tooltipInfoTypeDescription) - .setState(info.getId())); - infoTypeInput.onChanged(); - + createDisplayBrowser(); addRenderableWidget(new DLCheckBox(guiLeft + 45, guiTop + 67 + 1, 138, textDoubleSided.getString(), doubleSided, (box) -> { this.doubleSided = box.isChecked(); @@ -147,18 +152,37 @@ protected void init() { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - minecraft.setScreen(new LoadingScreen()); - GlobalSettingsManager.syncToClient(() -> { - ClientTrainStationSnapshot.syncToClient(() -> { - minecraft.setScreen(new GlobalSettingsScreen(level, instance)); - }); - }); + DLScreen.setScreen(new GlobalSettingsScreen(instance)); } }); addTooltip(DLTooltip.of(tooltipGlobalSettings).assignedTo(globalSettingsButton)); } } + private void createDisplayBrowser() { + if (infoTypeInput != null) { + removeWidget(infoTypeInput); + } + infoTypeInput = new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 45, 138, 18) + .forOptions(displayTypes.get().stream().map(x -> TextUtils.translate(x.getTranslationKey())).toList()) + .titled(tooltipInfoType) + .writingTo(infoTypeLabel) + .calling((i) -> { + typeKey = displayTypes.get().get(i); + }) + .setState(displayTypes.get().indexOf(typeKey)); + infoTypeInput.onChanged(); + addRenderableWidget(infoTypeInput); + } + + private MutableComponent displayTypeHint() { + StringBuilder sb = new StringBuilder(); + font.getSplitter().splitLines(TextUtils.translate(typeKey.category().getValueInfoTranslationKey(CreateRailwaysNavigator.MOD_ID)), width() / 3, Style.EMPTY).forEach(x -> { + sb.append("\n" + x.getString()); + }); + return TextUtils.text(sb.toString()); + } + @Override public boolean isPauseScreen() { return false; @@ -183,7 +207,7 @@ public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float p .render(graphics.poseStack()); type.getIcon().render(graphics, guiLeft + 22, guiTop + 24); - info.getIcon().render(graphics, guiLeft + 22, guiTop + 46); + ModGuiIcons.VERY_DETAILED.render(graphics, guiLeft + 22, guiTop + 46); ModGuiIcons.DOUBLE_SIDED.render(graphics, guiLeft + 22, guiTop + 68); super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AliasSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AliasSettingsScreen.java deleted file mode 100644 index 47c6b56f..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AliasSettingsScreen.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.Collection; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.widgets.AliasEntryWidget; -import de.mrjulsen.crn.data.AliasName; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.world.level.Level; - -public class AliasSettingsScreen extends AbstractEntryListSettingsScreen { - public AliasSettingsScreen(Level level, Screen lastScreen) { - super(level, lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.title")); - } - - @Override - protected AliasEntryWidget createWidget(WidgetCreationData> widgetData, TrainStationAlias data) { - Collection expandedAliasNames = widgetData.previousEntries().stream().filter(x -> x instanceof AliasEntryWidget && ((AliasEntryWidget)x).isExpanded()).map(x -> ((AliasEntryWidget)x).getAlias().getAliasName().get()).toList(); - return new AliasEntryWidget(widgetData.parent(), widgetData.x(), widgetData.y(), data, () -> refreshEntries(), expandedAliasNames.contains(data.getAliasName().get())); - } - - @Override - protected TrainStationAlias[] getData(String searchText) { - return GlobalSettingsManager.getInstance().getSettingsData().getAliasList().stream().filter(x -> x.getAliasName().get().toLowerCase().contains(searchText.toLowerCase())).toArray(TrainStationAlias[]::new); - } - - @Override - protected void onCreateNewEntry(String value, Runnable refreshAction) { - GlobalSettingsManager.getInstance().getSettingsData().registerAlias(new TrainStationAlias(AliasName.of(value)), refreshAction); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java index 5c021f05..092a0b0d 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java @@ -1,208 +1,438 @@ package de.mrjulsen.crn.client.gui.screen; -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; -import com.simibubi.create.foundation.gui.AllIcons; -import com.simibubi.create.foundation.gui.widget.IconButton; -import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; -import de.mrjulsen.crn.client.gui.widgets.SettingsOptionWidget; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.crn.client.gui.widgets.ModStationSuggestions; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutColorPicker; +import de.mrjulsen.crn.client.gui.widgets.options.DLOptionsList; +import de.mrjulsen.crn.client.gui.widgets.options.DataListContainer; +import de.mrjulsen.crn.client.gui.widgets.options.OptionEntry; +import de.mrjulsen.crn.client.gui.widgets.options.SimpleDataListNewEntry; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLVerticalScrollBar; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.client.util.WidgetsCollection; -import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.MathUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.Util; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; -import net.minecraft.world.level.Level; - -public class GlobalSettingsScreen extends DLScreen { - - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings.png"); - private static final int GUI_WIDTH = 255; - private static final int GUI_HEIGHT = 247; +public class GlobalSettingsScreen extends AbstractNavigatorScreen { + private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; - private static final int ENTRIES_START_Y_OFFSET = 10; - private final int ENTRY_SPACING = 4; + private DLOptionsList viewer; + private ModStationSuggestions destinationSuggestions; - private final int AREA_X = 16; - private final int AREA_Y = 16; - private final int AREA_W = 220; - private final int AREA_H = 194; - private GuiAreaDefinition workingArea; - - private int guiLeft, guiTop; - private LerpedFloat scroll = LerpedFloat.linear().startWithValue(0); - - // Data - private final Level level; - private final Font shadowlessFont; - private final Screen lastScreen; - private final GlobalSettingsScreen instance; - - // Controls - private IconButton backButton; - private final WidgetsCollection optionsCollection = new WidgetsCollection(); - - // Tooltips - private final Component optionAliasTitle = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option_alias.title"); - private final Component optionAliasDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option_alias.description"); + private final Component optionTagTitle = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option_alias.title"); + private final Component optionTagDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option_alias.description"); private final Component optionBlacklistTitle = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option_blacklist.title"); private final Component optionBlacklistDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option_blacklist.description"); private final Component optionTrainGroupTitle = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_group.title"); private final Component optionTrainGroupDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_group.description"); private final Component optionTrainBlacklistTitle = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_blacklist.title"); private final Component optionTrainBlacklistDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_blacklist.description"); + private final Component optionTrainLineTitle = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_line.title"); + private final Component optionTrainLineDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_line.description"); + private final Component textAdd = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.add"); + private final Component textColor = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.train_line.color"); + + private final List stationNames = new ArrayList<>(); + private final List trainNames = new ArrayList<>(); - @SuppressWarnings("resource") - public GlobalSettingsScreen(Level level, Screen lastScreen) { - super(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.title")); - this.level = level; - this.lastScreen = lastScreen; - this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); - this.instance = this; + public GlobalSettingsScreen(Screen lastScreen) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.title"), BarColor.GRAY); } @Override - protected void init() { - super.init(); - guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; - int startY = guiTop + AREA_X + ENTRIES_START_Y_OFFSET; - workingArea = new GuiAreaDefinition(guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } - optionsCollection.components.clear(); + @Override + public void tick() { + super.tick(); - optionsCollection.components.add(new SettingsOptionWidget(this, guiLeft + 26, startY, optionAliasTitle, optionAliasDescription, (btn) -> { - minecraft.setScreen(new AliasSettingsScreen(level, instance)); - })); + DLUtils.doIfNotNull(destinationSuggestions, x -> { + x.tick(); + if (!destinationSuggestions.getEditBox().canConsumeInput()) { + clearSuggestions(); + } + }); + } - optionsCollection.components.add(new SettingsOptionWidget(this, guiLeft + 26, startY + SettingsOptionWidget.HEIGHT + ENTRY_SPACING, optionBlacklistTitle, optionBlacklistDescription, (btn) -> { - minecraft.setScreen(new StationBlacklistScreen(level, instance)); - })); + @Override + protected void init() { + super.init(); + setAllowedLayer(0); - optionsCollection.components.add(new SettingsOptionWidget(this, guiLeft + 26, startY + (SettingsOptionWidget.HEIGHT + ENTRY_SPACING) * 2, optionTrainGroupTitle, optionTrainGroupDescription, (btn) -> { - minecraft.setScreen(new TrainGroupScreen(level, instance)); - })); - - optionsCollection.components.add(new SettingsOptionWidget(this, guiLeft + 26, startY + (SettingsOptionWidget.HEIGHT + ENTRY_SPACING) * 3, optionTrainBlacklistTitle, optionTrainBlacklistDescription, (btn) -> { - minecraft.setScreen(new TrainBlacklistScreen(level, instance)); - })); - - backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIG_BACK) { + DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - DEFAULT_ICON_BUTTON_WIDTH - 8, guiTop + 223, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - onClose(); + Util.getPlatform().openUri(Constants.HELP_PAGE_GLOBAL_SETTINGS); } }); - addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_BACK).assignedTo(backButton)); + addTooltip(DLTooltip.of(Constants.TEXT_HELP).assignedTo(helpButton)); + + int dy = FooterSize.DEFAULT.size() + 1; + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, guiLeft + GUI_WIDTH - 8, guiTop + dy, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, null); + viewer = new DLOptionsList(this, guiLeft + 3, guiTop + dy, GUI_WIDTH - 6, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, scrollBar); + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + + viewer.addOption(null, optionTagTitle, optionTagDescription, (a, b) -> Minecraft.getInstance().setScreen(new StationTagSettingsScreen(this)), null); + //viewer.addOption(null, optionTrainGroupTitle, optionTrainGroupDescription, (a, b) -> Minecraft.getInstance().setScreen(new TrainGroupScreen(this)), null); + + GlobalSettingsClient.getBlacklistedStations((datalist) -> { + addBlacklistedStationsWidget(datalist.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).toList(), scrollBar); + GlobalSettingsClient.getBlacklistedTrains((datalist2) -> { + addBlacklistedTrainsWidget(datalist2.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).toList(), scrollBar); + GlobalSettingsClient.getTrainGroups((datalist3) -> { + addTrainGroupsWidget(datalist3.stream().sorted((a, b) -> a.getGroupName().compareToIgnoreCase(b.getGroupName())).toList(), scrollBar); + GlobalSettingsClient.getTrainLines((datalist4) -> { + addTrainLinesWidget(datalist4.stream().sorted((a, b) -> a.getLineName().compareToIgnoreCase(b.getLineName())).toList(), scrollBar); + }); + }); + }); + }); + + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_TRAIN_NAMES, (names) -> { + this.trainNames.clear(); + this.trainNames.addAll(names); + + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_STATION_NAMES, (names2) -> { + this.stationNames.clear(); + this.stationNames.addAll(names2); + }); + }); + + //viewer.addOption(null, optionTrainBlacklistTitle, optionTrainBlacklistDescription, (a, b) -> OptionEntry.expandOrCollapse(a), null); } - - private int getMaxScrollHeight() { - return (SettingsOptionWidget.HEIGHT + ENTRY_SPACING) * 4 + ENTRIES_START_Y_OFFSET * 2; + + private void addBlacklistedStationsWidget(List datalist, DLVerticalScrollBar scrollBar) { + OptionEntry opt = viewer.addOption((option) -> { + GuiAreaDefinition workspace = option.getContentSpace(); + DataListContainer, String> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, + (list) -> { + return list.iterator(); + }, (data, entryWidget) -> { + entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { + GlobalSettingsClient.removeStationFromBlacklist(entry, (res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + }); + return data; + }, (data, entryWidget) -> { + entryWidget.addAddButton(ModGuiIcons.ADD.getAsSprite(16, 16), textAdd, + (btn, tg, inputValues, refreshAction) -> { + String name = inputValues.get(SimpleDataListNewEntry.MAIN_INPUT_KEY).get(); + if (name == null || name.isBlank()) { + return false; + } + GlobalSettingsClient.addStationToBlacklist(name,(res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + return true; + }); + entryWidget.editNameEditBox((box) -> { + box.setResponder((b) -> { + this.updateEditorSubwidgetsStations(box, data); + }); + box.setMaxLength(StationTag.MAX_NAME_LENGTH); + }); + }, (self) -> { + option.notifyContentSizeChanged(); + } + ); + cont.setPadding(3, 0, 3, 18); + cont.setFilter((entry, searchText) -> { + return entry.toLowerCase(Locale.ROOT).contains(searchText.get().toLowerCase(Locale.ROOT)); + }); + cont.setBordered(false); + + return cont; + }, optionBlacklistTitle, optionBlacklistDescription, (a, b) -> OptionEntry.expandOrCollapse(a), null); + opt.addAdditionalButton(ModGuiIcons.HELP.getAsSprite(16, 16), Constants.TEXT_HELP, (entry, btn) -> Util.getPlatform().openUri(Constants.HELP_PAGE_STATION_BLACKLIST)); + } - @Override - public void onClose() { - minecraft.setScreen(lastScreen); + private void addBlacklistedTrainsWidget(List datalist, DLVerticalScrollBar scrollBar) { + OptionEntry opt = viewer.addOption((option) -> { + GuiAreaDefinition workspace = option.getContentSpace(); + DataListContainer, String> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, + (list) -> { + return list.iterator(); + }, (data, entryWidget) -> { + entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { + GlobalSettingsClient.removeTrainFromBlacklist(entry, (res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + }); + return data; + }, (data, entryWidget) -> { + entryWidget.addAddButton(ModGuiIcons.ADD.getAsSprite(16, 16), textAdd, + (btn, tg, inputValues, refreshAction) -> { + String name = inputValues.get(SimpleDataListNewEntry.MAIN_INPUT_KEY).get(); + if (name == null || name.isBlank()) { + return false; + } + GlobalSettingsClient.addTrainToBlacklist(name, (res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + return true; + }); + entryWidget.editNameEditBox((box) -> { + box.setResponder((b) -> { + this.updateEditorSubwidgetsTrains(box, data); + }); + box.setMaxLength(StationTag.MAX_NAME_LENGTH); + }); + }, (self) -> { + option.notifyContentSizeChanged(); + } + ); + cont.setPadding(3, 0, 3, 18); + cont.setFilter((entry, searchText) -> { + return entry.toLowerCase(Locale.ROOT).contains(searchText.get().toLowerCase(Locale.ROOT)); + }); + cont.setBordered(false); + + return cont; + }, optionTrainBlacklistTitle, optionTrainBlacklistDescription, (a, b) -> OptionEntry.expandOrCollapse(a), null); + opt.addAdditionalButton(ModGuiIcons.HELP.getAsSprite(16, 16), Constants.TEXT_HELP, (entry, btn) -> Util.getPlatform().openUri(Constants.HELP_PAGE_TRAIN_BLACKLIST)); + } - @Override - public void tick() { - super.tick(); - - scroll.tickChaser(); + private void addTrainGroupsWidget(List datalist, DLVerticalScrollBar scrollBar) { + OptionEntry opt = viewer.addOption((option) -> { + GuiAreaDefinition workspace = option.getContentSpace(); + DataListContainer, TrainGroup> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, + (list) -> { + return list.iterator(); + }, (data, entryWidget) -> { + entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { + GlobalSettingsClient.deleteTrainGroup(entry.getGroupName(), () -> { + GlobalSettingsClient.getTrainGroups((res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + }); + }); + DLIconButton colorBtn = entryWidget.addButton(ModGuiIcons.COLOR_PALETTE.getAsSprite(16, 16), textColor, + (btn, tg, entry, refreshAction) -> { + final TrainGroup e = entry; + FlyoutColorPicker flyout = new FlyoutColorPicker<>(this, e.getColor(), this::addRenderableWidget, (w) -> { + GlobalSettingsClient.updateTrainGroupColor(e.getGroupName(), ((FlyoutColorPicker)w).getColorPicker().getSelectedColor(), () -> { + GlobalSettingsClient.getTrainGroups((res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + }); + removeWidget(w); + }); + flyout.setYOffset((int)-scrollBar.getScrollValue()); + flyout.open(btn); + }); + colorBtn.setBackColor(data.getColor()); + return data.getGroupName(); + }, (data, entryWidget) -> { + entryWidget.addAddButton(ModGuiIcons.ADD.getAsSprite(16, 16), textAdd, + (btn, tg, inputValues, refreshAction) -> { + String name = inputValues.get(SimpleDataListNewEntry.MAIN_INPUT_KEY).get(); + if (name == null || name.isBlank()) { + return false; + } + GlobalSettingsClient.createTrainGroup(name, (res) -> { + GlobalSettingsClient.getTrainGroups((r) -> { + refreshAction.accept(Optional.ofNullable(r)); + }); + }); + return true; + }); + entryWidget.editNameEditBox((box) -> { + box.setResponder((b) -> { + //this.updateEditorSubwidgetsTrains(box, data); + }); + box.setMaxLength(StationTag.MAX_NAME_LENGTH); + }); + }, (self) -> { + option.notifyContentSizeChanged(); + } + ); + cont.setPadding(3, 0, 3, 18); + cont.setFilter((entry, searchText) -> { + return entry.getGroupName().toLowerCase(Locale.ROOT).contains(searchText.get().toLowerCase(Locale.ROOT)); + }); + cont.setBordered(false); + return cont; + }, optionTrainGroupTitle, optionTrainGroupDescription, (a, b) -> OptionEntry.expandOrCollapse(a), null); + opt.addAdditionalButton(ModGuiIcons.HELP.getAsSprite(16, 16), Constants.TEXT_HELP, (entry, btn) -> Util.getPlatform().openUri(Constants.HELP_PAGE_TRAIN_GROUPS)); + } - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - final float partialTicks = Minecraft.getInstance().getFrameTime(); - float scrollOffset = -scroll.getValue(partialTicks); - - renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 19, guiTop + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + GUI_WIDTH - 22 - shadowlessFont.width(timeString), guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.LEFT, false); - - // Scrollbar - double maxHeight = getMaxScrollHeight(); - double aH = AREA_H + 1; - if (aH / maxHeight < 1) { - int scrollerHeight = Math.max(10, (int)(aH * (aH / maxHeight))); - int startY = guiTop + AREA_Y + (int)((AREA_H) * (Math.abs(scrollOffset) / maxHeight)); - - GuiUtils.fill(graphics, guiLeft + AREA_X + AREA_W - 3, startY, 3, scrollerHeight, 0x7FFFFFFF); - } + private void addTrainLinesWidget(List datalist, DLVerticalScrollBar scrollBar) { + OptionEntry opt = viewer.addOption((option) -> { + GuiAreaDefinition workspace = option.getContentSpace(); + DataListContainer, TrainLine> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, + (list) -> { + return list.iterator(); + }, (data, entryWidget) -> { + entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { + GlobalSettingsClient.deleteTrainLine(entry.getLineName(), () -> { + GlobalSettingsClient.getTrainLines((res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + }); + }); + DLIconButton colorBtn = entryWidget.addButton(ModGuiIcons.COLOR_PALETTE.getAsSprite(16, 16), textColor, + (btn, tg, entry, refreshAction) -> { + final TrainLine e = entry; + FlyoutColorPicker flyout = new FlyoutColorPicker<>(this, e.getColor(), this::addRenderableWidget, (w) -> { + GlobalSettingsClient.updateTrainLineColor(e.getLineName(), ((FlyoutColorPicker)w).getColorPicker().getSelectedColor(), () -> { + GlobalSettingsClient.getTrainLines((res) -> { + refreshAction.accept(Optional.ofNullable(res)); + }); + }); + removeWidget(w); + }); + flyout.setYOffset((int)-scrollBar.getScrollValue()); + flyout.open(btn); + }); + colorBtn.setBackColor(data.getColor()); + return data.getLineName(); + }, (data, entryWidget) -> { + entryWidget.addAddButton(ModGuiIcons.ADD.getAsSprite(16, 16), textAdd, + (btn, tg, inputValues, refreshAction) -> { + String name = inputValues.get(SimpleDataListNewEntry.MAIN_INPUT_KEY).get(); + if (name == null || name.isBlank()) { + return false; + } + GlobalSettingsClient.createTrainLine(name, (res) -> { + GlobalSettingsClient.getTrainLines((r) -> { + refreshAction.accept(Optional.ofNullable(r)); + }); + }); + return true; + }); + entryWidget.editNameEditBox((box) -> { + box.setResponder((b) -> { + //this.updateEditorSubwidgetsTrains(box, data); + }); + box.setMaxLength(StationTag.MAX_NAME_LENGTH); + }); + }, (self) -> { + option.notifyContentSizeChanged(); + } + ); + cont.setPadding(3, 0, 3, 18); + cont.setFilter((entry, searchText) -> { + return entry.getLineName().toLowerCase(Locale.ROOT).contains(searchText.get().toLowerCase(Locale.ROOT)); + }); + cont.setBordered(false); + return cont; + }, optionTrainLineTitle, optionTrainLineDescription, (a, b) -> OptionEntry.expandOrCollapse(a), null); + opt.addAdditionalButton(ModGuiIcons.HELP.getAsSprite(16, 16), Constants.TEXT_HELP, (entry, btn) -> Util.getPlatform().openUri(Constants.HELP_PAGE_TRAIN_LINES)); + } - // CONTENT - GuiUtils.enableScissor(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - graphics.poseStack().translate(0, scrollOffset, 0); + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); - optionsCollection.performForEachOfType(SettingsOptionWidget.class, x -> x.renderMainLayer(graphics, pMouseX, (int)(pMouseY - scrollOffset), partialTicks/*, workingArea.isInBounds(pMouseX, pMouseY)*/)); - - GuiUtils.disableScissor(graphics); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y, 200, AREA_W, 10, 0x77000000, 0x00000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y + AREA_H - 10, 200, AREA_W, 10, 0x00000000, 0x77000000); + int y = FooterSize.DEFAULT.size() - 1; + int h = GUI_HEIGHT - y - FooterSize.SMALL.size(); + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, h + 1, ContainerColor.PURPLE); - super.renderMainLayer(graphics, pMouseX, pMouseY, partialTicks); + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); } @Override - public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTicks) { - float scrollOffset = -scroll.getValue(pPartialTicks); - optionsCollection.performForEachOfType(SettingsOptionWidget.class, x -> workingArea.isInBounds(pMouseX, pMouseY), x -> x.renderFrontLayer(graphics, pMouseX, pMouseY, -scrollOffset)); - super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTicks); + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTick); + DLUtils.doIfNotNull(destinationSuggestions, x -> { + graphics.poseStack().pushPose(); + graphics.poseStack().translate(-viewer.getXScrollOffset(), -viewer.getYScrollOffset(), 0); + x.render(graphics.poseStack(), (int)(mouseX + viewer.getXScrollOffset()), (int)(mouseY + viewer.getYScrollOffset())); + graphics.poseStack().popPose(); + }); } - + @Override - public void mouseSelectEvent(int mouseX, int mouseY) { - - super.mouseSelectEvent(mouseX, mouseY); + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (destinationSuggestions != null && destinationSuggestions.mouseScrolled(mouseX + viewer.getXScrollOffset(), mouseY + viewer.getYScrollOffset(), MathUtils.clamp(delta, -1.0D, 1.0D))) + return true; + + return super.mouseScrolled(mouseX, mouseY, delta); } @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - float scrollOffset = -scroll.getValue(); - - optionsCollection.performForEachOfType(SettingsOptionWidget.class, - x -> workingArea.isInBounds(pMouseX, pMouseY) && x.isMouseOver(pMouseX, (int)(pMouseY - scrollOffset)), - x -> x.mouseClicked(pMouseX, (int)(pMouseY - scrollOffset), pButton) - ); - return super.mouseClicked(pMouseX, pMouseY, pButton); + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (destinationSuggestions != null && destinationSuggestions.mouseClicked(mouseX + viewer.getXScrollOffset(), mouseY + viewer.getYScrollOffset(), button)) + return true; + + return super.mouseClicked(mouseX, mouseY, button); + } + + private void clearSuggestions() { + if (destinationSuggestions != null) { + destinationSuggestions.getEditBox().setSuggestion(""); + } + destinationSuggestions = null; } - @Override - public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { - float chaseTarget = scroll.getChaseTarget(); - float max = -AREA_H + getMaxScrollHeight(); + public void updateEditorSubwidgetsTrains(DLEditBox field, Collection blacklisted) { + updateEditorSubwidgetsInternal(field, getViableTrains(trainNames, blacklisted)); + } - if (max > 0) { - chaseTarget -= pDelta * 12; - chaseTarget = Mth.clamp(chaseTarget, 0, max); - scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP); - } else - scroll.chase(0, 0.7f, Chaser.EXP); + public void updateEditorSubwidgetsStations(DLEditBox field, Collection blacklisted) { + updateEditorSubwidgetsInternal(field, getViableStations(stationNames, blacklisted)); + } - return super.mouseScrolled(pMouseX, pMouseY, pDelta); + private void updateEditorSubwidgetsInternal(DLEditBox field, List data) { + clearSuggestions(); + destinationSuggestions = new ModStationSuggestions(Minecraft.getInstance(), this, field, font, data, field.getHeight() + 2 + field.y); + destinationSuggestions.setAllowSuggestions(true); + destinationSuggestions.updateCommandInfo(); } + + private List getViableStations(Collection src, Collection blacklisted) { + return src.stream() + .distinct() + .filter(x -> !blacklisted.contains(x)) + .sorted((a, b) -> a.compareTo(b)) + .toList(); + } + + private List getViableTrains(Collection src, Collection blacklisted) { + return src.stream() + .distinct() + .filter(x -> !blacklisted.contains(x)) + .sorted((a, b) -> a.compareTo(b)) + .toList(); + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/LoadingScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/LoadingScreen.java deleted file mode 100644 index d03b0065..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/LoadingScreen.java +++ /dev/null @@ -1,45 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import com.simibubi.create.foundation.gui.AllIcons; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.TextUtils; - -public class LoadingScreen extends DLScreen { - - int angle = 0; - - public LoadingScreen() { - super(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".loading.title")); - } - - @Override - protected void init() { - super.init(); - } - - @Override - public void tick() { - angle += 6; - if (angle > 360) { - angle = 0; - } - super.tick(); - } - - @Override - public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { - super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); - - renderScreenBackground(graphics); - - double offsetX = Math.sin(Math.toRadians(angle)) * 5; - double offsetY = Math.cos(Math.toRadians(angle)) * 5; - - GuiUtils.drawString(graphics, font, width / 2, height / 2, title, 0xFFFFFF, EAlignment.CENTER, true); - AllIcons.I_MTD_SCAN.render(graphics.poseStack(), (int)(width / 2 + offsetX), (int)(height / 2 - 50 + offsetY)); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java index d571c410..d4d62821 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java @@ -1,101 +1,86 @@ package de.mrjulsen.crn.client.gui.screen; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.UUID; - -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL30; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; +import com.google.common.collect.ImmutableList; import com.simibubi.create.foundation.gui.AllIcons; import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; - -import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.CRNGui; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.widgets.AbstractNotificationPopup; import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; import de.mrjulsen.crn.client.gui.widgets.ModDestinationSuggestions; -import de.mrjulsen.crn.client.gui.widgets.RouteEntryOverviewWidget; -import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.RouteViewer; +import de.mrjulsen.crn.client.gui.widgets.SearchOptionButton; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutAdvancedSearchsettingsWidget; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutDepartureInWidget; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutTrainGroupsWidget; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutTransferTimeWidget; +import de.mrjulsen.crn.client.gui.widgets.notifications.NotificationTrainInitialization; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget.FlyoutPointer; import de.mrjulsen.crn.config.ModCommonConfig; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.crn.event.listeners.IJourneyListenerClient; -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.NavigationRequestPacket; -import de.mrjulsen.crn.network.packets.cts.NearestStationRequestPacket; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.crn.registry.ModAccessorTypes.NavigationData; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.GuiIcons; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.client.util.WidgetsCollection; import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.client.gui.components.toasts.SystemToast.SystemToastIds; -import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.sounds.SoundEvents; import net.minecraft.util.Mth; -import net.minecraft.world.level.Level; -public class NavigatorScreen extends DLScreen implements IJourneyListenerClient { - - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/navigator.png"); - private static final int GUI_WIDTH = 255; - private static final int GUI_HEIGHT = 247; +public class NavigatorScreen extends AbstractNavigatorScreen { private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; - private static final int ENTRIES_START_Y_OFFSET = 10; - private static final int ENTRY_SPACING = 4; - - private final int AREA_X = 16; - private final int AREA_Y = 67; - private final int AREA_W = 220; - private final int AREA_H = 143; - - private int guiLeft, guiTop; private boolean initialized = false; private int angle = 0; // Controls private DLCreateIconButton locationButton; private DLCreateIconButton searchButton; - private DLCreateIconButton goToTopButton; - private DLCreateIconButton globalSettingsButton; - private DLCreateIconButton searchSettingsButton; + private DLCreateIconButton globalSettingsButton; private DLEditBox fromBox; private DLEditBox toBox; + private RouteViewer routeViewer; private LerpedFloat scroll = LerpedFloat.linear().startWithValue(0); - private ModDestinationSuggestions destinationSuggestions; - private GuiAreaDefinition switchButtonsArea; - private final WidgetsCollection routesCollection = new WidgetsCollection(); + private ModDestinationSuggestions destinationSuggestions; + private AbstractNotificationPopup notificationPopup; + + @SuppressWarnings("resource") + private UserSettings userSettings = new UserSettings(Minecraft.getInstance().player.getUUID(), false); // Data - private SimpleRoute[] routes; + private final Collection routes = new ArrayList<>(); + private final List stationNames = new ArrayList<>(); private String stationFrom = ""; private String stationTo = ""; - private long lastRefreshedTime; private final NavigatorScreen instance; - private final Level level; - private final Font shadowlessFont; - private final UUID clientId = UUID.randomUUID(); // var private boolean isLoadingRoutes = false; @@ -113,47 +98,20 @@ public class NavigatorScreen extends DLScreen implements IJourneyListenerClient private final MutableComponent tooltipLocation = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.location.tooltip"); private final MutableComponent tooltipSwitch = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.switch.tooltip"); private final MutableComponent tooltipGlobalSettings = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.global_settings.tooltip"); - private final MutableComponent tooltipSearchSettings = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.search_settings.tooltip"); + //private final MutableComponent tooltipUserProfile = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.my_profile"); + private final MutableComponent tooltipSavedRoutes = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.title"); + private final MutableComponent tooltipScheduleViewer = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.title"); - @SuppressWarnings("resource") - public NavigatorScreen(Level level) { - super(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.title")); + public NavigatorScreen(Screen lastScreen) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.title"), BarColor.GRAY); this.instance = this; - this.level = level; - this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); - } - - @Override - public UUID getJourneyListenerClientId() { - return clientId; } private void generateRouteEntries() { - generatingRouteEntries = true; - routesCollection.components.clear(); - - if (routes != null && routes.length > 0) { - for (int i = 0; i < routes.length; i++) { - SimpleRoute route = routes[i]; - AbstractWidget w = new RouteEntryOverviewWidget(instance, level, lastRefreshedTime, guiLeft + 26, guiTop + 67 + ENTRIES_START_Y_OFFSET + (i * (RouteEntryOverviewWidget.HEIGHT + ENTRY_SPACING)), route, (btn) -> {}); - routesCollection.components.add(w); - } - } - generatingRouteEntries = false; + } - private void clearSuggestions() { - if (destinationSuggestions != null) { - destinationSuggestions.getEditBox().setSuggestion(""); - } - destinationSuggestions = null; - } - - private void setLastRefreshedTime() { - lastRefreshedTime = level.getDayTime(); - } - @Override public boolean isPauseScreen() { return false; @@ -161,12 +119,21 @@ public boolean isPauseScreen() { @Override public void onClose() { - JourneyListenerManager.getInstance().removeClientListenerForAll(this); super.onClose(); + clearRoutes(); + } + + @Override + public void removed() { + super.removed(); + } + + public synchronized void clearRoutes() { + routes.stream().forEach(x -> x.close()); + routes.clear(); } private void switchButtonClick() { - minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); String fromInput = fromBox.getValue(); String toInput = toBox.getValue(); @@ -174,254 +141,251 @@ private void switchButtonClick() { toBox.setValue(fromInput); } + @SuppressWarnings("resource") @Override protected void init() { super.init(); + setAllowedLayer(0); initialized = false; - guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; - switchButtonsArea = new GuiAreaDefinition(guiLeft + 190, guiTop + 34, 11, 12); - addTooltip(DLTooltip.of(tooltipSwitch).assignedTo(switchButtonsArea)); - - locationButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 208, guiTop + 20, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.POSITION.getAsCreateIcon()) { - @Override - public void onClick(double mouseX, double mouseY) { - super.onClick(mouseX, mouseY); - long id = InstanceManager.registerClientNearestStationResponseAction((result) -> { - if (result.aliasName.isPresent()) { - fromBox.setValue(result.aliasName.get().getAliasName().get()); - } - }); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new NearestStationRequestPacket(id, minecraft.player.position())); - } + DataAccessor.getFromServer(true, ModAccessorTypes.GET_ALL_STATIONS_AS_TAGS, (names) -> { + this.stationNames.clear(); + this.stationNames.addAll(names); }); - addTooltip(DLTooltip.of(tooltipLocation).assignedTo(locationButton)); - searchButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 208, guiTop + 42, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_MTD_SCAN) { + locationButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 195, guiTop + 20, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.POSITION.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - - if (stationFrom == null || stationTo == null || stationFrom.isBlank() || stationTo.isBlank()) { - Minecraft.getInstance().getToasts().addToast(new SystemToast(SystemToastIds.PERIODIC_NOTIFICATION, errorTitle, startEndNullText)); - return; - } - - if (stationFrom.equals(stationTo)) { - Minecraft.getInstance().getToasts().addToast(new SystemToast(SystemToastIds.PERIODIC_NOTIFICATION, errorTitle, startEndEqualText)); - return; - } - - isLoadingRoutes = true; - - long id = InstanceManager.registerClientNavigationResponseAction((routes, data) -> { - JourneyListenerManager.getInstance().removeClientListenerForAll(instance); - - instance.routes = routes.toArray(SimpleRoute[]::new); - setLastRefreshedTime(); - generateRouteEntries(); - isLoadingRoutes = false; - - for (SimpleRoute route : instance.routes) { - UUID listenerId = route.listen(instance); - JourneyListenerManager.getInstance().get(listenerId, instance).start(); + DataAccessor.getFromServer(minecraft.player.blockPosition(), ModAccessorTypes.GET_NEAREST_STATION, (result) -> { + if (result.tagName.isPresent()) { + fromBox.setValue(result.tagName.get().get()); } }); - scroll.chase(0, 0.7f, Chaser.EXP); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new NavigationRequestPacket(id, stationFrom, stationTo)); - } }); - addTooltip(DLTooltip.of(tooltipSearch).assignedTo(searchButton)); + addTooltip(DLTooltip.of(tooltipLocation).assignedTo(locationButton)); - fromBox = addEditBox(guiLeft + 50, guiTop + 25, 157, 12, stationFrom, TextUtils.empty(), false, (v) -> { + fromBox = addEditBox(guiLeft + 32 + 5, guiTop + 25, 157, 12, stationFrom, TextUtils.empty(), false, (v) -> { if (!initialized) { return; } stationFrom = v; updateEditorSubwidgets(fromBox); }, NO_EDIT_BOX_FOCUS_CHANGE_ACTION, null); - fromBox.setMaxLength(25); - fromBox.setTextColor(0xFFFFFF); + fromBox.setMaxLength(StationTag.MAX_NAME_LENGTH); - toBox = addEditBox(guiLeft + 50, guiTop + 47, 157, 12, stationTo, TextUtils.empty(), false, (v) -> { + toBox = addEditBox(guiLeft + 32 + 5, guiTop + 47, 157, 12, stationTo, TextUtils.empty(), false, (v) -> { if (!initialized) { return; } stationTo = v; updateEditorSubwidgets(toBox); }, NO_EDIT_BOX_FOCUS_CHANGE_ACTION, null); - toBox.setMaxLength(25); - toBox.setTextColor(0xFFFFFF); + toBox.setMaxLength(StationTag.MAX_NAME_LENGTH); - goToTopButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 10, guiTop + AREA_Y, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_PRIORITY_VERY_HIGH) { - @Override - public void onClick(double mouseX, double mouseY) { - super.onClick(mouseX, mouseY); - scroll.chase(0, 0.7f, Chaser.EXP); - } - }); - addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_TO_TOP).assignedTo(goToTopButton)); // Global Options Button if (minecraft.player.hasPermissions(ModCommonConfig.GLOBAL_SETTINGS_PERMISSION_LEVEL.get())) { - globalSettingsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 43, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.SETTINGS.getAsCreateIcon()) { + globalSettingsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 30, guiTop + 223, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.SETTINGS.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - minecraft.setScreen(new GlobalSettingsScreen(level, instance)); + minecraft.setScreen(new GlobalSettingsScreen(instance)); } }); addTooltip(DLTooltip.of(tooltipGlobalSettings).assignedTo(globalSettingsButton)); } - searchSettingsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.FILTER.getAsCreateIcon()) { + /* + DLCreateIconButton userProfileBtn = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - DEFAULT_ICON_BUTTON_WIDTH - 8, guiTop + 223, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.USER.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + } + }); + addTooltip(DLTooltip.of(tooltipUserProfile).assignedTo(userProfileBtn)); + */ + + DLCreateIconButton savedRoutes = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - DEFAULT_ICON_BUTTON_WIDTH - 8, guiTop + 223, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.MAP_PATH.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + minecraft.setScreen(new SavedRoutesScreen(instance)); + } + }); + addTooltip(DLTooltip.of(tooltipSavedRoutes).assignedTo(savedRoutes)); + + DLCreateIconButton scheduleBoardBtn = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - DEFAULT_ICON_BUTTON_WIDTH - 30, guiTop + 223, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.VERY_DETAILED.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - minecraft.setScreen(new SearchSettingsScreen(level, instance)); + minecraft.setScreen(new ScheduleBoardScreen(instance, null)); } }); - addTooltip(DLTooltip.of(tooltipSearchSettings).assignedTo(searchSettingsButton)); + addTooltip(DLTooltip.of(tooltipScheduleViewer).assignedTo(scheduleBoardBtn)); + - this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 42, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_MTD_CLOSE) { + DLIconButton btn = addRenderableWidget((new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, new Sprite(CRNGui.GUI, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT, 55, 0, 9, 12), guiLeft + 176, guiTop + 33, 13, 14, TextUtils.empty(), (b) -> switchButtonClick()))); + addTooltip(DLTooltip.of(tooltipSwitch).assignedTo(btn)); + btn.setBackColor(0x00000000); + + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, guiLeft + GUI_WIDTH - 8, guiTop + 88, 128, GuiAreaDefinition.empty()); + routeViewer = addRenderableWidget(new RouteViewer(this, guiLeft + 3, guiTop + 88, GUI_WIDTH - 6, 128, scrollBar)); + addRenderableWidget(scrollBar); + DLUtils.doIfNotNull(routeViewer, x -> x.displayRoutes(ImmutableList.copyOf(routes))); + + searchButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 195, guiTop + 42, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_MTD_SCAN) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - onClose(); + + if (stationFrom == null || stationTo == null || stationFrom.isBlank() || stationTo.isBlank()) { + Minecraft.getInstance().getToasts().addToast(new SystemToast(SystemToastIds.PERIODIC_NOTIFICATION, errorTitle, startEndNullText)); + return; + } + + if (stationFrom.equals(stationTo)) { + Minecraft.getInstance().getToasts().addToast(new SystemToast(SystemToastIds.PERIODIC_NOTIFICATION, errorTitle, startEndEqualText)); + return; + } + + isLoadingRoutes = true; + clearRoutes(); + routeViewer.clear(); + + DataAccessor.getFromServer(new NavigationData(stationFrom, stationTo, Minecraft.getInstance().player.getUUID()), ModAccessorTypes.NAVIGATE, (routeList) -> { + routes.addAll(routeList); + routeViewer.displayRoutes(ImmutableList.copyOf(routes)); + routeViewer.displayRoutes(routeList); + isLoadingRoutes = false; + + DataAccessor.getFromServer(null, ModAccessorTypes.ALL_TRAINS_INITIALIZED, (result) -> { + DLUtils.doIfNotNull(notificationPopup, x -> x.close()); + if (!result) notificationPopup = addRenderableWidget(new NotificationTrainInitialization(instance, guiLeft + 10, guiTop + GUI_HEIGHT - FooterSize.DEFAULT.size() - 20, GUI_WIDTH - 20, instance::removeWidget)); + }); + }); } }); + addTooltip(DLTooltip.of(tooltipSearch).assignedTo(searchButton)); + + // Search Options + final int btnCount = 3; + int btnWidth = (GUI_WIDTH - 6 - 16) / btnCount; + addRenderableWidget(new SearchOptionButton(guiLeft + 3, guiTop + 54 + FooterSize.DEFAULT.size() - 2, btnWidth, 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.departure_in"), () -> userSettings.navigationDepartureInTicks.toString(), (b) -> { + new FlyoutDepartureInWidget<>(this, FlyoutPointer.UP, ColorShade.DARK, this::addRenderableWidget, userSettings, () -> { + return userSettings.navigationDepartureInTicks; + }, (w) -> { + removeWidget(w); + reloadUserSettings(); + }).open(b); + })); + addRenderableWidget(new SearchOptionButton(guiLeft + 3 + btnWidth, guiTop + 54 + FooterSize.DEFAULT.size() - 2, btnWidth, 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.transfer_time"), () -> userSettings.navigationTransferTime.toString(), (b) -> { + new FlyoutTransferTimeWidget<>(this, FlyoutPointer.UP, ColorShade.DARK, this::addRenderableWidget, userSettings, () -> { + return userSettings.navigationTransferTime; + }, (w) -> { + removeWidget(w); + reloadUserSettings(); + }).open(b); + })); + addRenderableWidget(new SearchOptionButton(guiLeft + 3 + btnWidth * 2, guiTop + 54 + FooterSize.DEFAULT.size() - 2, btnWidth, 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups"), () -> userSettings.navigationExcludedTrainGroups.toString(), (b) -> { + new FlyoutTrainGroupsWidget<>(this, FlyoutPointer.UP, ColorShade.DARK, this::addRenderableWidget, userSettings, () -> { + return userSettings.navigationExcludedTrainGroups; + }, (w) -> { + removeWidget(w); + reloadUserSettings(); + }).open(b); + })); + DLIconButton moreSearchOptionsBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, GuiIcons.ARROW_RIGHT.getAsSprite(16, 16), guiLeft + GUI_WIDTH - 3 - (GUI_WIDTH - btnWidth * 3 - 6), guiTop + 54 + FooterSize.DEFAULT.size() - 2, (GUI_WIDTH - btnWidth * 3 - 6), 18, TextUtils.empty(), + (b) -> { + new FlyoutAdvancedSearchsettingsWidget<>(this, FlyoutPointer.UP, ColorShade.DARK, this::addRenderableWidget, (w) -> { + removeWidget(w); + }).open(b); + })); + moreSearchOptionsBtn.setBackColor(0x00000000); generateRouteEntries(); + reloadUserSettings(); initialized = true; } - protected void updateEditorSubwidgets(DLEditBox field) { - clearSuggestions(); + @SuppressWarnings("resource") + private void reloadUserSettings() { + DataAccessor.getFromServer(Minecraft.getInstance().player.getUUID(), ModAccessorTypes.GET_USER_SETTINGS, settings -> this.userSettings = settings); + } + + protected void updateEditorSubwidgets(DLEditBox field) { + updateEditorSubwidgetsInternal(field, getViableStations(stationNames)); + } - destinationSuggestions = new ModDestinationSuggestions(this.minecraft, this, field, this.font, getViableStations(field), field.getHeight() + 2 + field.y); + protected void updateEditorSubwidgetsInternal(DLEditBox field, List list) { + clearSuggestions(); + destinationSuggestions = new ModDestinationSuggestions(this.minecraft, this, field, this.font, list, field.getHeight() + 2 + field.y()); destinationSuggestions.setAllowSuggestions(true); destinationSuggestions.updateCommandInfo(); } - private List getViableStations(DLEditBox field) { - return ClientTrainStationSnapshot.getInstance().getAllTrainStations().stream() - .map(x -> GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(x)) + private List getViableStations(Collection src) { + return src.stream() .distinct() - .filter(x -> !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(x)) - .sorted((a, b) -> a.getAliasName().get().compareTo(b.getAliasName().get())) + .sorted((a, b) -> a.getTagName().get().compareToIgnoreCase(b.getTagName().get())) .toList(); } + private void clearSuggestions() { + if (destinationSuggestions != null) { + destinationSuggestions.getEditBox().setSuggestion(""); + } + destinationSuggestions = null; + } + @Override public void tick() { - angle += 6; - if (angle > 360) { - angle = 0; - } scroll.tickChaser(); - if (destinationSuggestions != null) { - destinationSuggestions.tick(); + DLUtils.doIfNotNull(destinationSuggestions, x -> { + x.tick(); if (!toBox.canConsumeInput() && !fromBox.canConsumeInput()) { clearSuggestions(); } - } - - if (goToTopButton != null) { - this.goToTopButton.visible = routes != null && scroll.getValue() > 0; - } + }); - searchButton.active = !isLoadingRoutes; + DLUtils.doIfNotNull(searchButton, x -> x.set_active(!isLoadingRoutes)); super.tick(); } - protected void startStencil(PoseStack matrixStack, float x, float y, float w, float h) { - RenderSystem.clear(GL30.GL_STENCIL_BUFFER_BIT | GL30.GL_DEPTH_BUFFER_BIT, Minecraft.ON_OSX); - - GL11.glDisable(GL11.GL_STENCIL_TEST); - RenderSystem.stencilMask(~0); - RenderSystem.clear(GL11.GL_STENCIL_BUFFER_BIT, Minecraft.ON_OSX); - GL11.glEnable(GL11.GL_STENCIL_TEST); - RenderSystem.stencilOp(GL11.GL_REPLACE, GL11.GL_KEEP, GL11.GL_KEEP); - RenderSystem.stencilMask(0xFF); - RenderSystem.stencilFunc(GL11.GL_NEVER, 1, 0xFF); - - matrixStack.pushPose(); - matrixStack.translate(x, y, 0); - matrixStack.scale(w, h, 1); - io.github.fabricators_of_create.porting_lib.util.client.GuiUtils.drawGradientRect(matrixStack.last().pose(), -100, 0, 0, 1, 1, 0xff000000, 0xff000000); - matrixStack.popPose(); - - GL11.glEnable(GL11.GL_STENCIL_TEST); - RenderSystem.stencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); - RenderSystem.stencilFunc(GL11.GL_EQUAL, 1, 0xFF); - } - - protected void endStencil() { - GL11.glDisable(GL11.GL_STENCIL_TEST); - } - @Override public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { pPartialTick = minecraft.getFrameTime(); - float scrollOffset = -scroll.getValue(pPartialTick); + angle += 6 * pPartialTick; + if (angle > 360) { + angle = 0; + } + + renderNavigatorBackground(graphics, pMouseX, pMouseY, pPartialTick); - renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - - //routesCollection.performForEach(x -> x.render(graphics.poseStack(), pMouseX, pMouseY, pPartialTick)); - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); + int y = FooterSize.DEFAULT.size() - 1; + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, 52, ContainerColor.BLUE); + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y + 52 - 1, GUI_WIDTH - 2, 22, ContainerColor.GOLD); + y += 52 + 22 - 2; + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, GUI_HEIGHT - y - FooterSize.SMALL.size() + 1, ContainerColor.GRAY); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 19, guiTop + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + GUI_WIDTH - 22 - shadowlessFont.width(timeString), guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.LEFT, false); + CreateDynamicWidgets.renderTextBox(graphics, guiLeft + 32, guiTop + 20, 159); + CreateDynamicWidgets.renderTextBox(graphics, guiLeft + 32, guiTop + 42, 159); + GuiUtils.drawTexture(CRNGui.GUI, graphics, guiLeft + 16, guiTop + 16, 7, 24, 0, 30, 7, 24, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT); + GuiUtils.drawTexture(CRNGui.GUI, graphics, guiLeft + 16, guiTop + 16 + 24, 7, 24, 7, 30, 7, 24, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT); + if (!isLoadingRoutes && !generatingRouteEntries) { if (routes == null) { GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 32 + GUI_HEIGHT / 2, notSearchedText, 0xFFFFFF, EAlignment.CENTER, false); ModGuiIcons.INFO.render(graphics, (int)(guiLeft + GUI_WIDTH / 2 - 8), (int)(guiTop + GUI_HEIGHT / 2)); - } else if (routes.length <= 0) { + } else if (routes.isEmpty()) { GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 32 + GUI_HEIGHT / 2, noConnectionsText, 0xFFFFFF, EAlignment.CENTER, false); AllIcons.I_ACTIVE.render(graphics.poseStack(), (int)(guiLeft + GUI_WIDTH / 2 - 8), (int)(guiTop + GUI_HEIGHT / 2)); - } else { - //GuiUtils.swapAndBlitColor(minecraft.getMainRenderTarget(), GuiUtils.getFramebuffer()); - //GuiUtils.startStencil(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - - //UIRenderHelper.swapAndBlitColor(minecraft.getMainRenderTarget(), UIRenderHelper.framebuffer); - //startStencil(graphics.poseStack(), guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - //graphics.poseStack().pushPose(); - GuiUtils.enableScissor(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - graphics.poseStack().translate(0, scrollOffset, 0); - - int start = (int)(Math.abs(scrollOffset + ENTRIES_START_Y_OFFSET) / (ENTRY_SPACING + RouteEntryOverviewWidget.HEIGHT)); - int end = Math.min(routesCollection.components.size(), start + 2 + (int)(AREA_H / (ENTRY_SPACING + RouteEntryOverviewWidget.HEIGHT))); - for (int i = start; i < end; i++) { - routesCollection.components.get(i).render(graphics.poseStack(), (int)(pMouseX), (int)(pMouseY - scrollOffset), pPartialTick); - } - - //graphics.poseStack().popPose(); - //GuiUtils.endStencil(); - //GuiUtils.swapAndBlitColor(GuiUtils.getFramebuffer(), minecraft.getMainRenderTarget()); - //endStencil(); - //UIRenderHelper.swapAndBlitColor(UIRenderHelper.framebuffer, minecraft.getMainRenderTarget()); - GuiUtils.disableScissor(graphics); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y, 0, AREA_W, 10, 0x77000000, 0x00000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y + AREA_H - 10, 0, AREA_W, 10, 0x00000000, 0x77000000); - - // Scrollbar - double maxHeight = ENTRIES_START_Y_OFFSET + routes.length * (RouteEntryOverviewWidget.HEIGHT + 4) + ENTRIES_START_Y_OFFSET; - double aH = AREA_H + 1; - if (aH / maxHeight < 1) { - int scrollerHeight = Math.max(10, (int)(aH * (aH / maxHeight))); - int startY = guiTop + AREA_Y + (int)((AREA_H) * (Math.abs(scrollOffset) / maxHeight)); - - GuiUtils.fill(graphics, guiLeft + AREA_X + AREA_W - 3, startY, 3, scrollerHeight, 0x7FFFFFFF); - } } } else { double offsetX = Math.sin(Math.toRadians(angle)) * 5; @@ -430,10 +394,8 @@ public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float p GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 32 + GUI_HEIGHT / 2, searchingText, 0xFFFFFF, EAlignment.CENTER, false); AllIcons.I_MTD_SCAN.render(graphics.poseStack(), (int)(guiLeft + GUI_WIDTH / 2 - 8 + offsetX), (int)(guiTop + GUI_HEIGHT / 2 + offsetY)); } - - if (switchButtonsArea.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, switchButtonsArea.getLeft(), switchButtonsArea.getTop(), switchButtonsArea.getWidth(), switchButtonsArea.getHeight(), 0x3FFFFFFF); - } + + super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); } @Override @@ -452,20 +414,7 @@ public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { if (destinationSuggestions != null && destinationSuggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton)) return true; - float scrollOffset = scroll.getValue(); - - if (switchButtonsArea.isInBounds(pMouseX, pMouseY)) { - switchButtonClick(); - } - if (super.mouseClicked(pMouseX, pMouseY, pButton)) { - return true; - } - - if (pMouseX > guiLeft + AREA_X && pMouseX < guiLeft + AREA_X + AREA_W && pMouseY > guiTop + AREA_Y && pMouseY < guiTop + AREA_Y + AREA_H) { - routesCollection.performForEach(x -> x.mouseClicked(pMouseX, pMouseY + scrollOffset, pButton)); - } - - return false; + return super.mouseClicked(pMouseX, pMouseY, pButton); } @Override @@ -481,19 +430,6 @@ public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { if (destinationSuggestions != null && destinationSuggestions.mouseScrolled(pMouseX, pMouseY, Mth.clamp(pDelta, -1.0D, 1.0D))) return true; - float chaseTarget = scroll.getChaseTarget(); - float max = -AREA_H; - if (routes != null && routes.length > 0) { - max += ENTRIES_START_Y_OFFSET + routes.length * (RouteEntryOverviewWidget.HEIGHT + 4) + ENTRIES_START_Y_OFFSET; - } - - if (max > 0) { - chaseTarget -= pDelta * 12; - chaseTarget = Mth.clamp(chaseTarget, 0, max); - scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP); - } else - scroll.chase(0, 0.7f, Chaser.EXP); - return super.mouseScrolled(pMouseX, pMouseY, pDelta); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java index 8d5bb394..c376fc77 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java @@ -1,127 +1,72 @@ package de.mrjulsen.crn.client.gui.screen; -import java.util.UUID; +import java.util.LinkedHashMap; +import java.util.Map; -import com.simibubi.create.content.trains.entity.TrainIconType; -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; -import com.simibubi.create.foundation.gui.AllIcons; -import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; +import com.simibubi.create.foundation.gui.widget.IconButton; +import com.simibubi.create.foundation.gui.widget.Indicator.State; +import com.simibubi.create.foundation.item.TooltipHelper; +import com.simibubi.create.foundation.item.TooltipHelper.Palette; +import com.simibubi.create.foundation.utility.Components; -import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.overlay.RouteDetailsOverlay; import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.gui.overlay.RouteDetailsOverlayScreen; import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; -import de.mrjulsen.crn.client.gui.widgets.ExpandButton; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIndicator; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.RouteDetailsViewer; import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.SimpleRoute.SimpleRoutePart; -import de.mrjulsen.crn.data.SimpleRoute.StationEntry; -import de.mrjulsen.crn.data.SimpleRoute.StationTag; -import de.mrjulsen.crn.event.listeners.IJourneyListenerClient; -import de.mrjulsen.crn.event.listeners.JourneyListener; -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; +import de.mrjulsen.crn.data.SavedRoutesManager; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.event.ModCommonEvents; import de.mrjulsen.crn.network.InstanceManager; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.OverlayManager; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.client.util.WidgetsCollection; import de.mrjulsen.mcdragonlib.core.EAlignment; import de.mrjulsen.mcdragonlib.data.Pair; -import de.mrjulsen.mcdragonlib.util.MathUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils.TimeFormat; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.Level; -public class RouteDetailsScreen extends DLScreen implements IJourneyListenerClient { +public class RouteDetailsScreen extends AbstractNavigatorScreen { - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/route_details.png"); - private static final int GUI_WIDTH = 255; - private static final int GUI_HEIGHT = 247; - - private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; - private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; - - private static final int ENTRIES_START_Y_OFFSET = 18; - private static final int ENTRY_WIDTH = 220; - private static final int ENTRY_TIME_X = 28; - private static final int ENTRY_DEST_X = 66; - - - private final int AREA_X = 16; - private final int AREA_Y = 53; - private final int AREA_W = 220; - private final int AREA_H = 157; - - private int guiLeft, guiTop; - private int scrollMax = 0; - - // Controls - private DLCreateIconButton backButton; - private DLCreateIconButton saveButton; - private LerpedFloat scroll = LerpedFloat.linear().startWithValue(0); - private final ExpandButton[] expandButtons; - private final WidgetsCollection expandButtonCollection = new WidgetsCollection(); - - // Data - private final SimpleRoute route; - private final Screen lastScreen; - private final Font font; - private final Font shadowlessFont; - private final Level level; - - // Tooltips private final MutableComponent textDeparture = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.departure"); - private final MutableComponent textTransferIn = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.next_transfer_time"); - private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.transfer"); - private final MutableComponent textJourneyCompleted = ELanguage.translate("gui.createrailwaysnavigator.route_overview.journey_completed"); - private final MutableComponent timeNowText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".time.now"); - private final MutableComponent textConnectionEndangered = ELanguage.translate("gui.createrailwaysnavigator.route_overview.connection_endangered").withStyle(ChatFormatting.GOLD).withStyle(ChatFormatting.BOLD); - private final MutableComponent textConnectionMissed = ELanguage.translate("gui.createrailwaysnavigator.route_overview.connection_missed").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD); - private final MutableComponent textTrainCanceled = ELanguage.translate("gui.createrailwaysnavigator.route_overview.train_canceled").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD); - private final MutableComponent textSaveRoute = TextUtils.translate("gui.createrailwaysnavigator.route_details.save_route"); - private final String keyTrainCancellationReason = "gui.createrailwaysnavigator.route_overview.train_cancellation_info"; - - private final UUID clientId = UUID.randomUUID(); - - @SuppressWarnings("resource") - public RouteDetailsScreen(Screen lastScreen, Level level, SimpleRoute route, UUID listenerId) { - super(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.title")); - this.lastScreen = lastScreen; + private final MutableComponent timeNowText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".time.now"); + private final MutableComponent tooltipSaveRoute = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.save_route.tooltip"); + private final MutableComponent tooltipRemoveRoute = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.remove_route.tooltip"); + private final MutableComponent tooltipShowPopup = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.show_popup.tooltip"); + private final MutableComponent tooltipShowNotifications = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overlay_settings.notifications"); + private final MutableComponent tooltipShowNotificationsDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overlay_settings.notifications.description").withStyle(ChatFormatting.GRAY); + + private final ClientRoute route; + + private RouteDetailsViewer viewer; + private DLCreateIconButton notificationButton; + private DLCreateIndicator notificationIndicator; + private DLCreateIconButton saveRouteBtn; + private DLTooltip saveBtnTooltip; + private DLCreateIconButton popupBtn; + + private final Map> buttonTooltips = new LinkedHashMap<>(); + private final WidgetsCollection buttons = new WidgetsCollection(); + + public RouteDetailsScreen(Screen lastScreen, ClientRoute route) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.title"), BarColor.GOLD); this.route = route; - this.font = Minecraft.getInstance().font; - this.shadowlessFont = new NoShadowFontWrapper(font); - this.level = level; - JourneyListenerManager.getInstance().get(listenerId, this); - - int count = route.getParts().size(); - expandButtons = new ExpandButton[count]; - for (int i = 0; i < count; i++) { - expandButtons[i] = new ExpandButton(0, 0, false, (btn) -> {}); - expandButtonCollection.components.add(expandButtons[i]); - } - } - - @Override - public UUID getJourneyListenerClientId() { - return clientId; - } - - public int getCurrentTime() { - return (int)(level.getDayTime() % DragonLib.TICKS_PER_DAY); } @Override @@ -131,339 +76,119 @@ public boolean isPauseScreen() { @Override public void onClose() { - this.minecraft.setScreen(lastScreen); - JourneyListenerManager.getInstance().removeClientListenerForAll(this); + Minecraft.getInstance().setScreen(lastScreen); } @Override protected void init() { - super.init(); - guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; - + super.init(); + buttons.clear(); + buttonTooltips.clear(); final int fWidth = width; final int fHeight = height; + int dy = FooterSize.DEFAULT.size() + 38; + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, guiLeft + GUI_WIDTH - 8, guiTop + dy, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, null); + viewer = new RouteDetailsViewer(this, guiLeft + 3, guiTop + dy, GUI_WIDTH - 6, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, scrollBar); + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + viewer.displayRoute(route); + - backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIG_BACK) { + saveRouteBtn = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 30, guiTop + 223, DLIconButton.DEFAULT_BUTTON_WIDTH, DLIconButton.DEFAULT_BUTTON_HEIGHT, SavedRoutesManager.isSaved(route) ? ModGuiIcons.BOOKMARK_FILLED.getAsCreateIcon() : ModGuiIcons.BOOKMARK.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - onClose(); + removeTooltips(x -> x == saveBtnTooltip); + addTooltip(DLTooltip.of(SavedRoutesManager.isSaved(route) ? tooltipRemoveRoute : tooltipSaveRoute).assignedTo(saveRouteBtn)); + if (SavedRoutesManager.isSaved(route)) { + SavedRoutesManager.removeRoute(route); + this.setIcon(ModGuiIcons.BOOKMARK.getAsCreateIcon()); + } else { + SavedRoutesManager.saveRoute(route); + this.setIcon(ModGuiIcons.BOOKMARK_FILLED.getAsCreateIcon()); + } + SavedRoutesManager.push(true, null); + boolean isSaved = SavedRoutesManager.isSaved(route); + notificationButton.set_visible(isSaved); + notificationIndicator.set_visible(isSaved); + route.setShowNotifications(isSaved); + } }); - addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_BACK).assignedTo(backButton)); - - saveButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21 + DEFAULT_ICON_BUTTON_WIDTH + 4, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.BOOKMARK.getAsCreateIcon()) { + addTooltip(DLTooltip.of(SavedRoutesManager.isSaved(route) ? tooltipRemoveRoute : tooltipSaveRoute).assignedTo(saveRouteBtn)); + + popupBtn = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 52, guiTop + 223, DLIconButton.DEFAULT_BUTTON_WIDTH, DLIconButton.DEFAULT_BUTTON_HEIGHT, ModGuiIcons.POPUP.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); - if (JourneyListenerManager.getInstance().exists(route.getListenerId())) { - InstanceManager.setRouteOverlay(OverlayManager.add(new RouteDetailsOverlayScreen(level, route, fWidth, fHeight))); - } + InstanceManager.setRouteOverlay(OverlayManager.add(new RouteDetailsOverlay(ModCommonEvents.getPhysicalLevel(), route, fWidth, fHeight))); } }); - addTooltip(DLTooltip.of(textSaveRoute).assignedTo(saveButton)); - } - - @Override - public void tick() { - super.tick(); - scroll.tickChaser(); + addTooltip(DLTooltip.of(tooltipShowPopup).assignedTo(popupBtn)); - saveButton.visible = JourneyListenerManager.getInstance().exists(route.getListenerId()); - } - - private Pair getStationInfo(StationEntry station) { - final boolean reachable = station.reachable(false); - MutableComponent timeText = TextUtils.text(TimeUtils.parseTime((int)((route.getRefreshTime() + DragonLib.DAYTIME_SHIFT) % 24000 + station.getTicks()), ModClientConfig.TIME_FORMAT.get())); - MutableComponent stationText = TextUtils.text(station.getStationName()); - if (!reachable) { - timeText = timeText.withStyle(ChatFormatting.RED).withStyle(ChatFormatting.STRIKETHROUGH); - stationText = stationText.withStyle(ChatFormatting.RED).withStyle(ChatFormatting.STRIKETHROUGH); - } - - return Pair.of(timeText, stationText); - } - - private int renderRouteStart(Graphics graphics, int x, int y, StationEntry stop) { - final int HEIGHT = 30; - final int V = 48; - - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, V, ENTRY_WIDTH, HEIGHT); - - Pair text = getStationInfo(stop); - float scale = shadowlessFont.width(text.getFirst()) > 30 ? 0.75f : 1; - graphics.poseStack().pushPose(); - graphics.poseStack().scale(scale, 1, 1); - int pY = y + 15; - if (stop.shouldRenderRealtime() && stop.reachable(false)) { - pY -= (stop.shouldRenderRealtime() ? 5 : 0); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_TIME_X) / scale), pY + 10, TextUtils.text(TimeUtils.parseTime((int)(stop.getEstimatedTimeWithThreshold() % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), stop.getDifferenceTime() > ModClientConfig.DEVIATION_THRESHOLD.get() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); - } - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_TIME_X) / scale), pY, text.getFirst(), 0xFFFFFF, EAlignment.LEFT, false); - graphics.poseStack().popPose(); - - Component platformText = TextUtils.text(stop.getUpdatedInfo().platform()); - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_DEST_X + 129 - shadowlessFont.width(platformText), y + 15, platformText, stop.stationInfoChanged() ? Constants.COLOR_DELAYED : 0xFFFFFF, EAlignment.LEFT, false); - - MutableComponent name = text.getSecond(); - int maxTextWidth = 135 - 12 - shadowlessFont.width(platformText); - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth - 3).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_DEST_X, y + 15, name, 0xFFFFFF, EAlignment.LEFT, false); - - return HEIGHT; - } - - private int renderTrainDetails(Graphics graphics, int x, int y, SimpleRoutePart part) { - final int HEIGHT = 43; - final int V = 99; - final float scale = 0.75f; - final float mul = 1 / scale; - - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, V, ENTRY_WIDTH, HEIGHT); - part.getTrainIcon().render(TrainIconType.ENGINE, graphics.poseStack(), x + ENTRY_DEST_X, y + 7); - - graphics.poseStack().pushPose(); - graphics.poseStack().scale(scale, scale, scale); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_DEST_X + 24) / scale), (int)((y + 7) / scale), TextUtils.text(String.format("%s (%s)", part.getTrainName(), part.getTrainID().toString().split("-")[0])), 0xDBDBDB, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_DEST_X + 24) / scale), (int)((y + 17) / scale), TextUtils.text(String.format("→ %s", part.getScheduleTitle())), 0xDBDBDB, EAlignment.LEFT, false); - graphics.poseStack().scale(mul, mul, mul); - graphics.poseStack().popPose(); - - return HEIGHT; - } - - private int renderStop(Graphics graphics, int x, int y, StationEntry stop) { - final int HEIGHT = 21; - final int V = 78; - - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, V, ENTRY_WIDTH, HEIGHT); - - Pair text = getStationInfo(stop); - float scale = shadowlessFont.width(text.getFirst()) > 30 ? 0.75f : 1; - graphics.poseStack().pushPose(); - graphics.poseStack().scale(scale, 1, 1); - int pY = y + 6; - if (stop.shouldRenderRealtime() && stop.reachable(false)) { - pY -= (stop.shouldRenderRealtime() ? 5 : 0); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_TIME_X) / scale), pY + 10, TextUtils.text(TimeUtils.parseTime((int)(stop.getEstimatedTimeWithThreshold() % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), stop.getDifferenceTime() > ModClientConfig.DEVIATION_THRESHOLD.get() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); - } - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_TIME_X) / scale), pY, text.getFirst(), 0xFFFFFF, EAlignment.LEFT, false); - graphics.poseStack().popPose(); - - Component platformText = TextUtils.text(stop.getUpdatedInfo().platform()); - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_DEST_X + 129 - shadowlessFont.width(platformText), y + 6, platformText, stop.stationInfoChanged() ? Constants.COLOR_DELAYED : 0xFFFFFF, EAlignment.LEFT, false); - - MutableComponent name = text.getSecond(); - int maxTextWidth = 135 - 12 - shadowlessFont.width(platformText); - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth - 3).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_DEST_X, y + 6, name, 0xFFFFFF, EAlignment.LEFT, false); - - return HEIGHT; - } - - private int renderRouteEnd(Graphics graphics, int x, int y, StationEntry stop) { - final int HEIGHT = 44; - final int V = 142; - - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, V, ENTRY_WIDTH, HEIGHT); - - Pair text = getStationInfo(stop); - float scale = shadowlessFont.width(text.getFirst()) > 30 ? 0.75f : 1; - graphics.poseStack().pushPose(); - graphics.poseStack().scale(scale, 1, 1); - int pY = y + 21; - if (stop.shouldRenderRealtime() && stop.reachable(false)) { - pY -= (stop.shouldRenderRealtime() ? 5 : 0); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_TIME_X) / scale), pY + 10, TextUtils.text(TimeUtils.parseTime((int)(stop.getEstimatedTimeWithThreshold() % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), stop.getDifferenceTime() > ModClientConfig.DEVIATION_THRESHOLD.get() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); - } - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + ENTRY_TIME_X) / scale), pY, text.getFirst(), 0xFFFFFF, EAlignment.LEFT, false); - graphics.poseStack().popPose(); - - Component platformText = TextUtils.text(stop.getUpdatedInfo().platform()); - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_DEST_X + 129 - shadowlessFont.width(platformText), y + 21, platformText, stop.stationInfoChanged() ? Constants.COLOR_DELAYED : 0xFFFFFF, EAlignment.LEFT, false); - - MutableComponent name = text.getSecond(); - int maxTextWidth = 135 - 12 - shadowlessFont.width(platformText); - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth - 3).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_DEST_X, y + 21, name, 0xFFFFFF, EAlignment.LEFT, false); - - return HEIGHT; - } - - private void renderHeadline(Graphics graphics, int pMouseX, int pMouseY) { - Component titleInfo = TextUtils.empty(); - Component headline = TextUtils.empty(); - JourneyListener listener = JourneyListenerManager.getInstance().get(route.getListenerId(), null); - - // Title - if (!route.isValid()) { - titleInfo = TextUtils.translate(keyTrainCancellationReason, route.getInvalidationTrainName()).withStyle(ChatFormatting.RED); - headline = textTrainCanceled; - } else if (listener != null && listener.getIndex() > 0) { - titleInfo = textTransferIn; - long arrivalTime = listener.currentStation().getParent().getEndStation().getEstimatedTimeWithThreshold(); - int time = (int)(arrivalTime % 24000 - getCurrentTime()); - headline = time < 0 || listener.currentStation().getTag() == StationTag.PART_START ? timeNowText : TextUtils.text(TimeUtils.parseTime(time, TimeFormat.HOURS_24)); - } else if (listener == null) { - titleInfo = TextUtils.empty(); - headline = textJourneyCompleted.withStyle(ChatFormatting.GREEN); - } else { - titleInfo = textDeparture; - int departureTicks = route.getStartStation().getTicks(); - int departureTime = (int)(route.getRefreshTime() % 24000 + departureTicks); - headline = departureTime - getCurrentTime() < 0 ? timeNowText : TextUtils.text(TimeUtils.parseTime(departureTime - getCurrentTime(), TimeFormat.HOURS_24)); - } - - GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 19, titleInfo, 0xFFFFFF, EAlignment.CENTER, false); - graphics.poseStack().pushPose(); - graphics.poseStack().scale(2, 2, 2); - GuiUtils.drawString(graphics, font, (guiLeft + GUI_WIDTH / 2) / 2, (guiTop + 31) / 2, headline, 0xFFFFFF, EAlignment.CENTER, false); - graphics.poseStack().popPose(); - } - - private int renderTransfer(Graphics graphics, int x, int y, long a, long b, StationEntry nextStation) { - final int HEIGHT = 24; - final int V = 186; - - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, V, ENTRY_WIDTH, HEIGHT); - - long time = -1; - if (a < 0 || b < 0) { - time = -1; - } else { - time = b - a; - } - - if (nextStation != null && !nextStation.reachable(true)) { - if (nextStation.isTrainCanceled()) { - ModGuiIcons.CROSS.render(graphics, x + ENTRY_TIME_X, y + 13 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_TIME_X + ModGuiIcons.ICON_SIZE + 4, y + 8, textTrainCanceled, 0xFFFFFF, EAlignment.LEFT, false); - } else if (nextStation.isDeparted()) { - ModGuiIcons.CROSS.render(graphics, x + ENTRY_TIME_X, y + 13 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_TIME_X + ModGuiIcons.ICON_SIZE + 4, y + 8, textConnectionMissed, 0xFFFFFF, EAlignment.LEFT, false); - } else { - ModGuiIcons.WARN.render(graphics, x + ENTRY_TIME_X, y + 13 - ModGuiIcons.ICON_SIZE / 2); - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_TIME_X + ModGuiIcons.ICON_SIZE + 4, y + 8, textConnectionEndangered, 0xFFFFFF, EAlignment.LEFT, false); - } - } else { - GuiUtils.drawString(graphics, shadowlessFont, x + ENTRY_TIME_X, y + 8, TextUtils.text(transferText.getString() + " " + (time < 0 ? "" : "(" + TimeUtils.parseDuration(time) + ")")), 0xFFFFFF, EAlignment.LEFT, false); - } - - return HEIGHT; + notificationIndicator = this.addRenderableWidget(new DLCreateIndicator(guiLeft + GUI_WIDTH - DLIconButton.DEFAULT_BUTTON_WIDTH - 8, guiTop + 220, Components.immutableEmpty())); + notificationButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - DLIconButton.DEFAULT_BUTTON_WIDTH - 8, guiTop + 225, DLIconButton.DEFAULT_BUTTON_WIDTH, DLIconButton.DEFAULT_BUTTON_HEIGHT, ModGuiIcons.INFO.getAsCreateIcon())); + notificationButton.withCallback(() -> { + route.setShowNotifications(!route.shouldShowNotifications()); + }); + buttonTooltips.put(notificationButton, Pair.of(tooltipShowNotifications, tooltipShowNotificationsDescription)); + boolean isSaved = SavedRoutesManager.isSaved(route); + notificationButton.set_visible(isSaved); + notificationIndicator.set_visible(isSaved); } @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - pPartialTick = Minecraft.getInstance().getFrameTime(); - float scrollOffset = -scroll.getValue(pPartialTick); - - renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - - /* - for (Widget widget : this.renderables) - widget.render(graphics, pMouseX, pMouseY, pPartialTick); - */ - - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 19, guiTop + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + GUI_WIDTH - 22 - shadowlessFont.width(timeString), guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.LEFT, false); + public void tick() { + super.tick(); + notificationIndicator.state = route.shouldShowNotifications() ? State.ON : State.OFF; + saveRouteBtn.set_visible(!route.getEnd().isDeparted() && !route.isClosed()); + popupBtn.set_visible(!route.getEnd().isDeparted() && !route.isClosed()); - renderHeadline(graphics, pMouseX, pMouseY); - - GuiUtils.enableScissor(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - graphics.poseStack().translate(0, scrollOffset, 0); - - int yOffs = guiTop + 45; - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, guiLeft + 16, yOffs, 22, ENTRIES_START_Y_OFFSET, 0, 48, 22, 1, 256, 256); - yOffs += + ENTRIES_START_Y_OFFSET; - SimpleRoutePart[] partsArray = route.getParts().toArray(SimpleRoutePart[]::new); - for (int i = 0; i < partsArray.length; i++) { - SimpleRoutePart part = partsArray[i]; - - yOffs += renderRouteStart(graphics, guiLeft + 16, yOffs, part.getStartStation()); - yOffs += renderTrainDetails(graphics, guiLeft + 16, yOffs, part); - - ExpandButton btn = expandButtons[i]; - btn.active = part.getStopovers().size() > 0; - - if (btn.active) { - btn.x = guiLeft + 78; - btn.y = yOffs - 14; - - btn.render(graphics.poseStack(), pMouseX, (int)(pMouseY - scrollOffset), pPartialTick); - } - - if (btn.isExpanded()) { - for (StationEntry stop : part.getStopovers()) { - yOffs += renderStop(graphics, guiLeft + 16, yOffs, stop); - } + buttons.performForEachOfType(IconButton.class, x -> { + if (!buttonTooltips.containsKey(x)) { + return; } - yOffs += renderRouteEnd(graphics, guiLeft + 16, yOffs, part.getEndStation()); + x.setToolTip(buttonTooltips.get(x).getFirst()); + x.getToolTip().add(TooltipHelper.holdShift(Palette.YELLOW, hasShiftDown())); - if (i < partsArray.length - 1) { - StationEntry currentStation = part.getEndStation(); - StationEntry nextStation = partsArray[i + 1].getStartStation(); - long a = currentStation.shouldRenderRealtime() ? currentStation.getCurrentTime() : currentStation.getScheduleTime(); - long b = nextStation.shouldRenderRealtime() ? nextStation.getCurrentTime() : nextStation.getScheduleTime(); - yOffs += renderTransfer(graphics, guiLeft + 16, yOffs, a, b, nextStation); + if (hasShiftDown()) { + x.getToolTip().add(buttonTooltips.get(x).getSecond()); } - } - scrollMax = yOffs - guiTop - 45; - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, guiLeft + 16, yOffs, 22, AREA_H, 0, 48, 22, 1, 256, 256); - - GuiUtils.disableScissor(graphics); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y, 0, AREA_W, 10, 0x77000000, 0x00000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y + AREA_H - 10, 0, AREA_W, 10, 0x00000000, 0x77000000); - - // Scrollbar - double maxHeight = scrollMax; - double aH = AREA_H + 1; - if (aH / maxHeight < 1) { - int scrollerHeight = Math.max(10, (int)(aH * (aH / maxHeight))); - int startY = guiTop + AREA_Y + (int)((AREA_H) * (Math.abs(scrollOffset) / maxHeight)); - - GuiUtils.fill(graphics, guiLeft + AREA_X + AREA_W - 3, startY, 3, scrollerHeight, 0x7FFFFFFF); - } + }); } @Override - public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); - float chaseTarget = scroll.getChaseTarget(); - float max = -AREA_H; - max += scrollMax; - - if (max > 0) { - chaseTarget -= pDelta * 12; - chaseTarget = MathUtils.clamp(chaseTarget, 0, max); - scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP); - } else - scroll.chase(0, 0.7f, Chaser.EXP); + int y = FooterSize.DEFAULT.size() - 1; + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, 38, ContainerColor.BLUE); + y += 38 - 1; + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, GUI_HEIGHT - y - FooterSize.SMALL.size() + 1, ContainerColor.GOLD); - return super.mouseScrolled(pMouseX, pMouseY, pDelta); - } + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - float scrollOffset = scroll.getValue(); - - if (pMouseX > guiLeft + AREA_X && pMouseX < guiLeft + AREA_X + AREA_W && pMouseY > guiTop + AREA_Y && pMouseY < guiTop + AREA_Y + AREA_H) { - expandButtonCollection.performForEach(x -> x.active, x -> x.mouseClicked(pMouseX, pMouseY + scrollOffset, pButton)); + if (!route.isAnyCancelled()) { + if (route.getStart().isDeparted()) { + GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 19, "Ankunft in", 0xFFFFFF, EAlignment.CENTER, false); + } else { + GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 19, textDeparture, 0xFFFFFF, EAlignment.CENTER, false); + } + graphics.poseStack().pushPose(); + graphics.poseStack().scale(2, 2, 2); + long time = 0; + if (route.getStart().isDeparted()) { + time = route.getEnd().getRealTimeArrivalTime() - DragonLib.getCurrentWorldTime(); + GuiUtils.drawString(graphics, font, (guiLeft + GUI_WIDTH / 2) / 2, (guiTop + 31) / 2, time < 0 ? timeNowText : TextUtils.text(TimeUtils.parseDurationShort(time)), 0xFFFFFF, EAlignment.CENTER, false); + } else { + time = route.getStart().getRealTimeDepartureTime() - DragonLib.getCurrentWorldTime(); + GuiUtils.drawString(graphics, font, (guiLeft + GUI_WIDTH / 2) / 2, (guiTop + 31) / 2, time < 0 ? timeNowText : TextUtils.text(TimeUtils.parseDurationShort(time)), 0xFFFFFF, EAlignment.CENTER, false); + } + graphics.poseStack().popPose(); } - return super.mouseClicked(pMouseX, pMouseY, pButton); + //GuiUtils.drawString(graphics, font, 5, 5, "State: " + route.getState(), 0xFFFF0000, EAlignment.LEFT, false); } - } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteOverlaySettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteOverlaySettingsScreen.java index 24df4dac..285a3ce5 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteOverlaySettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteOverlaySettingsScreen.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Map; -import com.mojang.text2speech.Narrator; import com.simibubi.create.content.trains.station.NoShadowFontWrapper; import com.simibubi.create.foundation.gui.AllIcons; import com.simibubi.create.foundation.gui.element.GuiGameElement; @@ -19,9 +18,8 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.gui.NavigatorToast; +import de.mrjulsen.crn.client.gui.overlay.RouteDetailsOverlay; import de.mrjulsen.crn.client.gui.overlay.OverlayPosition; -import de.mrjulsen.crn.client.gui.overlay.RouteDetailsOverlayScreen; import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; import de.mrjulsen.crn.client.gui.widgets.DLCreateIndicator; import de.mrjulsen.crn.config.ModClientConfig; @@ -29,6 +27,7 @@ import de.mrjulsen.crn.registry.ModItems; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.client.util.WidgetsCollection; @@ -45,7 +44,6 @@ public class RouteOverlaySettingsScreen extends DLScreen { - private static final Component title = TextUtils.translate("gui.createrailwaysnavigator.overlay_settings.title"); private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/route_overlay_settings.png"); private static final int GUI_WIDTH = 213; private static final int GUI_HEIGHT = 79; @@ -56,14 +54,12 @@ public class RouteOverlaySettingsScreen extends DLScreen { private final ItemStack renderedItem = new ItemStack(ModItems.NAVIGATOR.get()); private int guiLeft, guiTop; - private final RouteDetailsOverlayScreen overlay; + private final RouteDetailsOverlay overlay; private DLCreateIconButton backButton; private DLCreateIconButton detailsButton; private IconButton removeOverlayButton; - private DLCreateIconButton soundButton; private DLCreateIconButton notificationsButton; - private DLCreateIndicator soundIndicator; private DLCreateIndicator notificationsIndicator; private ScrollInput scaleInput; private Component scaleLabel; @@ -87,8 +83,8 @@ public class RouteOverlaySettingsScreen extends DLScreen { private static final MutableComponent textNotificationsDescription = TextUtils.translate("gui.createrailwaysnavigator.route_overlay_settings.notifications.description").withStyle(ChatFormatting.GRAY); @SuppressWarnings("resource") - public RouteOverlaySettingsScreen(RouteDetailsOverlayScreen overlay) { - super(title); + public RouteOverlaySettingsScreen(RouteDetailsOverlay overlay) { + super(TextUtils.translate("gui.createrailwaysnavigator.overlay_settings.title")); this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); this.overlay = overlay; } @@ -100,7 +96,6 @@ public void onClose() { super.onClose(); } - @SuppressWarnings("resource") @Override protected void init() { super.init(); @@ -118,16 +113,15 @@ protected void init() { detailsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 7, guiTop + 55, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_VIEW_SCHEDULE)); detailsButton.withCallback(() -> { Minecraft minecraft = Minecraft.getInstance(); - minecraft.setScreen(new RouteDetailsScreen(this, Minecraft.getInstance().level, overlay.getListener().getListeningRoute(), overlay.getListenerId())); + minecraft.setScreen(new RouteDetailsScreen(null, overlay.getRoute())); }); detailsButton.setToolTip(textShowDetails); buttons.add(detailsButton); removeOverlayButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 27, guiTop + 55, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIG_DISCARD)); removeOverlayButton.withCallback(() -> { - Minecraft minecraft = Minecraft.getInstance(); - minecraft.setScreen(new RouteDetailsScreen(null, Minecraft.getInstance().level, overlay.getListener().getListeningRoute(), overlay.getListenerId())); InstanceManager.removeRouteOverlay(); + onClose(); }); removeOverlayButton.setToolTip(textUnpin); buttons.add(removeOverlayButton); @@ -152,34 +146,13 @@ protected void init() { buttons.add(remOverlayButton); } - // On/Off Buttons - soundButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 10, guiTop + 26, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.SOUND_ON.getAsCreateIcon())); - soundButton.withCallback(() -> { - ModClientConfig.ROUTE_NARRATOR.set(!ModClientConfig.ROUTE_NARRATOR.get()); - - if (ModClientConfig.ROUTE_NARRATOR.get()) { - Narrator.getNarrator().say(narratorOn.getString(), true); - } else { - Narrator.getNarrator().say(narratorOff.getString(), true); - } - }); - buttons.add(soundButton); - buttonTooltips.put(soundButton, Pair.of(textNarrator, textNarratorDescription)); - soundIndicator = this.addRenderableWidget(new DLCreateIndicator(guiLeft + 10, guiTop + 20, Components.immutableEmpty())); - - notificationsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 28, guiTop + 26, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.INFO.getAsCreateIcon())); + notificationsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 20, guiTop + 26, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.INFO.getAsCreateIcon())); notificationsButton.withCallback(() -> { - ModClientConfig.ROUTE_NOTIFICATIONS.set(!ModClientConfig.ROUTE_NOTIFICATIONS.get()); - - if (ModClientConfig.ROUTE_NOTIFICATIONS.get()) { - Minecraft.getInstance().getToasts().addToast(NavigatorToast.multiline(notificationsOn, TextUtils.empty())); - } else { - Minecraft.getInstance().getToasts().addToast(NavigatorToast.multiline(notificationsOff, TextUtils.empty())); - } + overlay.getRoute().setShowNotifications(!overlay.getRoute().shouldShowNotifications()); }); buttons.add(notificationsButton); buttonTooltips.put(notificationsButton, Pair.of(textNotifications, textNotificationsDescription)); - notificationsIndicator = this.addRenderableWidget(new DLCreateIndicator(guiLeft + 28, guiTop + 20, Components.immutableEmpty())); + notificationsIndicator = this.addRenderableWidget(new DLCreateIndicator(guiLeft + 20, guiTop + 20, Components.immutableEmpty())); // scale scaleInput = addRenderableWidget(new ScrollInput(guiLeft + 63, guiTop + 23, 43, 18) @@ -204,8 +177,7 @@ public boolean isPauseScreen() { @Override public void tick() { super.tick(); - soundIndicator.state = ModClientConfig.ROUTE_NARRATOR.get() ? State.ON : State.OFF; - notificationsIndicator.state = ModClientConfig.ROUTE_NOTIFICATIONS.get() ? State.ON : State.OFF; + notificationsIndicator.state = overlay.getRoute().shouldShowNotifications() ? State.ON : State.OFF; buttons.performForEachOfType(IconButton.class, x -> { if (!buttonTooltips.containsKey(x)) { @@ -220,6 +192,20 @@ public void tick() { } }); } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double pDelta) { + + if (scaleInput.mouseScrolled(mouseX, mouseY, pDelta)) { + return true; + } + + if (super.mouseScrolled(mouseX, mouseY, pDelta)) { + return true; + } + + return false; + } @Override public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { @@ -241,8 +227,9 @@ public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float p @Override public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTick); buttons.performForEach(widget -> { - if (widget instanceof AbstractSimiWidget simiWidget && simiWidget.isHoveredOrFocused()) { + if (widget instanceof AbstractSimiWidget simiWidget && simiWidget instanceof IDragonLibWidget dlw && dlw.isMouseSelected()) { List tooltip = simiWidget.getToolTip(); if (tooltip.isEmpty()) return; @@ -251,6 +238,5 @@ public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float renderComponentTooltip(graphics.poseStack(), tooltip, ttx, tty); } }); - super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTick); } } \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/SavedRoutesScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/SavedRoutesScreen.java new file mode 100644 index 00000000..d22da03b --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/SavedRoutesScreen.java @@ -0,0 +1,58 @@ +package de.mrjulsen.crn.client.gui.screen; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.SavedRoutesViewer; +import de.mrjulsen.crn.data.SavedRoutesManager; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; + +public class SavedRoutesScreen extends AbstractNavigatorScreen { + + private SavedRoutesViewer viewer; + + private GuiAreaDefinition workingArea; + + public SavedRoutesScreen(Screen lastScreen) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.title"), BarColor.GOLD); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } + + @Override + protected void init() { + super.init(); + int wY = FooterSize.DEFAULT.size() - 1; + int wH = GUI_HEIGHT - wY - FooterSize.SMALL.size(); + workingArea = new GuiAreaDefinition(guiLeft + 3, guiTop + wY + 2, GUI_WIDTH - 6, wH - 3); + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, workingArea.getRight() - 5, workingArea.getY(), workingArea.getHeight(), null); + this.viewer = new SavedRoutesViewer(this, workingArea.getX(), workingArea.getY(), workingArea.getWidth(), workingArea.getHeight(), scrollBar); + this.viewer.displayRoutes(SavedRoutesManager.getAllSavedRoutes()); + + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX() - 2, workingArea.getY() - 2, workingArea.getWidth() + 4, workingArea.getHeight() + 4, ContainerColor.PURPLE); + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/ScheduleBoardScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/ScheduleBoardScreen.java new file mode 100644 index 00000000..b774c47b --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/ScheduleBoardScreen.java @@ -0,0 +1,268 @@ +package de.mrjulsen.crn.client.gui.screen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.simibubi.create.foundation.gui.AllIcons; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; +import de.mrjulsen.crn.client.gui.widgets.ModDestinationSuggestions; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.SearchOptionButton; +import de.mrjulsen.crn.client.gui.widgets.StationDeparturesViewer; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutDepartureInWidget; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutTrainGroupsWidget; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TagName; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.StationTag.ClientStationTag; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget.FlyoutPointer; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; + +public class ScheduleBoardScreen extends AbstractNavigatorScreen { + + private StationDeparturesViewer viewer; + + @SuppressWarnings("resource") + private UserSettings userSettings = new UserSettings(Minecraft.getInstance().player.getUUID(), false); + + private DLEditBox stationBox; + private String stationFrom; + private ModDestinationSuggestions destinationSuggestions; + + private GuiAreaDefinition workingArea; + private String stationTagName; + private final boolean fixedStation; + + private final List stationNames = new ArrayList<>(); + + private final MutableComponent tooltipSearch = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.search.tooltip"); + private final MutableComponent tooltipLocation = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.location.tooltip"); + private final MutableComponent tooltipRefresh = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.refresh.tooltip"); + + public ScheduleBoardScreen(Screen lastScreen, ClientStationTag tag) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.title"), BarColor.GOLD); + this.fixedStation = tag != null; + if (fixedStation) { + this.stationTagName = tag.tagName(); + } + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } + + @Override + public void tick() { + + DLUtils.doIfNotNull(destinationSuggestions, x -> { + x.tick(); + + if (!stationBox.canConsumeInput()) { + clearSuggestions(); + } + }); + super.tick(); + } + + @Override + protected void init() { + super.init(); + + DataAccessor.getFromServer(true, ModAccessorTypes.GET_ALL_STATIONS_AS_TAGS, (names) -> { + this.stationNames.clear(); + this.stationNames.addAll(names); + }); + + setAllowedLayer(0); + int wY = FooterSize.DEFAULT.size() - 1; + int wH = GUI_HEIGHT - wY - FooterSize.SMALL.size(); + workingArea = new GuiAreaDefinition(guiLeft + 3, guiTop + wY + 2, GUI_WIDTH - 6, wH - 3); + + if (!fixedStation) { + stationBox = addEditBox(guiLeft + 32 + 5, guiTop + 25, 152, 12, stationFrom, TextUtils.empty(), false, (v) -> { + stationFrom = v; + updateEditorSubwidgets(stationBox); + }, NO_EDIT_BOX_FOCUS_CHANGE_ACTION, null); + stationBox.setMaxLength(StationTag.MAX_NAME_LENGTH); + + DLCreateIconButton searchButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 190, guiTop + 20, 18, 18, AllIcons.I_MTD_SCAN) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + if (stationFrom == null || stationFrom.isBlank()) { + viewer.displayRoutes(null, userSettings); + return; + } + DataAccessor.getFromServer(TagName.of(stationFrom), ModAccessorTypes.GET_STATION_TAG_BY_TAG_NAME, (result) -> { + stationTagName = result.getTagName().get(); + viewer.displayRoutes(stationTagName, userSettings); + }); + } + }); + addTooltip(DLTooltip.of(tooltipSearch).assignedTo(searchButton)); + + DLCreateIconButton locationButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 212, guiTop + 20, 18, 18, ModGuiIcons.POSITION.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + DataAccessor.getFromServer(minecraft.player.blockPosition(), ModAccessorTypes.GET_NEAREST_STATION, (result) -> { + if (result.tagName.isPresent()) { + stationBox.setValue(result.tagName.get().get()); + } + }); + } + }); + addTooltip(DLTooltip.of(tooltipLocation).assignedTo(locationButton)); + } + + // Search Options + final int btnCount = 2; + int btnWidth = (workingArea.getWidth() - 16) / btnCount; + addRenderableWidget(new SearchOptionButton(workingArea.getLeft(), workingArea.getTop() + 16 + FooterSize.DEFAULT.size() - 2, btnWidth, 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.departure_in"), () -> userSettings.searchDepartureInTicks.toString(), (b) -> { + new FlyoutDepartureInWidget<>(this, FlyoutPointer.UP, ColorShade.DARK, this::addRenderableWidget, userSettings, () -> { + return userSettings.searchDepartureInTicks; + }, (w) -> { + removeWidget(w); + reloadUserSettings(() -> this.viewer.displayRoutes(stationTagName, userSettings)); + }).open(b); + })); + addRenderableWidget(new SearchOptionButton(workingArea.getLeft() + btnWidth, workingArea.getTop() + 16 + FooterSize.DEFAULT.size() - 2, btnWidth, 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups"), () -> userSettings.searchExcludedTrainGroups.toString(), (b) -> { + new FlyoutTrainGroupsWidget<>(this, FlyoutPointer.UP, ColorShade.DARK, this::addRenderableWidget, userSettings, () -> { + return userSettings.searchExcludedTrainGroups; + }, (w) -> { + removeWidget(w); + reloadUserSettings(() -> this.viewer.displayRoutes(stationTagName, userSettings)); + }).open(b); + })); + DLIconButton refreshBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), workingArea.getRight() - (workingArea.getWidth() - btnWidth * btnCount), workingArea.getTop() + 16 + FooterSize.DEFAULT.size() - 2, (workingArea.getWidth() - btnWidth * btnCount), 18, TextUtils.empty(), + (b) -> { + reloadUserSettings(() -> this.viewer.displayRoutes(stationTagName, userSettings)); + })); + refreshBtn.setBackColor(0x00000000); + addTooltip(DLTooltip.of(tooltipRefresh).assignedTo(refreshBtn)); + + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, workingArea.getRight() - 5, workingArea.getY() + 50, workingArea.getHeight() - 50, GuiAreaDefinition.of(lastScreen)); + this.viewer = new StationDeparturesViewer(this, workingArea.getX(), workingArea.getY() + 50, workingArea.getWidth(), workingArea.getHeight() - 50, scrollBar); + + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + reloadUserSettings(() -> this.viewer.displayRoutes(stationTagName, userSettings)); + } + + @SuppressWarnings("resource") + private void reloadUserSettings(Runnable andThen) { + DataAccessor.getFromServer(Minecraft.getInstance().player.getUUID(), ModAccessorTypes.GET_USER_SETTINGS, settings -> { + this.userSettings = settings; + DLUtils.doIfNotNull(andThen, Runnable::run); + }); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX() - 2, workingArea.getY() - 2, workingArea.getWidth() + 4, 30, ContainerColor.BLUE); + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX() - 2, workingArea.getY() - 2 + 29, workingArea.getWidth() + 4, 22, ContainerColor.GOLD); + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX() - 2, workingArea.getY() - 2 + 50, workingArea.getWidth() + 4, workingArea.getHeight() + 4 - 50, ContainerColor.PURPLE); + + if (fixedStation) { + graphics.poseStack().pushPose(); + graphics.poseStack().scale(2, 2, 2); + GuiUtils.drawString(graphics, font, (guiLeft + GUI_WIDTH / 2) / 2, (guiTop + 22) / 2, GuiUtils.ellipsisString(font, TextUtils.text(stationTagName), GUI_WIDTH / 2), 0xFFFFFF, EAlignment.CENTER, false); + graphics.poseStack().popPose(); + } else { + ModGuiIcons.POSITION.render(graphics, workingArea.getX() + 5, workingArea.getY() + 4); + CreateDynamicWidgets.renderTextBox(graphics, guiLeft + 32, guiTop + 20, 154); + } + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + if (destinationSuggestions != null) { + graphics.poseStack().pushPose(); + graphics.poseStack().translate(0, 0, 500); + destinationSuggestions.render(graphics.poseStack(), mouseX, mouseY); + graphics.poseStack().popPose(); + } + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + } + + protected void updateEditorSubwidgets(DLEditBox field) { + updateEditorSubwidgetsInternal(field, getViableStations(stationNames)); + } + + protected void updateEditorSubwidgetsInternal(DLEditBox field, List list) { + clearSuggestions(); + destinationSuggestions = new ModDestinationSuggestions(this.minecraft, this, field, this.font, list, field.getHeight() + 2 + field.y()); + destinationSuggestions.setAllowSuggestions(true); + destinationSuggestions.updateCommandInfo(); + } + + private List getViableStations(Collection src) { + return src.stream() + .distinct() + .sorted((a, b) -> a.getTagName().get().compareToIgnoreCase(b.getTagName().get())) + .toList(); + } + + private void clearSuggestions() { + if (destinationSuggestions != null) { + destinationSuggestions.getEditBox().setSuggestion(""); + } + destinationSuggestions = null; + } @Override + public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { + if (destinationSuggestions != null && destinationSuggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton)) + return true; + + return super.mouseClicked(pMouseX, pMouseY, pButton); + } + + @Override + public boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) { + if (destinationSuggestions != null && destinationSuggestions.keyPressed(pKeyCode, pScanCode, pModifiers)) + return true; + + return super.keyPressed(pKeyCode, pScanCode, pModifiers); + } + + @Override + public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { + if (destinationSuggestions != null && destinationSuggestions.mouseScrolled(pMouseX, pMouseY, Mth.clamp(pDelta, -1.0D, 1.0D))) + return true; + + return super.mouseScrolled(pMouseX, pMouseY, pDelta); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/SearchSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/SearchSettingsScreen.java deleted file mode 100644 index 95679c9e..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/SearchSettingsScreen.java +++ /dev/null @@ -1,432 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Map.Entry; - -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; -import com.simibubi.create.foundation.gui.AllIcons; -import com.simibubi.create.foundation.gui.widget.AbstractSimiWidget; -import com.simibubi.create.foundation.gui.widget.ScrollInput; -import com.simibubi.create.foundation.utility.animation.LerpedFloat; -import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; -import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.gui.MutableGuiAreaDefinition; -import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; -import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.DLScreen; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainGroup; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.components.Widget; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; -import net.minecraft.world.level.Level; - -public class SearchSettingsScreen extends DLScreen { - - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings.png"); - private static final int GUI_WIDTH = 255; - private static final int GUI_HEIGHT = 247; - - private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; - private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; - private static final int ENTRIES_START_Y_OFFSET = 10; - private static final int ENTRY_HEIGHT = 62; - private static final int ENTRY_SPACING = 4; - private static final int DISPLAY_WIDTH = 164; - - private static final int ARRAY_ENTRY_HEIGHT = 20; - - private final int AREA_X = 16; - private final int AREA_Y = 16; - private final int AREA_W = 220; - private final int AREA_H = 194; - private de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition workingArea; - - - private int guiLeft, guiTop; - private LerpedFloat scroll = LerpedFloat.linear().startWithValue(0); - private int maxY = 0; - - // Data - private final Level level; - private final Font shadowlessFont; - private final Screen lastScreen; - private final TrainGroup[] trainGroups; - private final Map areaByTrainGroup = new HashMap<>(); - private int transferTimeInputInitialY = 0; - private int transferTimeLabelInitialY = 0; - - private boolean trainGroupsExpanded; - - // Widgets - private DLCreateIconButton backButton; - private DLCreateIconButton defaultsButton; - private ScrollInput transferTimeInput; - private Component transferLabel; - private MultiLineLabel transferOptionLabel; - private MultiLineLabel trainGroupsOptionLabel; - - private MutableGuiAreaDefinition trainGroupResetButton; - private MutableGuiAreaDefinition trainGroupExpandButton; - - // Tooltips - private final MutableComponent transferTimeBoxText = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.transfer_time"); - private final MutableComponent transferTimeBoxDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.transfer_time.description"); - private final MutableComponent trainGroupsText = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.train_groups"); - private final MutableComponent trainGroupsDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.train_groups.description"); - private final MutableComponent trainGroupsOverviewAll = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.train_groups.overview.all"); - private final MutableComponent trainGroupsOverviewNone = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.train_groups.overview.none"); - private final MutableComponent tooltipTrainGroupsReset = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.train_groups.tooltip.reset"); - private final String trainGroupsOverviewKey = "gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.train_groups.overview"; - - - @SuppressWarnings("resource") - public SearchSettingsScreen(Level level, Screen lastScreen) { - super(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.title")); - this.level = level; - this.lastScreen = lastScreen; - this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); - this.trainGroups = GlobalSettingsManager.getInstance().getSettingsData().getTrainGroupsList().toArray(TrainGroup[]::new); - } - - @Override - protected void init() { - super.init(); - guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; - - workingArea = new GuiAreaDefinition(guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - - backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 21, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIG_BACK) { - @Override - public void onClick(double mouseX, double mouseY) { - super.onClick(mouseX, mouseY); - onClose(); - } - }); - addTooltip(DLTooltip.of(Constants.TOOLTIP_GO_BACK).assignedTo(backButton)); - - defaultsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 43, guiTop + 222, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_REFRESH) { - @Override - public void onClick(double mouseX, double mouseY) { - super.onClick(mouseX, mouseY); - ModClientConfig.resetSearchSettings(); - clearWidgets(); - init(); - } - }); - addTooltip(DLTooltip.of(Constants.TOOLTIP_RESET_DEFAULTS).assignedTo(defaultsButton)); - - transferTimeLabelInitialY = guiTop + AREA_Y + ENTRIES_START_Y_OFFSET + (0 * (ENTRY_HEIGHT + ENTRY_SPACING)) + 44; - transferTimeInputInitialY = guiTop + AREA_Y + ENTRIES_START_Y_OFFSET + (0 * (ENTRY_HEIGHT + ENTRY_SPACING)) + 39; - transferTimeInput = addRenderableWidget(new ScrollInput(guiLeft + AREA_X + 10 + 25, transferTimeInputInitialY, 60, 18) - .withRange(0, ModClientConfig.MAX_TRANSFER_TIME + 1) - .withStepFunction(x -> 500 * (x.shift ? 2 : 1)) - .titled(transferTimeBoxText.copy()) - .calling((i) -> { - ModClientConfig.TRANSFER_TIME.set(i); - ModClientConfig.TRANSFER_TIME.save(); - ModClientConfig.SPEC.afterReload(); - transferLabel = TextUtils.text(TimeUtils.parseDurationShort(transferTimeInput.getState())); - }) - .setState(ModClientConfig.TRANSFER_TIME.get())); - transferTimeInput.onChanged(); - transferOptionLabel = MultiLineLabel.create(shadowlessFont, transferTimeBoxDescription, (int)((DISPLAY_WIDTH) / 0.75f)); - - - trainGroupExpandButton = new MutableGuiAreaDefinition(0, 0, 16, 16); - trainGroupResetButton = new MutableGuiAreaDefinition(0, 0, 16, 16); - areaByTrainGroup.clear(); - for (int i = 0; i < trainGroups.length; i++) { - TrainGroup group = trainGroups[i]; - areaByTrainGroup.put(group, new MutableGuiAreaDefinition(2, 0, 200 - 4, ARRAY_ENTRY_HEIGHT)); - } - - trainGroupsOptionLabel = MultiLineLabel.create(shadowlessFont, trainGroupsDescription, (int)((DISPLAY_WIDTH - 32) / 0.75f)); - } - - @Override - public void onClose() { - ModClientConfig.SPEC.save(); - ModClientConfig.SPEC.afterReload(); - minecraft.setScreen(lastScreen); - } - - @Override - public void tick() { - super.tick(); - scroll.tickChaser(); - transferTimeInput.tick(); - } - - private void renderDefaultOptionWidget(Graphics graphics, int x, int y, String text, MultiLineLabel label) { - graphics.poseStack().pushPose(); - GuiUtils.drawString(graphics, shadowlessFont, x + 25, y + 6, TextUtils.text(text), 0xFFFFFF, EAlignment.LEFT, false); - graphics.poseStack().scale(0.75f, 0.75f, 0.75f); - label.renderLeftAligned(graphics.poseStack(), (int)((x + 25) / 0.75f), (int)((y + 19) / 0.75f), 10, 0xDBDBDB); - graphics.poseStack().popPose(); - } - - private void modifyTrainGroupFilter(TrainGroup group) { - List current = new ArrayList<>(ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.get()); - if (current.contains(group.getGroupName())) { - current.removeIf(x -> x.equals(group.getGroupName())); - } else { - current.add(group.getGroupName()); - } - ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.set(current); - ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.save(); - ModClientConfig.SPEC.afterReload(); - } - - private void resetTrainGroupFilter() { - ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.set(new ArrayList<>()); - ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.save(); - ModClientConfig.SPEC.afterReload(); - } - - private int getMaxScrollHeight() { - return maxY; - } - - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - pPartialTick = Minecraft.getInstance().getFrameTime(); - renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - float scrollOffset = -scroll.getValue(pPartialTick); - - // SCROLLABLE AREA START - graphics.poseStack().pushPose(); - GuiUtils.enableScissor(graphics, guiLeft + AREA_X, guiTop + AREA_Y, AREA_W, AREA_H); - graphics.poseStack().translate(0, scrollOffset, 0); - - final int defaultWidth = 200; - final int defaultDescriptionHeight = 36; - final int defaultOptionHeight = 26; - int wX = guiLeft + AREA_X + 10; - int wY = guiTop + AREA_Y + ENTRIES_START_Y_OFFSET; - - // transfer time - CreateDynamicWidgets.renderDuoShadeWidget(graphics, wX, wY, defaultWidth, defaultDescriptionHeight, ColorShade.LIGHT, defaultOptionHeight, ColorShade.DARK); - CreateDynamicWidgets.renderTextSlotOverlay(graphics, wX + 25, wY + 39, 163, 18); - CreateDynamicWidgets.renderTextBox(graphics, wX + 25, wY + 39, 66); - renderDefaultOptionWidget(graphics, wX, wY, transferTimeBoxText.getString(), transferOptionLabel); - GuiUtils.drawString(graphics, font, guiLeft + AREA_X + 10 + 30, transferTimeLabelInitialY, transferLabel, 0xFFFFFF, EAlignment.LEFT, true); - wY += ENTRY_SPACING + defaultOptionHeight + defaultDescriptionHeight; - - // train groups - int dY = wY; - if (trainGroupsExpanded) { - CreateDynamicWidgets.renderWidgetInner(graphics, wX, dY, defaultWidth, defaultDescriptionHeight, ColorShade.LIGHT); - CreateDynamicWidgets.renderWidgetTopBorder(graphics, wX, dY, defaultWidth); - dY += defaultDescriptionHeight; - CreateDynamicWidgets.renderWidgetInner(graphics, wX, dY, defaultWidth, 2, ColorShade.DARK); - dY += 2; - - for (int i = 0; i < trainGroups.length; i++) { - if (dY + (i * ARRAY_ENTRY_HEIGHT) > workingArea.getTop() + workingArea.getHeight() - scrollOffset || dY + (i * ARRAY_ENTRY_HEIGHT) < workingArea.getTop() - ARRAY_ENTRY_HEIGHT - scrollOffset) { - continue; - } - - TrainGroup group = trainGroups[i]; - MutableGuiAreaDefinition area = areaByTrainGroup.get(group); - area.setXOffset(wX); - area.setYOffset(dY + (i * ARRAY_ENTRY_HEIGHT)); - - CreateDynamicWidgets.renderWidgetInner(graphics, wX, dY + (i * ARRAY_ENTRY_HEIGHT), defaultWidth, 20, ColorShade.DARK); - CreateDynamicWidgets.renderTextSlotOverlay(graphics, wX + 25, dY + (i * ARRAY_ENTRY_HEIGHT) + 1, 163, ARRAY_ENTRY_HEIGHT - 2); - - MutableComponent name = TextUtils.text(group.getGroupName()); - int maxTextWidth = 163 - 12; - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, wX + 30, dY + (i * ARRAY_ENTRY_HEIGHT) + 1 + 5, name, 0xFFFFFF, EAlignment.LEFT, false); - - CreateDynamicWidgets.renderTextSlotOverlay(graphics, wX + 6, dY + (i * ARRAY_ENTRY_HEIGHT) + 1, 16, ARRAY_ENTRY_HEIGHT - 2); - - if (ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.get().stream().noneMatch(x -> x.equals(group.getGroupName()))) { - AllIcons.I_CONFIRM.render(graphics.poseStack(), wX + 6, dY + (i * ARRAY_ENTRY_HEIGHT) + 2); - } - - if (workingArea.isInBounds(pMouseX, pMouseY) && area.isInBounds(pMouseX, pMouseY - scrollOffset)) { - GuiUtils.fill(graphics, area.getX(), area.getY(), area.getWidth(), area.getHeight(), 0x1AFFFFFF); - } - } - dY += trainGroups.length * ARRAY_ENTRY_HEIGHT; - CreateDynamicWidgets.renderWidgetInner(graphics, wX, dY, defaultWidth, 2, ColorShade.DARK); - dY += 2; - CreateDynamicWidgets.renderWidgetBottomBorder(graphics, wX, dY, defaultWidth); - } else { - CreateDynamicWidgets.renderDuoShadeWidget(graphics, wX, wY, defaultWidth, defaultDescriptionHeight, ColorShade.LIGHT, defaultOptionHeight, ColorShade.DARK); - int amount = trainGroups.length - ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.get().size(); - String text = String.valueOf(amount); - if (amount <= 0) { - text = trainGroupsOverviewNone.getString(); - } else if (amount >= trainGroups.length) { - text = trainGroupsOverviewAll.getString(); - } - GuiUtils.drawString(graphics, font, wX + 25, wY + defaultDescriptionHeight + defaultOptionHeight / 2 - font.lineHeight / 2, TextUtils.translate(trainGroupsOverviewKey, text), amount <= 0 ? 0xFF8888 : 0xFFFF88, EAlignment.LEFT, false); - } - - renderDefaultOptionWidget(graphics, wX, wY, trainGroupsText.getString(), trainGroupsOptionLabel); - trainGroupExpandButton.setXOffset(wX + defaultWidth - 2 - 16); - trainGroupExpandButton.setYOffset(wY + defaultDescriptionHeight / 2 - 7); - trainGroupResetButton.setXOffset(wX + defaultWidth - 2 - 32); - trainGroupResetButton.setYOffset(wY + defaultDescriptionHeight / 2 - 7); - - AllIcons.I_REFRESH.render(graphics.poseStack(), trainGroupResetButton.getX(), trainGroupResetButton.getY()); - if (trainGroupsExpanded) { - ModGuiIcons.COLLAPSE.render(graphics, trainGroupExpandButton.getX(), trainGroupExpandButton.getY()); - } else { - ModGuiIcons.EXPAND.render(graphics, trainGroupExpandButton.getX(), trainGroupExpandButton.getY()); - } - - // Button highlight - if (workingArea.isInBounds(pMouseX, pMouseY)) { - if (trainGroupExpandButton.isInBounds(pMouseX, pMouseY - scrollOffset)) { - GuiUtils.fill(graphics, trainGroupExpandButton.getX(), trainGroupExpandButton.getY(), trainGroupExpandButton.getWidth(), trainGroupExpandButton.getHeight(), 0x1AFFFFFF); - } else if (trainGroupResetButton.isInBounds(pMouseX, pMouseY - scrollOffset)) { - GuiUtils.fill(graphics, trainGroupResetButton.getX(), trainGroupResetButton.getY(), trainGroupResetButton.getWidth(), trainGroupResetButton.getHeight(), 0x1AFFFFFF); - } - } - - wY += ENTRY_SPACING + dY; - - GuiUtils.disableScissor(graphics); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y, 0, AREA_W, 10, 0x77000000, 0x00000000); - GuiUtils.fillGradient(graphics, guiLeft + AREA_X, guiTop + AREA_Y + AREA_H - 10, 0, AREA_W, 10, 0x00000000, 0x77000000); - - // widgets y offset - transferTimeInput.y = (int)(transferTimeInputInitialY + scrollOffset); - - // set scrollbar values - maxY = wY - AREA_H; - graphics.poseStack().popPose(); - // SCROLLABLE AREA END - - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 19, guiTop + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + GUI_WIDTH - 22 - shadowlessFont.width(timeString), guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.LEFT, false); - - double maxHeight = getMaxScrollHeight(); - double aH = AREA_H + 1; - if (aH / maxHeight < 1) { - int scrollerHeight = Math.max(10, (int)(aH * (aH / maxHeight))); - int startY = guiTop + AREA_Y + (int)((AREA_H) * (Math.abs(scrollOffset) / maxHeight)); - - GuiUtils.fill(graphics, guiLeft + AREA_X + AREA_W - 3, startY, 3, scrollerHeight, 0x7FFFFFFF); - } - - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - } - - @Override - public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - int scrollOffset = (int)scroll.getValue(pPartialTick); - - if (workingArea.isInBounds(pMouseX, pMouseY)) { - GuiUtils.renderTooltipWithOffset(this, trainGroupResetButton, List.of(tooltipTrainGroupsReset), width, graphics, pMouseX, pMouseY, 0, scrollOffset); - } - - for (Widget widget : renderables) { - if (widget instanceof AbstractSimiWidget simiWidget && simiWidget.isHoveredOrFocused() - && simiWidget.visible) { - List tooltip = simiWidget.getToolTip(); - if (tooltip.isEmpty()) - continue; - int ttx = simiWidget.lockedTooltipX == -1 ? pMouseX : simiWidget.lockedTooltipX + simiWidget.x; - int tty = simiWidget.lockedTooltipY == -1 ? pMouseY : simiWidget.lockedTooltipY + simiWidget.y; - renderComponentTooltip(graphics.poseStack(), tooltip, ttx, tty); - } - } - - for (Entry entry : areaByTrainGroup.entrySet()) { - if (!workingArea.isInBounds(pMouseX, pMouseY)) { - continue; - } - - if (shadowlessFont.width(entry.getKey().getGroupName()) > 163 - 12 && GuiUtils.renderTooltipAt(this, entry.getValue(), List.of(TextUtils.text(entry.getKey().getGroupName())), width, graphics, entry.getValue().getLeft() + 24, (int)(entry.getValue().getTop() + 2 - scrollOffset), pMouseX, pMouseY, 0, (int)scrollOffset)) { - break; - } - } - - super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTick); - } - - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - float scrollOffset = -scroll.getValue(0); - if (workingArea.isInBounds(pMouseX, pMouseY)) { - if (trainGroupResetButton.isInBounds(pMouseX, pMouseY - scrollOffset)) { - resetTrainGroupFilter(); - GuiUtils.playButtonSound(); - return true; - } else if (trainGroupExpandButton.isInBounds(pMouseX, pMouseY - scrollOffset)) { - trainGroupsExpanded = !trainGroupsExpanded; - GuiUtils.playButtonSound(); - return true; - } - - if (trainGroupsExpanded) { - Optional> area = areaByTrainGroup.entrySet().stream().filter(x -> x.getValue().isInBounds(pMouseX, pMouseY - scrollOffset)).findFirst(); - if (area.isPresent()) { - modifyTrainGroupFilter(area.get().getKey()); - GuiUtils.playButtonSound(); - return true; - } - } - } - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - -@Override -public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { - - if (transferTimeInput.isHoveredOrFocused()) { - boolean b = transferTimeInput.mouseScrolled(pMouseX, pMouseY, pDelta); - if (b) { - return b; - } - } - - float chaseTarget = scroll.getChaseTarget(); - float max = -AREA_H + getMaxScrollHeight(); - - if (max > 0) { - chaseTarget -= pDelta * 12; - chaseTarget = Mth.clamp(chaseTarget, 0, max); - scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP); - } else { - scroll.chase(0, 0.7f, Chaser.EXP); - } - - return super.mouseScrolled(pMouseX, pMouseY, pDelta); -} -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationBlacklistScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationBlacklistScreen.java deleted file mode 100644 index f2aaeee5..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationBlacklistScreen.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.Collection; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.world.level.Level; - -public class StationBlacklistScreen extends AbstractBlacklistScreen { - - public StationBlacklistScreen(Level level, Screen lastScreen) { - super(level, lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".blacklist.title")); - } - - @Override - protected Collection getSuggestions() { - return ClientTrainStationSnapshot.getInstance().getAllTrainStations(); - } - - @Override - protected boolean checkIsBlacklisted(String entry) { - return GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(entry); - } - - @Override - protected String[] getBlacklistedNames(String searchText) { - return GlobalSettingsManager.getInstance().getSettingsData().getBlacklist().stream().filter(x -> x.toLowerCase().contains(searchText.toLowerCase())).toArray(String[]::new); - } - - @Override - protected void addToBlacklist(String name, Runnable andThen) { - if (GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(name) || ClientTrainStationSnapshot.getInstance().getAllTrainStations().stream().noneMatch(x -> x.toLowerCase().equals(name.toLowerCase()))) { - return; - } - - GlobalSettingsManager.getInstance().getSettingsData().addToBlacklist(name, andThen); - } - - @Override - protected void removeFromBlacklist(String name, Runnable andThen) { - GlobalSettingsManager.getInstance().getSettingsData().removeFromBlacklist(name, andThen); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationTagSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationTagSettingsScreen.java new file mode 100644 index 00000000..aa3402db --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/StationTagSettingsScreen.java @@ -0,0 +1,289 @@ +package de.mrjulsen.crn.client.gui.screen; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.lwjgl.glfw.GLFW; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; +import de.mrjulsen.crn.client.gui.widgets.ModStationSuggestions; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.options.DLOptionsList; +import de.mrjulsen.crn.client.gui.widgets.options.DataListContainer; +import de.mrjulsen.crn.client.gui.widgets.options.NewEntryWidget; +import de.mrjulsen.crn.client.gui.widgets.options.OptionEntry; +import de.mrjulsen.crn.client.gui.widgets.options.SimpleDataListNewEntry; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Pair; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.MathUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.MutableComponent; + +public class StationTagSettingsScreen extends AbstractNavigatorScreen { + + private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; + private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; + + private DLOptionsList viewer; + private DLEditBox searchBox; + + private final MutableComponent tooltipDeleteTag = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".station_tags.delete_alias.tooltip"); + private final MutableComponent textAdd = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.add"); + private final MutableComponent textStationName = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".station_tags.hint.station_name"); + private final MutableComponent textPlatformName = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".station_tags.hint.platform"); + + private ModStationSuggestions destinationSuggestions; + private StationTag selectedTag; + private String searchText = ""; + + private final List stationNames = new ArrayList<>(); + + public StationTagSettingsScreen(Screen lastScreen) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".station_tags.title"), BarColor.GRAY); + } + + @Override + public void tick() { + super.tick(); + + DLUtils.doIfNotNull(destinationSuggestions, x -> { + x.tick(); + if (!destinationSuggestions.getEditBox().canConsumeInput()) { + clearSuggestions(); + } + }); + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } + + public void reload() { + clearWidgets(); + init(); + } + + @Override + protected void init() { + super.init(); + + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_STATION_NAMES, (names) -> { + this.stationNames.clear(); + this.stationNames.addAll(names); + }); + + DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - DEFAULT_ICON_BUTTON_WIDTH - 8, guiTop + 223, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + Util.getPlatform().openUri(Constants.HELP_PAGE_STATION_TAGS); + } + }); + addTooltip(DLTooltip.of(Constants.TEXT_HELP).assignedTo(helpButton)); + + int dy = FooterSize.DEFAULT.size() + 1; + + searchBox = addRenderableWidget(new DLEditBox(font, guiLeft + 4, guiTop + dy + 1, GUI_WIDTH - 8, 16, TextUtils.empty()) { + @Override + public boolean keyPressed(int code, int p_keyPressed_2_, int p_keyPressed_3_) { + if (code == GLFW.GLFW_KEY_ENTER) { + searchText = getValue(); + reload(); + return true; + } + return super.keyPressed(code, p_keyPressed_2_, p_keyPressed_3_); + } + }); + searchBox.setValue(searchText); + searchBox.withHint(DragonLib.TEXT_SEARCH); + dy += 18; + + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, guiLeft + GUI_WIDTH - 8, guiTop + dy, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, null); + viewer = new DLOptionsList(this, guiLeft + 3, guiTop + dy, GUI_WIDTH - 6, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, scrollBar); + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_STATION_TAGS, (tgs) -> { + List tags = tgs.stream().sorted((a, b) -> a.getTagName().get().compareToIgnoreCase(b.getTagName().get())).toList(); + for (StationTag tag : tags) { + if (!tag.getTagName().get().toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT))) { + continue; + } + final StationTag stationTag = tag; + OptionEntry>> opt = viewer.addOption((option) -> { + GuiAreaDefinition workspace = option.getContentSpace(); + + DataListContainer> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), stationTag, + (tg) -> { + return tg.getAllStations().entrySet().stream().sorted((a, b) -> a.getKey().compareToIgnoreCase(b.getKey())).iterator(); + }, (data, entryWidget) -> { + entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { + GlobalSettingsClient.removeStationTagEntry(tag.getId(), entry.getKey(), + (newTag) -> { + newTag.ifPresent(a -> refreshAction.accept(newTag)); + }); + }); + entryWidget.addDataSection(40, (entry) -> entry.getValue().platform(), EAlignment.RIGHT, + (tg, entry, newValue, refreshAction) -> { + if (!newValue.isBlank() && !entry.getValue().platform().equals(newValue)) { + GlobalSettingsClient.updateStationTagEntry(tg.getId(), entry.getKey(), new StationInfo(newValue), + (newTag) -> { + newTag.ifPresent(a -> refreshAction.accept(newTag)); + }); + } + }); + return data.getKey(); + }, (data, entryWidget) -> { + entryWidget.addAddButton(ModGuiIcons.ADD.getAsSprite(16, 16), textAdd, + (btn, tg, inputValues, refreshAction) -> { + String name = inputValues.get(SimpleDataListNewEntry.MAIN_INPUT_KEY).get(); + String platform = inputValues.get("platform").get(); + if (name == null || platform == null || name.isBlank() || platform.isBlank()) { + return false; + } + GlobalSettingsClient.addStationTagEntry(tg.getId(), name, new StationInfo(platform), + (newTag) -> { + newTag.ifPresent(a -> refreshAction.accept(newTag)); + }); + return true; + }); + entryWidget.editNameEditBox((box) -> { + box.setResponder((b) -> { + this.updateEditorSubwidgets(box, data); + }); + box.setMaxLength(StationTag.MAX_NAME_LENGTH); + }); + entryWidget.setNameEditBoxTooltip((box) -> textStationName); + entryWidget.addDataSection(40, "platform", textPlatformName, (box) -> box.setMaxLength(StationInfo.MAX_PLATFORM_NAME_LENGTH)); + }, (self) -> { + option.notifyContentSizeChanged(); + } + ); + cont.setPadding(3, 0, 3, 18); + cont.setFilter((entry, searchText) -> { + return entry.getKey().toLowerCase(Locale.ROOT).contains(searchText.get().toLowerCase(Locale.ROOT)); + }); + cont.setBordered(false); + + return cont; + }, TextUtils.text(tag.getTagName().get()), TextUtils.empty(), (a, b) -> OptionEntry.expandOrCollapse(a), + (str) -> { + if (!str.isBlank()) { + GlobalSettingsClient.updateStationTagNameData(stationTag.getId(), str, () -> {}); + return true; + } + return false; + }); + opt.addAdditionalButton(ModGuiIcons.DELETE.getAsSprite(16, 16), tooltipDeleteTag, + (entry, btn) -> { + GlobalSettingsClient.deleteStationTag(entry.getContentContainer().getData().getId(), () -> { + reload(); + }); + }); + + opt.setTooltip(List.of( + TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".station_tags.summary", stationTag.getAllStationNames().size()), + TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".station_tags.editor", stationTag.getLastEditorName(), stationTag.getLastEditedTimeFormatted()) + )); + } + + viewer.addRenderableWidget(new NewEntryWidget(this, () -> Pair.of(-viewer.getXScrollOffset(), -viewer.getYScrollOffset()), (val) -> { + GlobalSettingsClient.createStationTag(val, (tag) -> { + reload(); + }); + return true; + }, 0, 0, viewer.getContentWidth())); + + }); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); + + int y = FooterSize.DEFAULT.size() - 1; + int h = GUI_HEIGHT - y - FooterSize.SMALL.size(); + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, h + 1, ContainerColor.PURPLE); + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTick); + + DLUtils.doIfNotNull(destinationSuggestions, x -> { + graphics.poseStack().pushPose(); + graphics.poseStack().translate(-viewer.getXScrollOffset(), -viewer.getYScrollOffset(), 0); + x.render(graphics.poseStack(), (int)(mouseX + viewer.getXScrollOffset()), (int)(mouseY + viewer.getYScrollOffset())); + graphics.poseStack().popPose(); + }); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (destinationSuggestions != null && destinationSuggestions.mouseScrolled(mouseX + viewer.getXScrollOffset(), mouseY + viewer.getYScrollOffset(), MathUtils.clamp(delta, -1.0D, 1.0D))) + return true; + + return super.mouseScrolled(mouseX, mouseY, delta); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (destinationSuggestions != null && destinationSuggestions.mouseClicked(mouseX + viewer.getXScrollOffset(), mouseY + viewer.getYScrollOffset(), button)) + return true; + + return super.mouseClicked(mouseX, mouseY, button); + } + + private void clearSuggestions() { + if (destinationSuggestions != null) { + destinationSuggestions.getEditBox().setSuggestion(""); + } + destinationSuggestions = null; + } + + + public void updateEditorSubwidgets(EditBox field, StationTag tag) { + clearSuggestions(); + this.selectedTag = tag; + + destinationSuggestions = new ModStationSuggestions(Minecraft.getInstance(), this, field, font, getViableStations(stationNames, field), field.getHeight() + 2 + field.y); + destinationSuggestions.setAllowSuggestions(true); + destinationSuggestions.updateCommandInfo(); + } + + private List getViableStations(Collection src, EditBox field) { + return src.stream() + .distinct() + .filter(x -> !selectedTag.contains(x)) + .sorted((a, b) -> a.compareTo(b)) + .toList(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainBlacklistScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainBlacklistScreen.java deleted file mode 100644 index 1bbcc6ca..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainBlacklistScreen.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.Collection; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.world.level.Level; - -public class TrainBlacklistScreen extends AbstractBlacklistScreen { - - public TrainBlacklistScreen(Level level, Screen lastScreen) { - super(level, lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_blacklist.title")); - } - - @Override - protected Collection getSuggestions() { - return ClientTrainStationSnapshot.getInstance().getAllTrainNames(); - } - - @Override - protected boolean checkIsBlacklisted(String entry) { - return GlobalSettingsManager.getInstance().getSettingsData().isTrainBlacklisted(entry); - } - - @Override - protected String[] getBlacklistedNames(String searchText) { - return GlobalSettingsManager.getInstance().getSettingsData().getTrainBlacklist().stream().filter(x -> x.toLowerCase().contains(searchText.toLowerCase())).toArray(String[]::new); - } - - @Override - protected void addToBlacklist(String name, Runnable andThen) { - if (GlobalSettingsManager.getInstance().getSettingsData().isTrainBlacklisted(name) || ClientTrainStationSnapshot.getInstance().getAllTrainNames().stream().noneMatch(x -> x.equals(name))) { - return; - } - - GlobalSettingsManager.getInstance().getSettingsData().addTrainToBlacklist(name, andThen); - } - - @Override - protected void removeFromBlacklist(String name, Runnable andThen) { - GlobalSettingsManager.getInstance().getSettingsData().removeTrainFromBlacklist(name, andThen); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainDebugScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainDebugScreen.java new file mode 100644 index 00000000..3b9bffaf --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainDebugScreen.java @@ -0,0 +1,66 @@ +package de.mrjulsen.crn.client.gui.screen; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.TrainDebugViewer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; + +public class TrainDebugScreen extends AbstractNavigatorScreen { + + private TrainDebugViewer viewer; + + private GuiAreaDefinition workingArea; + + public TrainDebugScreen(Screen lastScreen) { + super(lastScreen, TextUtils.text("Train Status Viewer"), BarColor.PURPLE); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } + + @Override + protected void init() { + super.init(); + int wY = FooterSize.DEFAULT.size() - 1; + int wH = GUI_HEIGHT - wY - FooterSize.SMALL.size(); + workingArea = new GuiAreaDefinition(guiLeft + 3, guiTop + wY + 2, GUI_WIDTH - 6, wH - 3); + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, workingArea.getRight() - 5, workingArea.getY(), workingArea.getHeight(), null); + this.viewer = new TrainDebugViewer(this, workingArea.getX(), workingArea.getY(), workingArea.getWidth(), workingArea.getHeight(), scrollBar); + this.viewer.reload(); + + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + + this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 18 - 8, guiTop + 223, 18, 18, ModGuiIcons.REFRESH.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + viewer.reload(); + } + }); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX() - 2, workingArea.getY() - 2, workingArea.getWidth() + 4, workingArea.getHeight() + 4, ContainerColor.PURPLE); + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainGroupScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainGroupScreen.java deleted file mode 100644 index e3033d9c..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainGroupScreen.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.mrjulsen.crn.client.gui.screen; - -import java.util.Collection; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.widgets.TrainGroupEntryWidget; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainGroup; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.world.level.Level; - -public class TrainGroupScreen extends AbstractEntryListSettingsScreen { - - public TrainGroupScreen(Level level, Screen lastScreen) { - super(level, lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_group_settings.title")); - } - - @Override - protected TrainGroup[] getData(String searchText) { - return GlobalSettingsManager.getInstance().getSettingsData().getTrainGroupsList().stream().filter(x -> x.getGroupName().toLowerCase().contains(searchText.toLowerCase())).toArray(TrainGroup[]::new); - } - - @Override - protected TrainGroupEntryWidget createWidget(WidgetCreationData> widgetData, TrainGroup data) { - Collection expandedAliasNames = widgetData.previousEntries().stream().filter(x -> x instanceof TrainGroupEntryWidget w && w.isExpanded()).map(x -> ((TrainGroupEntryWidget)x).getTrainGroup().getGroupName()).toList(); - return new TrainGroupEntryWidget(widgetData.parent(), widgetData.x(), widgetData.y(), data, () -> refreshEntries(), expandedAliasNames.contains(data.getGroupName())); - } - - @Override - protected void onCreateNewEntry(String value, Runnable refreshAction) { - GlobalSettingsManager.getInstance().getSettingsData().registerTrainGroup(new TrainGroup(value), refreshAction); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java new file mode 100644 index 00000000..23d26ab2 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.client.gui.screen; + +import java.util.UUID; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.RouteDetailsViewer; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.world.level.Level; + +public class TrainJourneySreen extends AbstractNavigatorScreen { + + private final ClientRoute route; + private final ClientRoutePart part; + + private RouteDetailsViewer viewer; + + public TrainJourneySreen(Screen lastScreen, ClientRoute route, UUID trainId) { + super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.title"), BarColor.GOLD); + this.route = route; + this.part = route.getClientParts().stream().filter(x -> x.getTrainId().equals(trainId)).findFirst().orElse(route.getFirstClientPart()); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(lastScreen); + } + + @Override + protected void init() { + super.init(); + int dy = FooterSize.DEFAULT.size() + 32; + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, guiLeft + GUI_WIDTH - 8, guiTop + dy, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, null); + viewer = new RouteDetailsViewer(this, guiLeft + 3, guiTop + dy, GUI_WIDTH - 6, GUI_HEIGHT - dy - FooterSize.SMALL.size() - 1, scrollBar); + viewer.setShowTrainDetails(false); + viewer.setCanExpandCollapse(false); + viewer.setInitialExpanded(true); + viewer.setShowJourney(true); + addRenderableWidget(viewer); + addRenderableWidget(scrollBar); + viewer.displayPart(route, x -> x == part); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderNavigatorBackground(graphics, mouseX, mouseY, partialTicks); + + int y = FooterSize.DEFAULT.size() - 1; + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, 32, ContainerColor.BLUE); + GuiUtils.drawString(graphics, font, guiLeft + 8, guiTop + y + 7, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.date", (ModCommonEvents.getPhysicalLevel().dayTime() / Level.TICKS_PER_DAY)), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, guiLeft + 8, guiTop + y + 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.train", part.getFirstStop().getTrainName(), part.getFirstStop().getTrainId().toString().split("-")[0], part.getFirstStop().getDisplayTitle()), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + y += 32 - 1; + CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, GUI_HEIGHT - y - FooterSize.SMALL.size() + 1, ContainerColor.GOLD); + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java new file mode 100644 index 00000000..ec310b75 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java @@ -0,0 +1,225 @@ +package de.mrjulsen.crn.client.gui.screen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.simibubi.create.AllItems; +import com.simibubi.create.foundation.gui.AllIcons; +import com.simibubi.create.foundation.gui.element.GuiGameElement; +import com.simibubi.create.foundation.gui.widget.AbstractSimiWidget; +import com.simibubi.create.foundation.gui.widget.Label; +import com.simibubi.create.foundation.gui.widget.ScrollInput; +import com.simibubi.create.foundation.utility.Components; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; +import de.mrjulsen.crn.client.gui.widgets.DLCreateLabel; +import de.mrjulsen.crn.client.gui.widgets.DLCreateSelectionScrollInput; +import de.mrjulsen.crn.config.ModCommonConfig; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.crn.data.schedule.instruction.TravelSectionInstruction; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLCheckBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +public class TrainSectionSettingsScreen extends DLScreen { + + private static final ResourceLocation TEXTURE = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/section_settings.png"); + private static final int GUI_WIDTH = 212; + private static final int GUI_HEIGHT = 143; + private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; + private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; + private static final ItemStack DISPLAY_ITEM = new ItemStack(AllItems.SCHEDULE.get()); + + private final CompoundTag nbt; + private final Screen lastScreen; + + // Settings + private boolean includePreviousStation = false; + private boolean usable = true; + private String trainGroupId; + private String trainLineId; + + private Map groupsById; + private Map linesById; + + // GUI + private int guiLeft; + private int guiTop; + + private ScrollInput infoTypeInput; + private Label infoTypeLabel; + private ScrollInput displayTypeInput; + private Label displayTypeLabel; + private DLCreateIconButton backButton; + private DLCreateIconButton globalSettingsButton; + + private final MutableComponent tooltipGlobalSettings = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.global_settings.tooltip"); + private final MutableComponent tooltipTrainGroup = TextUtils.translate("gui.createrailwaysnavigator.section_settings.train_groups"); + private final MutableComponent tooltipTrainLine = TextUtils.translate("gui.createrailwaysnavigator.section_settings.train_lines"); + private final MutableComponent textIncludePreviousStation = TextUtils.translate("gui.createrailwaysnavigator.section_settings.include_previous_station"); + private final MutableComponent textUsable = TextUtils.translate("gui.createrailwaysnavigator.section_settings.usable"); + private final MutableComponent textNone = TextUtils.translate("gui.createrailwaysnavigator.section_settings.none"); + + public TrainSectionSettingsScreen(Screen lastScreen, CompoundTag nbt) { + super(TextUtils.translate("gui.createrailwaysnavigator.section_settings.title")); + this.lastScreen = lastScreen; + this.nbt = nbt; + + this.includePreviousStation = nbt.contains(TravelSectionInstruction.NBT_INCLUDE_PREVIOUS_STATION) ? nbt.getBoolean(TravelSectionInstruction.NBT_INCLUDE_PREVIOUS_STATION) : false; + this.usable = nbt.contains(TravelSectionInstruction.NBT_USABLE) ? nbt.getBoolean(TravelSectionInstruction.NBT_USABLE) : true; + this.trainGroupId = nbt.contains(TravelSectionInstruction.NBT_TRAIN_GROUP) ? nbt.getString(TravelSectionInstruction.NBT_TRAIN_GROUP) : null; + this.trainLineId = nbt.contains(TravelSectionInstruction.NBT_TRAIN_LINE) ? nbt.getString(TravelSectionInstruction.NBT_TRAIN_LINE) : null; + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + nbt.putString(TravelSectionInstruction.NBT_TRAIN_GROUP, trainGroupId == null ? "" : trainGroupId); + nbt.putString(TravelSectionInstruction.NBT_TRAIN_LINE, trainLineId == null ? "" : trainLineId); + nbt.putBoolean(TravelSectionInstruction.NBT_INCLUDE_PREVIOUS_STATION, includePreviousStation); + nbt.putBoolean(TravelSectionInstruction.NBT_USABLE, usable); + Minecraft.getInstance().setScreen(lastScreen); + } + + @Override + protected void init() { + super.init(); + guiLeft = width() / 2 - GUI_WIDTH / 2; + guiTop = height() / 2 - GUI_HEIGHT / 2; + + backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 179, guiTop + 119, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIRM)); + backButton.withCallback(() -> { + onClose(); + }); + + DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 179 - DEFAULT_ICON_BUTTON_WIDTH - 10, guiTop + 119, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + Util.getPlatform().openUri(Constants.HELP_PAGE_SCHEDULE_SECTIONS); + } + }); + addTooltip(DLTooltip.of(Constants.TEXT_HELP).assignedTo(helpButton)); + + // Global Options Button + if (minecraft.player.hasPermissions(ModCommonConfig.GLOBAL_SETTINGS_PERMISSION_LEVEL.get())) { + final Screen instance = this; + globalSettingsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 7, guiTop + 119, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.SETTINGS.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + DLScreen.setScreen(new GlobalSettingsScreen(instance)); + } + }); + addTooltip(DLTooltip.of(tooltipGlobalSettings).assignedTo(globalSettingsButton)); + } + + GlobalSettingsClient.getTrainGroups((trainGroups) -> { + this.groupsById = trainGroups.stream().collect(Collectors.toMap(x -> x.getGroupName(), x -> x)); + GlobalSettingsClient.getTrainLines((trainLines) -> { + this.linesById = trainLines.stream().collect(Collectors.toMap(x -> x.getLineName(), x -> x)); + + List groupsList = new ArrayList<>(trainGroups.stream().map(x -> TextUtils.text(x.getGroupName())).toList()); + groupsList.add(0, textNone); + displayTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 23 + 5, Components.immutableEmpty()).withShadow()); + displayTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 23, 138, 18) + .forOptions(groupsList) + .titled(tooltipTrainGroup) + .writingTo(displayTypeLabel) + .calling((i) -> { + this.trainGroupId = i <= 0 ? null : trainGroups.get(i - 1).getGroupName(); + }) + .setState(trainGroupId != null && groupsById.containsKey(trainGroupId) ? trainGroups.indexOf(groupsById.get(trainGroupId)) + 1 : 0) + ); + displayTypeInput.onChanged(); + + List linesList = new ArrayList<>(trainLines.stream().map(x -> TextUtils.text(x.getLineName())).toList()); + linesList.add(0, textNone); + infoTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 45 + 5, Components.immutableEmpty()).withShadow()); + infoTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 45, 138, 18) + .forOptions(linesList) + .titled(tooltipTrainLine) + .writingTo(infoTypeLabel) + .calling((i) -> { + this.trainLineId = i <= 0 ? null : trainLines.get(i - 1).getLineName(); + }) + .setState(trainLineId != null && linesById.containsKey(trainLineId) ? trainLines.indexOf(linesById.get(trainLineId)) + 1 : 0) + ); + infoTypeInput.onChanged(); + + addRenderableWidget(new DLCheckBox(guiLeft + 21, guiTop + 67 + 1, 165, textIncludePreviousStation.getString(), includePreviousStation, (box) -> { + this.includePreviousStation = box.isChecked(); + })); + addRenderableWidget(new DLCheckBox(guiLeft + 21, guiTop + 87 + 1, 165, textUsable.getString(), usable, (box) -> { + this.usable = box.isChecked(); + })); + }); + }); + + } + + @Override + public void tick() { + super.tick(); + DLUtils.doIfNotNull(displayTypeInput, x -> x.tick()); + DLUtils.doIfNotNull(infoTypeInput, x -> x.tick()); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderScreenBackground(graphics); + GuiUtils.drawTexture(TEXTURE, graphics, guiLeft, guiTop, GUI_WIDTH, GUI_HEIGHT, 0, 0, 256, 256); + GuiUtils.drawString(graphics, font, guiLeft + 6, guiTop + 4, getTitle(), DragonLib.NATIVE_UI_FONT_COLOR, EAlignment.LEFT, false); + + ModGuiIcons.TRAIN.render(graphics, guiLeft + 22, guiTop + 24); + ModGuiIcons.MAP_PATH.render(graphics, guiLeft + 22, guiTop + 46); + + GuiGameElement.of(DISPLAY_ITEM).at(guiLeft + GUI_WIDTH, guiTop + GUI_HEIGHT - 48, -200) + .scale(4f) + .render(graphics.poseStack()); + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + @Override + public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTick); + for (Widget widget : renderables) { + if (widget instanceof AbstractSimiWidget simiWidget && simiWidget.isHoveredOrFocused() && simiWidget.visible) { + List tooltip = simiWidget.getToolTip(); + if (tooltip.isEmpty()) + continue; + int ttx = simiWidget.lockedTooltipX == -1 ? pMouseX : simiWidget.lockedTooltipX + simiWidget.x; + int tty = simiWidget.lockedTooltipY == -1 ? pMouseY : simiWidget.lockedTooltipY + simiWidget.y; + renderComponentTooltip(graphics.poseStack(), tooltip, ttx, tty); + } + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractEntryListOptionWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractEntryListOptionWidget.java deleted file mode 100644 index d31eba5f..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractEntryListOptionWidget.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; - -public abstract class AbstractEntryListOptionWidget extends WidgetContainer implements IEntryListSettingsOption { - - public AbstractEntryListOptionWidget(int x, int y, int width, int height) { - super(x, y, width, height); - } - - @Override - public boolean consumeScrolling(double mouseX, double mouseY) { - return false; - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java new file mode 100644 index 00000000..92b7faeb --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java @@ -0,0 +1,223 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.function.Consumer; + +import com.mojang.blaze3d.systems.RenderSystem; + +import de.mrjulsen.crn.client.CRNGui; +import de.mrjulsen.crn.client.gui.Animator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.MathUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; + +public abstract class AbstractFlyoutWidget extends WidgetContainer { + + protected final DLScreen screen; + protected final FlyoutPointer pointer; + protected final ColorShade pointerShade; + protected final int distanceToParent = 0; + protected final Animator animator = addRenderableOnly(new Animator()); + private final Consumer addRenderableWidgetFunc; + private final Consumer removeWidgetFunc; + private int xOffset; + private int yOffset; + + private boolean isClosing = false; + + private final Cache contentArea = new Cache<>(() -> new GuiAreaDefinition(x() + (FlyoutPointer.WIDTH - 2), y() + (FlyoutPointer.HEIGHT - 2), width() - (FlyoutPointer.WIDTH - 2) * 2, height() - (FlyoutPointer.HEIGHT - 2) * 2)); + + public AbstractFlyoutWidget(DLScreen screen, int width, int height, FlyoutPointer pointer, ColorShade pointerShade, Consumer addRenderableWidgetFunc, Consumer removeWidgetFunc) { + super(0, 0, width, height); + this.screen = screen; + this.pointerShade = pointerShade; + this.pointer = pointer; + this.addRenderableWidgetFunc = addRenderableWidgetFunc; + this.removeWidgetFunc = removeWidgetFunc; + } + + public GuiAreaDefinition getContentArea() { + return contentArea.get(); + } + + @SuppressWarnings({ "resource", "unchecked" }) + public void open(IDragonLibWidget parent) { + screen.setAllowedLayer(screen.getAllowedLayer() + 1); + setWidgetLayerIndex(screen.getAllowedLayer()); + + switch (pointer) { + case UP -> { + set_x(MathUtils.clamp(xOffset + parent.x() + parent.width() / 2 - width() / 2, 0, Minecraft.getInstance().screen.width - width())); + set_y(MathUtils.clamp(yOffset + parent.y() + parent.height() + distanceToParent, 0, Minecraft.getInstance().screen.height - height() - distanceToParent)); + } + case DOWN -> { + set_x(MathUtils.clamp(xOffset + parent.x() + parent.width() / 2 - width() / 2, 0, Minecraft.getInstance().screen.width - width())); + set_y(MathUtils.clamp(yOffset + parent.y() - height() - distanceToParent, 0, Minecraft.getInstance().screen.height - height() - distanceToParent)); + } + case RIGHT -> { + set_x(MathUtils.clamp(xOffset + parent.x() - width() - distanceToParent, 0, Minecraft.getInstance().screen.width - width() - distanceToParent)); + set_y(MathUtils.clamp(yOffset + parent.y() + parent.height() / 2 - height() / 2, 0, Minecraft.getInstance().screen.height - height())); + } + case LEFT -> { + set_x(MathUtils.clamp(xOffset + parent.x() + parent.width() + distanceToParent, 0, Minecraft.getInstance().screen.width - width() - distanceToParent)); + set_y(MathUtils.clamp(yOffset + parent.y() + parent.height() / 2 - height() / 2, 0, Minecraft.getInstance().screen.height - height())); + } + } + contentArea.clear(); + animator.start(3, null, null, null); + addRenderableWidgetFunc.accept((T)this); + } + + public void close() { + isClosing = true; + screen.setAllowedLayer(getWidgetLayerIndex() - 1); + animator.start(3, null, null, () -> { + screen.setAllowedLayer(getWidgetLayerIndex() - 1); + removeWidgetFunc.accept(this); + }); + } + + public void closeImmediately() { + isClosing = true; + screen.setAllowedLayer(getWidgetLayerIndex() - 1); + removeWidgetFunc.accept(this); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + graphics.poseStack().pushPose(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + if (animator.isRunning()) { + if (isClosing) { + switch (pointer) { + case UP -> graphics.poseStack().translate(0, -animator.getCurrentTicksSmooth() * 2, 0); + case DOWN -> graphics.poseStack().translate(0, animator.getTotalTicks() * 2 - animator.getCurrentTicksSmooth() * 2, 0); + case LEFT -> graphics.poseStack().translate(animator.getTotalTicks() * 2 - animator.getCurrentTicksSmooth() * 2, 0, 0); + case RIGHT -> graphics.poseStack().translate(animator.getCurrentTicksSmooth() * 2, 0, 0); + } + GuiUtils.setTint(1, 1, 1, 1f - animator.getPercentage()); + } else { + switch (pointer) { + case UP -> graphics.poseStack().translate(0, -animator.getTotalTicks() * 2 + animator.getCurrentTicksSmooth() * 2, 0); + case DOWN -> graphics.poseStack().translate(0, animator.getCurrentTicksSmooth() * 2, 0); + case LEFT -> graphics.poseStack().translate(-animator.getCurrentTicksSmooth() * 2, 0, 0); + case RIGHT -> graphics.poseStack().translate(animator.getTotalTicks() * 2 - animator.getCurrentTicksSmooth() * 2, 0, 0); + } + GuiUtils.setTint(1, 1, 1, animator.getPercentage()); + } + } + + renderFlyout(graphics, mouseX, mouseY, partialTicks, getContentArea()); + renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, getContentArea()); + graphics.poseStack().popPose(); + } + + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + public void renderFlyout(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + CreateDynamicWidgets.renderShadow(graphics, contentArea.getX(), contentArea.getY(), contentArea.getWidth(), contentArea.getHeight()); + CreateDynamicWidgets.renderSingleShadeWidget(graphics, contentArea.getX(), contentArea.getY(), contentArea.getWidth(), contentArea.getHeight(), pointerShade); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + switch (pointer) { + case UP: + pointer.render(graphics, x() + width() / 2 - FlyoutPointer.WIDTH / 2, y(), pointerShade); + break; + case DOWN: + pointer.render(graphics, x() + width() / 2 - FlyoutPointer.WIDTH / 2, y() + height() - FlyoutPointer.HEIGHT, pointerShade); + break; + case LEFT: + pointer.render(graphics, x(), y() + height() / 2 - FlyoutPointer.HEIGHT / 2, pointerShade); + break; + case RIGHT: + pointer.render(graphics, x() + width() - FlyoutPointer.WIDTH, y() + height() / 2 - FlyoutPointer.HEIGHT / 2, pointerShade); + break; + default: + break; + } + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY)) { + close(); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + public int getXOffset() { + return xOffset; + } + + public void setXOffset(int xOffset) { + this.xOffset = xOffset; + } + + public int getYOffset() { + return yOffset; + } + + public void setYOffset(int yOffset) { + this.yOffset = yOffset; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return true; + } + + public static enum FlyoutPointer { + UP(0, 54), + DOWN(14, 54), + RIGHT(28, 54), + LEFT(42, 54); + + private final int u; + private final int v; + public static final int WIDTH = 7; + public static final int HEIGHT = 7; + + private FlyoutPointer(int u, int v) { + this.u = u; + this.v = v; + } + + public int getU() { + return u; + } + + public int getV() { + return v; + } + + public void render(Graphics graphics, int x, int y, ColorShade shade) { + int u = shade == ColorShade.LIGHT ? getU() : getU() + WIDTH; + GuiUtils.drawTexture(CRNGui.GUI, graphics, x, y, WIDTH, HEIGHT, u, v, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java new file mode 100644 index 00000000..9e10c75a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java @@ -0,0 +1,115 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.function.Consumer; + +import com.mojang.blaze3d.systems.RenderSystem; + +import de.mrjulsen.crn.client.gui.Animator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; + +public abstract class AbstractNotificationPopup extends WidgetContainer { + + protected final DLScreen screen; + protected final ColorShade shade; + protected final Animator animator = addRenderableOnly(new Animator()); + private final Consumer removeWidgetFunc; + private int xOffset; + private int yOffset; + + private boolean isClosing = false; + + public AbstractNotificationPopup(DLScreen screen, int x, int y, int width, int height, ColorShade shade, Consumer removeWidgetFunc) { + super(x, y, width, height); + this.screen = screen; + this.shade = shade; + this.removeWidgetFunc = removeWidgetFunc; + animator.start(3, null, null, null); + } + + public void close() { + isClosing = true; + animator.start(3, null, null, () -> { + removeWidgetFunc.accept(this); + }); + } + + public void closeImmediately() { + isClosing = true; + removeWidgetFunc.accept(this); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + graphics.poseStack().pushPose(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + if (animator.isRunning()) { + if (isClosing) { + graphics.poseStack().translate(0, animator.getCurrentTicksSmooth() * 2, 0); + GuiUtils.setTint(1, 1, 1, 1f - animator.getPercentage()); + } else {graphics.poseStack().translate(0, animator.getTotalTicks() * 2 - animator.getCurrentTicksSmooth() * 2, 0); + GuiUtils.setTint(1, 1, 1, animator.getPercentage()); + } + } + + renderFlyout(graphics, mouseX, mouseY, partialTicks, GuiAreaDefinition.of(this)); + renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, GuiAreaDefinition.of(this)); + graphics.poseStack().popPose(); + } + + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + public void renderFlyout(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + CreateDynamicWidgets.renderShadow(graphics, contentArea.getX(), contentArea.getY(), contentArea.getWidth(), contentArea.getHeight()); + CreateDynamicWidgets.renderSingleShadeWidget(graphics, contentArea.getX(), contentArea.getY(), contentArea.getWidth(), contentArea.getHeight(), shade); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY)) { + close(); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + public int getXOffset() { + return xOffset; + } + + public void setXOffset(int xOffset) { + this.xOffset = xOffset; + } + + public int getYOffset() { + return yOffset; + } + + public void setYOffset(int yOffset) { + this.yOffset = yOffset; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return true; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AliasEntryWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AliasEntryWidget.java deleted file mode 100644 index 2fa32994..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AliasEntryWidget.java +++ /dev/null @@ -1,501 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.Map.Entry; - -import com.mojang.blaze3d.vertex.PoseStack; -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.screen.AbstractEntryListSettingsScreen; -import de.mrjulsen.crn.data.AliasName; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.MathUtils; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; - -public class AliasEntryWidget extends AbstractEntryListOptionWidget { - - private static final ResourceLocation GUI_WIDGETS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings_widgets.png"); - public static final int WIDTH = 200; - public static final int HEIGHT = 48; - private static final int STATION_ENTRY_HEIGHT = 20; - - private final AbstractEntryListSettingsScreen parent; - private final Font shadowlessFont; - private final Minecraft minecraft; - - private final Runnable onUpdate; - - // Data - private TrainStationAlias alias; - private boolean expanded = false; - - // Controls - private final DLEditBox titleBox; - private final DLEditBox newEntryBox; - private final DLEditBox newEntryPlatformBox; - //private final WidgetsCollection controls = new WidgetsCollection(); - private final Map removeStationButtons = new HashMap<>(); - private final Map stationInfoAreas = new HashMap<>(); - private final Map stationNameAreas = new HashMap<>(); - - private GuiAreaDefinition titleBarArea; - - private GuiAreaDefinition deleteButton; - private GuiAreaDefinition expandButton; - private GuiAreaDefinition addButton; - - private ModStationSuggestions destinationSuggestions; - - // Edit station info - private String selectedStationName = null; - private final DLEditBox editAliasPlatform; - - // Tooltips - private final MutableComponent tooltipDeleteAlias = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.delete_alias.tooltip"); - private final MutableComponent tooltipDeleteStation = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.delete_station.tooltip"); - private final MutableComponent tooltipAddStation = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.add_station.tooltip"); - private final MutableComponent tooltipStationName = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.hint.station_name"); - private final MutableComponent tooltipPlatform = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.hint.platform"); - - - public AliasEntryWidget(AbstractEntryListSettingsScreen parent, int pX, int pY, TrainStationAlias alias, Runnable onUpdate, boolean expanded) { - super(pX, pY, 200, 48); - - Minecraft minecraft = Minecraft.getInstance(); - shadowlessFont = new NoShadowFontWrapper(minecraft.font); - - this.minecraft = Minecraft.getInstance(); - this.parent = parent; - this.alias = alias; - this.expanded = expanded; - this.onUpdate = onUpdate; - - titleBox = new DLEditBox(minecraft.font, pX + 30, pY + 10, 129, 12, TextUtils.empty()); - titleBox.setBordered(false); - titleBox.setMaxLength(32); - titleBox.setTextColor(0xFFFFFF); - titleBox.setValue(alias.getAliasName().get()); - titleBox.withOnFocusChanged((box, focused) -> { - if (!focused) { - if (!setAliasName(box.getValue())) { - titleBox.setValue(alias.getAliasName().get()); - } - } - }); - titleBox.visible = expanded; - addRenderableWidget(titleBox); - - - newEntryBox = new DLEditBox(minecraft.font, pX + 30, pY + 30, 95, 12, TextUtils.empty()); - newEntryBox.setBordered(false); - newEntryBox.setMaxLength(32); - newEntryBox.setTextColor(0xFFFFFF); - newEntryBox.visible = expanded; - newEntryBox.setResponder(x -> { - updateEditorSubwidgets(newEntryBox); - }); - addRenderableWidget(newEntryBox); - - newEntryPlatformBox = new DLEditBox(minecraft.font, pX + 134, pY + 30, 25, 12, TextUtils.empty()); - newEntryPlatformBox.setBordered(false); - newEntryPlatformBox.setMaxLength(10); - newEntryPlatformBox.setTextColor(0xFFFFFF); - newEntryPlatformBox.visible = expanded; - addRenderableWidget(newEntryPlatformBox); - - editAliasPlatform = new DLEditBox(minecraft.font, pX + 134, 0, 33, 14, TextUtils.empty()); - editAliasPlatform.setBordered(true); - editAliasPlatform.setMaxLength(10); - editAliasPlatform.setTextColor(0xFFFFFF); - editAliasPlatform.visible = false; - editAliasPlatform.withOnFocusChanged((box, focus) -> { - if (!focus) { - if (selectedStationName != null && !selectedStationName.isBlank()) { - alias.updateInfoForStation(selectedStationName, new StationInfo(box.getValue())); - alias.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateAlias(alias.getAliasName(), alias, () -> { - onUpdate.run(); - initStationDeleteButtons(); - }); - } - box.visible = false; - selectedStationName = null; - box.setValue(""); - } - }); - addRenderableWidget(editAliasPlatform); - - setYPos(pY); - } - - public TrainStationAlias getAlias() { - return alias; - } - - public boolean isExpanded() { - return expanded; - } - - @Override - public void setYPos(int y) { - this.y = y; - deleteButton = new GuiAreaDefinition(x + 165, y + 6, 16, 16); - expandButton = new GuiAreaDefinition(x + 182, y + 6, 16, 16); - addButton = new GuiAreaDefinition(x + 165, y + 26 + (alias.getAllStationNames().size() * STATION_ENTRY_HEIGHT) + 2, 16, 16); - titleBox.y = y + 10; - initStationDeleteButtons(); - } - - private void initStationDeleteButtons() { - removeStationButtons.clear(); - stationInfoAreas.clear(); - stationNameAreas.clear(); - - String[] names = alias.getAllStationNames().toArray(String[]::new); - for (int i = 0; i < names.length; i++) { - String name = names[i]; - removeStationButtons.put(name, new GuiAreaDefinition(x + 165, y + 26 + (i * STATION_ENTRY_HEIGHT) + 2, 16, 16)); - stationNameAreas.put(name, new GuiAreaDefinition(x + 25, y + 26 + (i * STATION_ENTRY_HEIGHT) + 2, 104, 16)); - stationInfoAreas.put(name, new GuiAreaDefinition(x + 129, y + 26 + (i * STATION_ENTRY_HEIGHT) + 2, 35, 16)); - } - - titleBarArea = new GuiAreaDefinition(x + 25, y + 6, 129, 16); - } - - @Override - public void tick() { - super.tick(); - - if (destinationSuggestions != null) { - destinationSuggestions.tick(); - - if (!newEntryBox.canConsumeInput()) { - clearSuggestions(); - } - } - } - - private void toggleExpanded() { - this.expanded = !expanded; - titleBox.visible = expanded; - newEntryBox.visible = expanded; - newEntryPlatformBox.visible = expanded; - } - - private void deleteAlias() { - GlobalSettingsManager.getInstance().getSettingsData().unregisterAlias(alias, onUpdate); - } - - private void addStation(String name, StationInfo info) { - AliasName prevName = alias.getAliasName(); - if (ClientTrainStationSnapshot.getInstance().getAllTrainStations().stream().noneMatch(x -> x.equals(name)) || newEntryPlatformBox.getValue().isBlank()) { - return; - } - - alias.add(name, info); - alias.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateAlias(prevName, alias, () -> { - onUpdate.run(); - initStationDeleteButtons(); - }); - - newEntryBox.setValue(""); - newEntryBox.setFocus(false); - newEntryPlatformBox.setValue(""); - newEntryPlatformBox.setFocus(false); - } - - private boolean setAliasName(String name) { - AliasName prevName = alias.getAliasName(); - - if (name == null || name.isBlank()) { - return false; - } - - if (GlobalSettingsManager.getInstance().getSettingsData().getAliasList().stream().anyMatch(x -> x.getAliasName().get().toLowerCase().equals(name.toLowerCase()))) { - return false; - } - - alias.setName(AliasName.of(name)); - alias.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateAlias(prevName, alias, onUpdate); - return true; - } - - - @Override - public int getHeight() { - return height; - } - - private void removeStation(String name) { - AliasName prevName = alias.getAliasName(); - alias.remove(name); - alias.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateAlias(prevName, alias, () -> { - onUpdate.run(); - initStationDeleteButtons(); - }); - initStationDeleteButtons(); - onUpdate.run(); - } - - @Override - public int calcHeight() { - if (expanded) { - height = STATION_ENTRY_HEIGHT * alias.getAllStationNames().size() + 50; - } else { - height = HEIGHT; - } - return height; - } - - private void editStationInfo(String stationName, GuiAreaDefinition buttonArea) { - //parent.unfocusAllWidgets(); - //parent.unfocusAllEntries(); - - selectedStationName = stationName; - editAliasPlatform.setValue(alias.getInfoForStation(selectedStationName).platform()); - editAliasPlatform.x = buttonArea.getLeft() + 1; - editAliasPlatform.y = buttonArea.getTop() + 1; - editAliasPlatform.visible = true; - editAliasPlatform.setFocus(true); - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x, y, 0, 0, WIDTH, HEIGHT); - - GuiUtils.drawTexture(GUI_WIDGETS, graphics, deleteButton.getX(), deleteButton.getY(), 232, 0, 16, 16); // delete button - GuiUtils.drawTexture(GUI_WIDGETS, graphics, expandButton.getX(), expandButton.getY(), expanded ? 216 : 200, 0, 16, 16); // expand button - - if (expanded) { - Map names = alias.getAllStations(); - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x + 25, y + 5, 0, 92, 139, 18); // textbox - newEntryBox.y = y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 6; - newEntryPlatformBox.y = y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 6; - - for (int i = 0; i < names.size(); i++) { - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x, y + 26 + (i * STATION_ENTRY_HEIGHT), 0, 48, 200, STATION_ENTRY_HEIGHT); - } - - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x, y + 26 + (names.size() * STATION_ENTRY_HEIGHT), 0, 68, 200, 24); - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x + 25, y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 1, 0, 92, 103, 18); // textbox - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x + 25 + 102, y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 1, 138, 92, 1, 18); // textbox - - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x + 129, y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 1, 0, 92, 35, 18); // textbox - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x + 129 + 34, y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 1, 138, 92, 1, 18); // textbox - GuiUtils.drawTexture(GUI_WIDGETS, graphics, addButton.getX(), addButton.getY(), 200, 16, 16, 16); // add button - - for (GuiAreaDefinition def : removeStationButtons.values()) { - GuiUtils.drawTexture(GUI_WIDGETS, graphics, def.getX(), def.getY(), 232, 0, 16, 16); // delete button - } - - int i = 0; - for (Entry entry : names.entrySet()) { - MutableComponent name = TextUtils.text(entry.getKey()); - int maxTextWidth = 104 - 12; - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, x + 30, y + 26 + (i * STATION_ENTRY_HEIGHT) + 6, name, 0xFFFFFF, EAlignment.LEFT, false); - - StationInfo info = entry.getValue(); - MutableComponent platform = TextUtils.text(info.platform()); - int maxPlatformWidth = 35 - 7; - if (shadowlessFont.width(platform) > maxPlatformWidth) { - platform = TextUtils.text(shadowlessFont.substrByWidth(platform, maxPlatformWidth - 3).getString()).append(Constants.ELLIPSIS_STRING); - } - int platformTextWidth = shadowlessFont.width(platform); - GuiUtils.drawString(graphics, shadowlessFont, x + 30 + 130 - platformTextWidth, y + 26 + (i * STATION_ENTRY_HEIGHT) + 6, platform, 0xFFFFFF, EAlignment.LEFT, false); - i++; - } - - } else { - MutableComponent name = TextUtils.text(alias.getAliasName().get()); - int maxTextWidth = 129; - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, x + 30, y + 10, name, 0xFFFFFF, EAlignment.LEFT, false); - - graphics.poseStack().scale(0.75f, 0.75f, 0.75f); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 5) / 0.75f), (int)((y + 30) / 0.75f), TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.summary", alias.getAllStationNames().size()), 0xDBDBDB, EAlignment.LEFT, false); - if (alias.getLastEditorName() != null && !alias.getLastEditorName().isBlank()) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 5) / 0.75f), (int)((y + 38) / 0.75f), TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".alias_settings.editor", alias.getLastEditorName(), alias.getLastEditedTimeFormatted()), 0xDBDBDB, EAlignment.LEFT, false); - } - float s = 1 / 0.75f; - graphics.poseStack().scale(s, s, s); - } - - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - - // Button highlight - if (deleteButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, deleteButton.getX(), deleteButton.getY(), deleteButton.getWidth(), deleteButton.getHeight(), 0x1AFFFFFF); - } else if (expandButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, expandButton.getX(), expandButton.getY(), expandButton.getWidth(), expandButton.getHeight(), 0x1AFFFFFF); - } else if (expanded && addButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, addButton.getX(), addButton.getY(), addButton.getWidth(), addButton.getHeight(), 0x1AFFFFFF); - } else if (expanded && removeStationButtons.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY))) { - for (Entry entry : removeStationButtons.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, entry.getValue().getX(), entry.getValue().getY(), entry.getValue().getWidth(), entry.getValue().getHeight(), 0x1AFFFFFF); - } - } - } else if (expanded && stationInfoAreas.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY))) { - for (Entry entry : stationInfoAreas.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, entry.getValue().getX(), entry.getValue().getY(), entry.getValue().getWidth(), entry.getValue().getHeight(), 0x1AFFFFFF); - } - } - } - } - - @Override - public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { - GuiUtils.renderTooltipWithOffset(parent, newEntryBox, List.of(tooltipStationName), width / 2, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - GuiUtils.renderTooltipWithOffset(parent, newEntryPlatformBox, List.of(tooltipPlatform), width / 2, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - GuiUtils.renderTooltipWithOffset(parent, deleteButton, List.of(tooltipDeleteAlias), width / 2, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - GuiUtils.renderTooltipWithOffset(parent, expandButton, List.of(expanded ? Constants.TOOLTIP_COLLAPSE : Constants.TOOLTIP_EXPAND), width / 2, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - - if (expanded) { - GuiUtils.renderTooltipWithOffset(parent, addButton, List.of(tooltipAddStation), width / 2, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - for (Entry entry : removeStationButtons.entrySet()) { - if (GuiUtils.renderTooltipWithOffset(parent, entry.getValue(), List.of(tooltipDeleteStation), width / 2, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks))) { - break; - } - } - - for (Entry entry : stationNameAreas.entrySet()) { - if (shadowlessFont.width(entry.getKey()) > 104 - 12 && GuiUtils.renderTooltipAt(parent, entry.getValue(), List.of(TextUtils.text(entry.getKey())), width, graphics, entry.getValue().getLeft() + 1, entry.getValue().getTop() - parent.getScrollOffset(partialTicks), mouseX, mouseY, 0, parent.getScrollOffset(partialTicks))) { - break; - } - } - - for (Entry entry : stationInfoAreas.entrySet()) { - MutableComponent text = TextUtils.text(alias.getInfoForStation(entry.getKey()).platform()); - if ((selectedStationName == null || !entry.getKey().equals(selectedStationName)) && shadowlessFont.width(text) > 35 - 7 && GuiUtils.renderTooltipAt(parent, entry.getValue(), List.of(text), width, graphics, entry.getValue().getLeft(), entry.getValue().getTop() - parent.getScrollOffset(partialTicks), mouseX, mouseY, 0, parent.getScrollOffset(partialTicks))) { - break; - } - } - } else { - if (titleBarArea.isInBounds(mouseX, mouseY + parent.getScrollOffset(partialTicks)) && shadowlessFont.width(alias.getAliasName().get()) > 129) { - GuiUtils.renderTooltipAt(parent, titleBarArea, List.of(TextUtils.text(alias.getAliasName().get())), width, graphics, titleBarArea.getLeft() + 1, titleBarArea.getTop() - parent.getScrollOffset(partialTicks), mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - } - } - } - - @Override - public void renderSuggestions(PoseStack matrixStack, int mouseX, int mouseY, float partialTicks) { - if (destinationSuggestions != null) { - matrixStack.pushPose(); - matrixStack.translate(0, -parent.getScrollOffset(partialTicks), 500); - destinationSuggestions.render(matrixStack, mouseX, mouseY + parent.getScrollOffset(partialTicks)); - matrixStack.popPose(); - } - } - - @Override - public boolean mouseClickedLoop(double pMouseX, double pMouseY, int pButton) { - //parent.unfocusAllEntries(); - - if (destinationSuggestions != null && destinationSuggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton)) - return super.mouseClicked(pMouseX, pMouseY, pButton); - - return false; - } - - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - - if (expanded && stationInfoAreas.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY))) { - for (Entry entry : stationInfoAreas.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY)) { - editStationInfo(entry.getKey(), entry.getValue()); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - } - } - - editAliasPlatform.setFocus(false); - - if (deleteButton.isInBounds(pMouseX, pMouseY)) { - deleteAlias(); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (expandButton.isInBounds(pMouseX, pMouseY)) { - toggleExpanded(); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (expanded && addButton.isInBounds(pMouseX, pMouseY)) { - addStation(newEntryBox.getValue(), new StationInfo(newEntryPlatformBox.getValue())); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (expanded && removeStationButtons.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY))) { - for (Entry entry : removeStationButtons.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY)) { - removeStation(entry.getKey()); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - } - } - - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - @Override - public boolean mouseScrolledLoop(double pMouseX, double pMouseY, double pDelta) { - if (destinationSuggestions != null && destinationSuggestions.mouseScrolled(pMouseX, pMouseY, MathUtils.clamp(pDelta, -1.0D, 1.0D))) - return true; - - return false; - } - - private void clearSuggestions() { - if (destinationSuggestions != null) { - destinationSuggestions.getEditBox().setSuggestion(""); - } - destinationSuggestions = null; - } - - - protected void updateEditorSubwidgets(DLEditBox field) { - clearSuggestions(); - - destinationSuggestions = new ModStationSuggestions(minecraft, parent, field, minecraft.font, getViableStations(field), field.getHeight() + 2 + field.y); - destinationSuggestions.setAllowSuggestions(true); - destinationSuggestions.updateCommandInfo(); - } - - private List getViableStations(DLEditBox field) { - return ClientTrainStationSnapshot.getInstance().getAllTrainStations().stream() - .distinct() - .filter(x -> !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(x) && !alias.contains(x)) - .sorted((a, b) -> a.compareTo(b)) - .toList(); - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.HOVERED; - } - - @Override - public void updateNarration(NarrationElementOutput narrationElementOutput) { - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java new file mode 100644 index 00000000..277dbf12 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java @@ -0,0 +1,87 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.function.Function; + +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class CRNListBox extends ScrollableWidgetContainer { + + private final Screen parent; + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + private final Map values = new HashMap<>(); + + public CRNListBox(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.parent = parent; + this.scrollBar = scrollBar; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public Screen getParent() { + return parent; + } + + public void displayData(List data, Function createItem) { + clearWidgets(); + values.clear(); + contentHeight = 0; + for (int i = 0; i < data.size(); i++) { + T entry = data.get(i); + W widget = createItem.apply(entry); + widget.set_x(x()); + widget.set_width(width()); + widget.set_y(y() + contentHeight); + addRenderableWidget(widget); + values.put(widget, entry); + contentHeight += widget.height(); + } + scrollBar.updateMaxScroll(contentHeight); + } + + public Set> getEntries() { + return values.entrySet(); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + if (scrollBar.getScrollValue() > 0) { + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + } + if (scrollBar.getScrollValue() < scrollBar.getMaxScroll()) { + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + } + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java new file mode 100644 index 00000000..94ac0b66 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java @@ -0,0 +1,109 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLColorPickerScreen; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class ColorPickerWidget extends WidgetContainer { + + private int selectedColor = 0; + + public ColorPickerWidget(Screen parent, int px, int py, int[] sampleColors, int maxColorsPerLine, int preselectedColor, Consumer onPick) { + super(px, py, 1, 1); + this.selectedColor = preselectedColor; + + int lines = (int)Math.ceil((double)sampleColors.length / (double)maxColorsPerLine); + set_height(18 * 2 + lines * 13 - 1); + set_width(maxColorsPerLine * 13 - 1); + addRenderableWidget(new ColorBrowserButton(x(), y(), width(), () -> selectedColor, (btn) -> { + DLScreen.setScreen(new DLColorPickerScreen(parent, selectedColor, c -> { + selectedColor = c.toInt(); + onPick.accept(this); + }, true)); + })); + for (int i = 0, y = 0; y < lines && i < sampleColors.length; y++) { + for (int x = 0; x < maxColorsPerLine && i < sampleColors.length; x++, i++) { + final int j = i; + addRenderableWidget(new ColorButton(x() + x * 13, y() + 18 + y * 13, sampleColors[j], btn -> { + selectedColor = sampleColors[j]; + onPick.accept(this); + })); + } + } + addRenderableWidget(new NoColorButton(x(), y() + height() - 16, width(), (btn) -> { + selectedColor = 0; + onPick.accept(this); + })); + } + + public int getSelectedColor() { + return selectedColor; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + + + private static class ColorBrowserButton extends DLButton { + private final Supplier color; + public ColorBrowserButton(int pX, int pY, int pWidth, Supplier color, Consumer pOnPress) { + super(pX, pY, pWidth, 16, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".color_picker.custom"), pOnPress); + this.color = color; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + GuiUtils.drawBox(graphics, GuiAreaDefinition.of(this), color.get(), isMouseSelected() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED); + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2 - font.lineHeight / 2, getMessage(), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.CENTER, true); + } + } + + private static class ColorButton extends DLButton { + private final int color; + public ColorButton(int pX, int pY, int color, Consumer pOnPress) { + super(pX, pY, 12, 12, TextUtils.empty(), pOnPress); + this.color = color; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + GuiUtils.drawBox(graphics, GuiAreaDefinition.of(this), color, isMouseSelected() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED); + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2 - font.lineHeight / 2, getMessage(), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.CENTER, true); + } + } + + private static class NoColorButton extends DLButton { + public NoColorButton(int pX, int pY, int pWidth, Consumer pOnPress) { + super(pX, pY, pWidth, 16, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".color_picker.no_color"), pOnPress); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + GuiUtils.drawBox(graphics, GuiAreaDefinition.of(this), 0, isMouseSelected() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED); + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2 - font.lineHeight / 2, getMessage(), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.CENTER, true); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateIconButton.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateIconButton.java index 4aac7a96..b7cf048e 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateIconButton.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateIconButton.java @@ -1,5 +1,10 @@ package de.mrjulsen.crn.client.gui.widgets; +import javax.annotation.Nonnull; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.foundation.gui.AllGuiTextures; import com.simibubi.create.foundation.gui.element.ScreenElement; import com.simibubi.create.foundation.gui.widget.IconButton; @@ -18,6 +23,20 @@ public DLCreateIconButton(int x, int y, ScreenElement icon) { super(x, y, icon); } + @Override + public void renderButton(@Nonnull PoseStack matrixStack, int mouseX, int mouseY, float partialTicks) { + if (visible) { + isHovered = mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height; + + AllGuiTextures button = !isActive() ? AllGuiTextures.BUTTON_DOWN + : isMouseSelected() ? AllGuiTextures.BUTTON_HOVER : AllGuiTextures.BUTTON; + + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + drawBg(matrixStack, button); + icon.render(matrixStack, x + 1, y + 1); + } + } + @Override public void onFocusChangeEvent(boolean focus) {} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateLabel.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateLabel.java index db758ce3..952896b8 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateLabel.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateLabel.java @@ -33,7 +33,7 @@ public boolean isMouseSelected() { @Override public void setMouseSelected(boolean selected) { this.mouseSelected = selected; - } + } @Override public int x() { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java index 8a5718fe..a9fd0c27 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java @@ -32,7 +32,7 @@ public boolean isMouseSelected() { @Override public void setMouseSelected(boolean selected) { this.mouseSelected = selected; - } + } @Override public int x() { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ExpandButton.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ExpandButton.java deleted file mode 100644 index 2518f7b5..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ExpandButton.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import java.util.function.Consumer; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.ChatFormatting; - -public class ExpandButton extends DLButton { - - public static final int WIDTH = 200; - public static final int HEIGHT = 48; - - // Data - private boolean expanded; - - private static final String expandText = "▼ " + TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.expand").getString(); - private static final String collapseText = "▲ " + TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.collapse").getString(); - - - public ExpandButton(int pX, int pY, boolean initialState, Consumer onClick) { - super(pX, pY, 20, 20, TextUtils.empty(), onClick); - this.expanded = initialState; - - int w1 = font.width(expandText) + 10; - int w2 = font.width(collapseText) + 10; - int h = font.lineHeight + 6; - - width = w1 > w2 ? w1 : w2; - height = h; - } - - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - if (isMouseOver(pMouseX, pMouseY)) { - expanded = !expanded; - } - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - if (isMouseOver(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, x, y, width, height, 0x1AFFFFFF); - GuiUtils.drawString(graphics, font, x + width / 2, y + height / 2 - font.lineHeight / 2, expanded ? TextUtils.text(collapseText).withStyle(ChatFormatting.UNDERLINE) : TextUtils.text(expandText).withStyle(ChatFormatting.UNDERLINE), 0xFFFFFF, EAlignment.CENTER, true); - } else { - GuiUtils.drawString(graphics, font, x + width / 2, y + height / 2 - font.lineHeight / 2, expanded ? TextUtils.text(collapseText) : TextUtils.text(expandText), 0xFFFFFF, EAlignment.CENTER, true); - } - } - - public boolean isExpanded() { - return expanded; - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/FlatCheckBox.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/FlatCheckBox.java new file mode 100644 index 00000000..3eb558a3 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/FlatCheckBox.java @@ -0,0 +1,34 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.function.Consumer; + +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLCheckBox; +import de.mrjulsen.mcdragonlib.client.render.GuiIcons; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; + +public class FlatCheckBox extends DLCheckBox { + + public FlatCheckBox(int pX, int pY, int pWidth, String pMessage, boolean checked, Consumer onCheckedChanged) { + super(pX, pY, pWidth, pMessage, checked, onCheckedChanged); + set_height(GuiIcons.ICON_SIZE); + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + if (isChecked()) { + GuiIcons.CHECKMARK.render(graphics, x(), y()); + } + GuiUtils.drawString(graphics, font, x() + GuiIcons.ICON_SIZE + 2, y() + height() / 2 - font.lineHeight / 2, getMessage(), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + + if (isMouseSelected()) { + GuiUtils.fill(graphics, x(), y(), width(), height(), 0x44FFFFFF); + } + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IEntryListSettingsOption.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IEntryListSettingsOption.java deleted file mode 100644 index ccc91eab..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IEntryListSettingsOption.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import com.mojang.blaze3d.vertex.PoseStack; - -public interface IEntryListSettingsOption { - void setYPos(int y); - void tick(); - int calcHeight(); - void renderSuggestions(PoseStack poseStack, int mouseX, int mouseY, float partialTicks); - /** - * This method is always called, even if the used clicked outside the working area of the container window. - * This additional method should fix the usage of the suggestions popup, which can be rendered outside of the working area. - * @param pMouseX - * @param pMouseY - * @param pButton - * @return - */ - boolean mouseClickedLoop(double pMouseX, double pMouseY, int pButton); - - /** - * This method is always called, even if the used scrolled outside the working area of the container window. - * This additional method should fix the usage of the suggestions popup, which can be rendered outside of the working area. - * @param pMouseX - * @param pMouseY - * @param pDelta - * @return - */ - boolean mouseScrolledLoop(double pMouseX, double pMouseY, double pDelta); -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModDestinationSuggestions.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModDestinationSuggestions.java index f1df5aef..d9ed0959 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModDestinationSuggestions.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModDestinationSuggestions.java @@ -6,7 +6,7 @@ import com.mojang.brigadier.context.StringRange; import com.mojang.brigadier.suggestion.Suggestion; -import de.mrjulsen.crn.data.TrainStationAlias; +import de.mrjulsen.crn.data.StationTag; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.components.EditBox; @@ -15,7 +15,7 @@ public class ModDestinationSuggestions extends ModCommandSuggestions { - private List viableStations; + private List viableStations; private String previous = "<>"; private Font font; private boolean active; @@ -23,7 +23,7 @@ public class ModDestinationSuggestions extends ModCommandSuggestions { private List currentSuggestions; private int yOffset; - public ModDestinationSuggestions(Minecraft pMinecraft, Screen pScreen, EditBox pInput, Font pFont, List viableStations, int yOffset) { + public ModDestinationSuggestions(Minecraft pMinecraft, Screen pScreen, EditBox pInput, Font pFont, List viableStations, int yOffset) { super(pMinecraft, pScreen, pInput, pFont, true, true, 0, 7, false, 0xee_303030); this.font = pFont; this.viableStations = viableStations; @@ -53,8 +53,8 @@ public void updateCommandInfo() { previous = value; currentSuggestions = viableStations.stream() - .filter(ia -> !ia.getAliasName().get().equals(value) && ia.getAliasName().get().toLowerCase().startsWith(value.toLowerCase())) - .map(s -> new Suggestion(new StringRange(0, s.getAliasName().get().length()), s.getAliasName().get())) + .filter(ia -> !ia.getTagName().get().equals(value) && ia.getTagName().get().toLowerCase().startsWith(value.toLowerCase())) + .map(s -> new Suggestion(new StringRange(0, s.getTagName().get().length()), s.getTagName().get())) .toList(); showSuggestions(false); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModernVerticalScrollBar.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModernVerticalScrollBar.java new file mode 100644 index 00000000..31816928 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ModernVerticalScrollBar.java @@ -0,0 +1,46 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.mcdragonlib.client.ITickable; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLVerticalScrollBar; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import net.minecraft.client.gui.screens.Screen; + +public class ModernVerticalScrollBar extends DLVerticalScrollBar implements ITickable { + + private final Screen parent; + + public ModernVerticalScrollBar(Screen parent, int x, int y, int h, GuiAreaDefinition scrollArea) { + super(x, y, 5, h, null); + this.parent = parent; + setAutoScrollerSize(true); + set_width(5); + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + // Render background + if (isMouseSelected() || (parent.isDragging() && isScrolling)) { + GuiUtils.fill(graphics, x(), y(), width(), height(), 0xFF444444); + } + + // Render scrollbar + int x1 = x + (isMouseSelected() || (parent.isDragging() && isScrolling) ? 0 : 2); + int y1 = y + (int)(scrollPercentage * (height - scrollerSize)); + int w = isMouseSelected() || (parent.isDragging() && isScrolling) ? width : 1; + int h = scrollerSize; + + if (canScroll()) { + GuiUtils.fill(graphics, x1, y1 + (isMouseSelected() || (parent.isDragging() && isScrolling) ? 0 : 2), w, h - (isMouseSelected() || (parent.isDragging() && isScrolling) ? 0 : 4), 0x88FFFFFF); + } + } + + @Override + public void tick() { + if (!parent.isDragging()) { + isScrolling = false; + } + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ResizableButton.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ResizableButton.java new file mode 100644 index 00000000..8f35fc30 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ResizableButton.java @@ -0,0 +1,35 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; + +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; + +public class ResizableButton extends Button { + + public ResizableButton(int x, int y, int width, int height, Component message, OnPress onPress, OnTooltip onTooltip) { + super(x, y, width, height, message, onPress, onTooltip); + } + + public ResizableButton(int x, int y, int width, int height, Component message, OnPress onPress) { + super(x, y, width, height, message, onPress, NO_TOOLTIP); + } + + @SuppressWarnings("resource") + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, GuiAreaDefinition.of(this), AreaStyle.NATIVE, isActive() ? (isHoveredOrFocused() ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + int j = isActive() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; + GuiUtils.drawString(graphics, Minecraft.getInstance().font, this.x + this.width / 2, this.y + (this.height - 8) / 2, this.getMessage(), j, EAlignment.CENTER, true); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java new file mode 100644 index 00000000..0bef955b --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java @@ -0,0 +1,146 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Predicate; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.widgets.routedetails.RouteDetailsTransferWidget; +import de.mrjulsen.crn.client.gui.widgets.routedetails.RoutePartWidget; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.crn.data.navigation.RoutePart; +import de.mrjulsen.crn.data.navigation.TransferConnection; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class RouteDetailsViewer extends ScrollableWidgetContainer { + + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + private Set expandedParts = new HashSet<>(); + private boolean canExpandCollapse = true; + private boolean showTrainDetails = true; + private boolean initialExpanded = false; + private boolean showJourney = false; + + private final Screen parent; + + public RouteDetailsViewer(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.scrollBar = scrollBar; + this.parent = parent; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public void displayRoute(ClientRoute route) { + displayRouteInternal(route, route.getClientParts(), true); + } + + public void displayPart(ClientRoute route, Predicate verifiedSelector) { + displayRouteInternal(route, route.getClientParts().stream().filter(verifiedSelector).toList(), false); + } + + public void displayRouteInternal(ClientRoute route, List parts, boolean displayConnections) { + clearWidgets(); + contentHeight = 10; + Queue connections = new ConcurrentLinkedQueue<>(route.getConnections()); + for (int i = 0; i < parts.size(); i++) { + ClientRoutePart part = parts.get(i); + RoutePartWidget widget = new RoutePartWidget(parent, x(), y() + contentHeight, width(), route, part); + widget.setShowTrainDetails(showTrainDetails); + widget.setCanExpandCollapse(canExpandCollapse); + widget.setShowJourney(showJourney); + widget.setExpanded(expandedParts.contains(part) || initialExpanded); + widget.withOnGuiChangedEvent((w) -> { + if (w.isExpanded()) expandedParts.add(part); else expandedParts.remove(part); + displayRoute(route); + }); + addRenderableWidget(widget); + contentHeight += widget.height(); + + if (!connections.isEmpty() && displayConnections) { + RouteDetailsTransferWidget transfer = addRenderableOnly(new RouteDetailsTransferWidget(x(), y() + contentHeight, width(), connections.poll())); + contentHeight += transfer.height(); + } + } + + contentHeight += 10; + scrollBar.updateMaxScroll(contentHeight); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + + //DLUtils.doIfNotNull(route, r -> GuiUtils.drawString(graphics, font, x(), y(), r.getState().name() + ", Running: " + !r.isClosed(), 0xFFFF0000, EAlignment.LEFT, false)); + } + + @Override + public void renderMainLayerScrolled(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x(), y(), 22, 10, 0, 179, 22, 1, 256, 256); + GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x(), y() + contentHeight - 10, 22, Math.max(10, height() - contentHeight + 10), 0, 179, 22, 1, 256, 256); + super.renderMainLayerScrolled(graphics, mouseX, mouseY, partialTicks); + } + + public boolean canExpandCollapse() { + return canExpandCollapse; + } + + public void setCanExpandCollapse(boolean canExpandCollapse) { + this.canExpandCollapse = canExpandCollapse; + } + + public boolean showTrainDetails() { + return showTrainDetails; + } + + public void setShowTrainDetails(boolean showTrainDetails) { + this.showTrainDetails = showTrainDetails; + } + + public boolean isInitialExpanded() { + return initialExpanded; + } + + public void setInitialExpanded(boolean b) { + this.initialExpanded = b; + } + + public boolean isShowingJourney() { + return showJourney; + } + + public void setShowJourney(boolean b) { + this.showJourney = b; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteEntryOverviewWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteEntryOverviewWidget.java deleted file mode 100644 index 3a966afa..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteEntryOverviewWidget.java +++ /dev/null @@ -1,141 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import java.util.function.Consumer; - -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; -import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; -import de.mrjulsen.crn.client.gui.screen.NavigatorScreen; -import de.mrjulsen.crn.client.gui.screen.RouteDetailsScreen; -import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.SimpleRoute.SimpleRoutePart; -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; -import de.mrjulsen.crn.event.listeners.JourneyListener.State; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.ColorUtils; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.world.level.Level; - -public class RouteEntryOverviewWidget extends DLButton { - - public static final int WIDTH = 200; - public static final int HEIGHT = 54; - - private static final int DISPLAY_WIDTH = 190; - - private final SimpleRoute route; - private final NavigatorScreen parent; - private final Level level; - private final long lastRefreshedTime; - - private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); - private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); - private final MutableComponent trainCanceled = ELanguage.translate("gui.createrailwaysnavigator.route_overview.stop_canceled"); - - public RouteEntryOverviewWidget(NavigatorScreen parent, Level level, long lastRefreshedTime, int pX, int pY, SimpleRoute route, Consumer onClick) { - super(pX, pY, WIDTH, HEIGHT, TextUtils.text(route.getName()), onClick); // 48 - this.route = route; - this.parent = parent; - this.level = level; - this.lastRefreshedTime = lastRefreshedTime; - } - - - @Override - public void onClick(double pMouseX, double pMouseY) { - super.onClick(pMouseX, pMouseY); - Minecraft minecraft = Minecraft.getInstance(); - minecraft.setScreen(new RouteDetailsScreen(parent, level, route, route.getListenerId())); - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - - final float scale = 0.75f; - float l = isMouseOver(pMouseX, pMouseY) ? 0.1f : 0; - boolean isActive = JourneyListenerManager.getInstance().get(route.getListenerId(), null) != null; - boolean beforeJourney = isActive && JourneyListenerManager.getInstance().get(route.getListenerId(), null).getCurrentState() == State.BEFORE_JOURNEY; - - int color = ColorUtils.lightenColor(ColorShade.DARK.getColor(), l); - if (!beforeJourney) { - color = ColorUtils.applyTint(color, 0x663300); - } - CreateDynamicWidgets.renderSingleShadeWidget(graphics, x, y, WIDTH, HEIGHT, color); - CreateDynamicWidgets.renderHorizontalSeparator(graphics, x + 6, y + 22, 188); - - Minecraft minecraft = Minecraft.getInstance(); - SimpleRoutePart[] parts = route.getParts().toArray(SimpleRoutePart[]::new); - Font shadowlessFont = new NoShadowFontWrapper(minecraft.font); - - String timeStart = TimeUtils.parseTime((int)((lastRefreshedTime + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY) + route.getStartStation().getTicks(), ModClientConfig.TIME_FORMAT.get()); - String timeEnd = TimeUtils.parseTime((int)((lastRefreshedTime + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY) + route.getEndStation().getTicks(), ModClientConfig.TIME_FORMAT.get()); - String dash = " - "; - MutableComponent line = TextUtils.text(String.format("%s%s%s | %s %s | %s", - timeStart, - dash, - timeEnd, - route.getTransferCount(), - transferText.getString(), - TimeUtils.parseDurationShort(route.getTotalDuration()) - )); - - if (!route.isValid()) { - line = line.withStyle(ChatFormatting.RED).withStyle(ChatFormatting.STRIKETHROUGH); - } - - float localScale = shadowlessFont.width(line) > WIDTH - 12 ? scale : 1; - graphics.poseStack().pushPose(); - graphics.poseStack().scale(localScale, 1, 1); - GuiUtils.drawString(graphics, minecraft.font, (int)((x + 6) / localScale), y + 5, line, 0xFFFFFF, EAlignment.LEFT, false); - graphics.poseStack().popPose(); - - int routePartWidth = DISPLAY_WIDTH / parts.length; - String end = route.getEndStation().getStationName(); - int textW = shadowlessFont.width(end); - - for (int i = 0; i < parts.length; i++) { - GuiUtils.fill(graphics, x + 5 + (i * routePartWidth) + 1, y + 27, routePartWidth - 2, 11, 0xFF393939); - } - - graphics.poseStack().pushPose(); - graphics.poseStack().scale(scale, scale, scale); - - if (route.getStartStation().shouldRenderRealtime()) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 6 + shadowlessFont.width(timeStart) * localScale / 2.0f) / scale) - shadowlessFont.width(timeStart) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)(route.getStartStation().getEstimatedTimeWithThreshold() % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), route.getStartStation().isDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); - } - if (route.getEndStation().shouldRenderRealtime()) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 6 + shadowlessFont.width(timeEnd) * localScale * 1.5f + (shadowlessFont.width(dash)) * localScale) / scale) - shadowlessFont.width(timeEnd) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)(route.getEndStation().getEstimatedTimeWithThreshold() % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), route.getEndStation().isDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); - } - - if (!route.isValid()) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + WIDTH - 5) / scale) - shadowlessFont.width(trainCanceled), (int)((y + 15) / scale), trainCanceled, Constants.COLOR_DELAYED, EAlignment.LEFT, false); - } else if (!beforeJourney) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + WIDTH - 5) / scale) - shadowlessFont.width(connectionInPast), (int)((y + 15) / scale), connectionInPast, Constants.COLOR_DELAYED, EAlignment.LEFT, false); - } - - - for (int i = 0; i < parts.length; i++) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 5 + (i * routePartWidth) + (routePartWidth / 2)) / 0.75f), (int)((y + 30) / 0.75f), TextUtils.text(parts[i].getTrainName()), 0xFFFFFF, EAlignment.CENTER, false); - } - - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 6) / scale), (int)((y + 43) / scale), TextUtils.text(route.getStartStation().getStationName()), 0xDBDBDB, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + WIDTH - 6) / scale) - textW, (int)((y + 43) / scale), TextUtils.text(end), 0xDBDBDB, EAlignment.LEFT, false); - - graphics.poseStack().popPose(); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java new file mode 100644 index 00000000..0c13edad --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java @@ -0,0 +1,72 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.List; + +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class RouteViewer extends ScrollableWidgetContainer { + + private final Screen parent; + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + + public RouteViewer(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.parent = parent; + this.scrollBar = scrollBar; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public Screen getParent() { + return parent; + } + + public void displayRoutes(List routes) { + clearWidgets(); + contentHeight = 5; + for (int i = 0; i < routes.size(); i++) { + RouteWidget widget = new RouteWidget(this, routes.get(i), x + 10, y + contentHeight); + addRenderableWidget(widget); + contentHeight += (RouteWidget.HEIGHT + 3); + } + contentHeight += 2; + scrollBar.updateMaxScroll(contentHeight); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + + public void clear() { + clearWidgets(); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java new file mode 100644 index 00000000..8ba80431 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java @@ -0,0 +1,146 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import com.google.common.collect.ImmutableList; +import com.simibubi.create.content.trains.station.NoShadowFontWrapper; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ModGuiUtils; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.screen.RouteDetailsScreen; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.SavedRoutesManager; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.RoutePart; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenu; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem.ContextMenuItemData; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class RouteWidget extends DLButton { + + public static final int WIDTH = 214; + public static final int HEIGHT = 54; + + private static final int DISPLAY_WIDTH = WIDTH - 10; + + private final ClientRoute route; + + private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); + private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); + private final MutableComponent trainCanceled = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); + private final MutableComponent textShowDetails = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.show_details"); + private final MutableComponent textSave = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.save"); + private final MutableComponent textShare = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.share"); + private final MutableComponent textRemove = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.remove"); + + public RouteWidget(RouteViewer parent, ClientRoute route, int x, int y) { + super(x, y, WIDTH, HEIGHT, TextUtils.empty(), (b) -> Minecraft.getInstance().setScreen(new RouteDetailsScreen(parent.getParent(), route))); + this.route = route; + + setRenderStyle(AreaStyle.FLAT); + setMenu(new DLContextMenu(() -> GuiAreaDefinition.of(this), () -> new DLContextMenuItem.Builder() + .add(new ContextMenuItemData(textShowDetails, Sprite.empty(), true, (b) -> onPress.onPress(b), null)) + .addSeparator() + .add(new ContextMenuItemData(SavedRoutesManager.isSaved(route) ? textRemove : textSave, Sprite.empty(), true, (b) -> { + if (SavedRoutesManager.isSaved(route)) { + SavedRoutesManager.removeRoute(route); + } else { + SavedRoutesManager.saveRoute(route); + } + }, null)) + //.add(new ContextMenuItemData(textShare, Sprite.empty(), true, (b) -> {}, null)) + )); + } + + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + final int precision = ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(); + + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x, y, WIDTH, HEIGHT, ColorShade.DARK.getColor()); + CreateDynamicWidgets.renderHorizontalSeparator(graphics, x + 6, y + 22, WIDTH - 12); + + if (isMouseSelected()) { + GuiUtils.fill(graphics, x(), y(), width(), height(), 0x22FFFFFF); + } + + Minecraft minecraft = Minecraft.getInstance(); + ImmutableList parts = route.getParts(); + Font shadowlessFont = new NoShadowFontWrapper(minecraft.font); + + String timeStart = TimeUtils.parseTime((int)((route.getStart().getScheduledDepartureTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + String timeEnd = TimeUtils.parseTime((int)((route.getEnd().getScheduledArrivalTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + String dash = " - "; + MutableComponent summary = TextUtils.text(String.format("%s%s%s | %s %s | %s", + timeStart, + dash, + timeEnd, + route.getTransferCount(), + transferText.getString(), + TimeUtils.parseDurationShort((int)route.travelTime()) + )); + + final float scale = 0.75f; + + float localScale = shadowlessFont.width(summary) > WIDTH - 12 ? scale : 1; + graphics.poseStack().pushPose(); + graphics.poseStack().scale(localScale, 1, 1); + GuiUtils.drawString(graphics, minecraft.font, (int)((x + 6) / localScale), y + 5, summary, 0xFFFFFF, EAlignment.LEFT, false); + graphics.poseStack().popPose(); + + int routePartWidth = DISPLAY_WIDTH / parts.size(); + String endStationName = route.getEnd().getClientTag().tagName(); + int textW = shadowlessFont.width(endStationName); + + for (int i = 0; i < parts.size(); i++) { + int color = parts.get(i).getFirstStop().getTrainDisplayColor(); + GuiUtils.fill(graphics, x + 6 + (i * routePartWidth) + 1, y + 27, routePartWidth - 4, 1, color); + GuiUtils.fill(graphics, x + 5 + (i * routePartWidth) + 1, y + 28, routePartWidth - 2, 9, color); + GuiUtils.fill(graphics, x + 6 + (i * routePartWidth) + 1, y + 37, routePartWidth - 4, 1, color); + } + + graphics.poseStack().pushPose(); + graphics.poseStack().scale(scale, scale, scale); + + for (int i = 0; i < parts.size(); i++) { + int color = parts.get(i).getFirstStop().getTrainDisplayColor(); + int fontColor = ModGuiUtils.useWhiteOrBlackForeColor(color) ? 0xFFFFFFFF: 0xFF000000; + Component trainName = GuiUtils.ellipsisString(font, TextUtils.text(parts.get(i).getFirstStop().getTrainDisplayName()), (int)((routePartWidth - 10) / 0.75f)); + GuiUtils.drawString(graphics, font, (int)((x + 5 + (i * routePartWidth) + (routePartWidth / 2)) / 0.75f), (int)((y + 30) / 0.75f), trainName, fontColor, EAlignment.CENTER, false); + } + + GuiUtils.drawString(graphics, font, (int)((x + 6) / scale), (int)((y + 43) / scale), TextUtils.text(route.getStart().getClientTag().tagName()), 0xDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, (int)((x + WIDTH - 6) / scale) - textW, (int)((y + 43) / scale), TextUtils.text(endStationName), 0xDBDBDB, EAlignment.LEFT, false); + if (route.getStart().shouldRenderRealTime()) { + GuiUtils.drawString(graphics, font, (int)((x + 6 + font.width(timeStart) * localScale / 2.0f) / scale) - font.width(timeStart) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)((route.getStart().getScheduledDepartureTime() + (route.getStart().getDepartureTimeDeviation() / precision * precision)) % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), route.getStart().isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + } + if (route.getEnd().shouldRenderRealTime()) { + GuiUtils.drawString(graphics, font, (int)((x + 6 + font.width(timeEnd) * localScale * 1.5f + (font.width(dash)) * localScale) / scale) - font.width(timeEnd) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)((route.getEnd().getScheduledArrivalTime() + (route.getEnd().getArrivalTimeDeviation() / precision * precision)) % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), route.getEnd().isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + } + + if (route.isAnyCancelled()) { + GuiUtils.drawString(graphics, shadowlessFont, (int)((x + WIDTH - 5) / scale), (int)((y + 15) / scale), trainCanceled, Constants.COLOR_DELAYED, EAlignment.RIGHT, false); + } else if (route.getStart().isDeparted()) { + GuiUtils.drawString(graphics, shadowlessFont, (int)((x + WIDTH - 5) / scale), (int)((y + 15) / scale), connectionInPast, Constants.COLOR_DELAYED, EAlignment.RIGHT, false); + } + + graphics.poseStack().popPose(); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java new file mode 100644 index 00000000..55618ca5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java @@ -0,0 +1,105 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.screen.RouteDetailsScreen; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.ISaveableNavigatorData; +import de.mrjulsen.crn.data.SavedRoutesManager; +import de.mrjulsen.crn.data.ISaveableNavigatorData.SaveableNavigatorDataLine; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.Route; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenu; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem.ContextMenuItemData; +import de.mrjulsen.mcdragonlib.client.render.GuiIcons; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.MutableComponent; + +public class SavedRouteWidget extends DLButton { + + public static final int WIDTH = 180; + public static final int HEADER_HEIGHT = 20; + public static final int DEFAULT_LINE_HEIGHT = 12; + public static final float DEFAULT_SCALE = 0.75f; + + private static final int DISPLAY_WIDTH = WIDTH - 20; + + private final ISaveableNavigatorData data; + + private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); + private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); + private final MutableComponent trainCanceled = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); + private final MutableComponent textShowDetails = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_route_widget.show_details"); + private final MutableComponent textRemove = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.remove"); + private final MutableComponent textShare = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_route_widget.share"); + private final MutableComponent textShowNotifications = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_route_widget.notifications"); + + public SavedRouteWidget(SavedRoutesViewer parent, int x, int y, ISaveableNavigatorData data) { + super(x, y, WIDTH, 50, TextUtils.empty(), (b) -> clickAction(parent, data)); + this.data = data; + set_height(HEADER_HEIGHT + 10 + data.getOverviewData().stream().mapToInt(a -> (int)(Math.max(DEFAULT_LINE_HEIGHT, ClientWrapper.getTextBlockHeight(font, a.text(), (int)(DISPLAY_WIDTH / DEFAULT_SCALE))) * DEFAULT_SCALE)).sum()); + + setRenderStyle(AreaStyle.FLAT); + setMenu(new DLContextMenu(() -> GuiAreaDefinition.of(this), () -> new DLContextMenuItem.Builder() + .add(new ContextMenuItemData(textShowDetails, Sprite.empty(), true, (b) -> onPress.onPress(b), null)) + .addSeparator() + .add(new ContextMenuItemData(textRemove, Sprite.empty(), true, (b) -> { + SavedRoutesManager.removeRoute((ClientRoute)data); + SavedRoutesManager.push(true, null); + parent.displayRoutes(SavedRoutesManager.getAllSavedRoutes()); + }, null)) + //.add(new ContextMenuItemData(textShare, Sprite.empty(), true, (b) -> {}, null)) + .addSeparator() + .add(new ContextMenuItemData(textShowNotifications, data instanceof ClientRoute route && route.shouldShowNotifications() ? GuiIcons.CHECKMARK.getAsSprite(8, 8) : Sprite.empty(), data instanceof Route, (b) -> { + if (data instanceof ClientRoute route) { + route.setShowNotifications(!route.shouldShowNotifications()); + } + }, null)) + )); + } + + private static void clickAction(SavedRoutesViewer parent, ISaveableNavigatorData data) { + if (data instanceof ClientRoute route) { + Minecraft.getInstance().setScreen(new RouteDetailsScreen(parent.getParent(), route)); + } + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.DARK.getColor()); + CreateDynamicWidgets.renderHorizontalSeparator(graphics, x() + 6, y() + 16, width() - 12 - data.getTitle().icon().getWidth()); + + if (isMouseSelected()) { + GuiUtils.fill(graphics, x(), y(), width(), height(), 0x22FFFFFF); + } + + GuiUtils.drawString(graphics, font, x() + 6, y() + 5, data.getTitle().text(), 0xFFFFFFFF, EAlignment.LEFT, false); + data.getTitle().icon().render(graphics, x() + width() - data.getTitle().icon().getWidth() - 3, y() + 3); + + graphics.poseStack().pushPose(); + graphics.poseStack().translate(x() + 10, y() + HEADER_HEIGHT, 0); + for (SaveableNavigatorDataLine line : data.getOverviewData()) { + graphics.poseStack().pushPose(); + graphics.poseStack().scale(DEFAULT_SCALE, DEFAULT_SCALE, 1); + line.icon().render(graphics, 0, -2); + int height = (int)(ClientWrapper.renderMultilineLabelSafe(graphics, (int)(16 / DEFAULT_SCALE), (int)(2 / DEFAULT_SCALE), font, line.text(), (int)(DISPLAY_WIDTH / DEFAULT_SCALE), 0xFFFFFFFF) * DEFAULT_SCALE); + graphics.poseStack().popPose(); + graphics.poseStack().translate(0, Math.max((int)(DEFAULT_LINE_HEIGHT * DEFAULT_SCALE), height + 4), 0); + } + + graphics.poseStack().popPose(); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java new file mode 100644 index 00000000..7c20b4f3 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java @@ -0,0 +1,132 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.ISaveableNavigatorData; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class SavedRoutesViewer extends ScrollableWidgetContainer { + + private final Screen parent; + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + + private List data = List.of(); + + public SavedRoutesViewer(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.parent = parent; + this.scrollBar = scrollBar; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public Screen getParent() { + return parent; + } + + public void refresh() { + displayRoutes(data); + } + + public void displayRoutes(List data) { + this.data = data; + Collections.sort(data, Comparator + .comparing(x -> ((ISaveableNavigatorData)x).customGroup() == null ? null : ((ISaveableNavigatorData)x).customGroup().getFirst(), Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparingLong(x -> ((ISaveableNavigatorData)x).dayOrderValue()) + .thenComparingLong(x -> ((ISaveableNavigatorData)x).timeOrderValue())); + + clearWidgets(); + contentHeight = 5; + ISaveableNavigatorData lastData = null; + for (int i = 0; i < data.size(); i++) { + ISaveableNavigatorData d = data.get(i); + + if (lastData != null && lastData.customGroup() != d.customGroup()) { + contentHeight += addRenderableOnly(new GroupingHeader(x(), y() + contentHeight, width(), (d.customGroup() == null ? TextUtils.empty() : d.customGroup().getSecond()).withStyle(ChatFormatting.BOLD))).height(); + } + if (lastData == null || lastData.dayOrderValue() != d.dayOrderValue()) { + Component text; + long worldTime = DragonLib.getCurrentWorldTime(); + long dayDiff = d.dayOrderValue() - (worldTime + DragonLib.DAYTIME_SHIFT) / DragonLib.TICKS_PER_DAY; + if (d.timeOrderValue() < worldTime) text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.in_the_past"); + else if (dayDiff == 0) text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.today"); + else if (dayDiff == 1) text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.tomorrow"); + else text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.in_days", dayDiff); + contentHeight += addRenderableOnly(new GroupingHeader(x(), y() + contentHeight, width(), text)).height(); + } + + lastData = d; + SavedRouteWidget widget = new SavedRouteWidget(this, x(), y() + contentHeight, d); + addRenderableWidget(widget); + widget.set_x(x() + width() / 2 - widget.width() / 2); + contentHeight += (widget.height() + 3); + + } + contentHeight += 10; + scrollBar.updateMaxScroll(contentHeight); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + if (children().isEmpty()) { + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".empty_list"), 0xFFDBDBDB, EAlignment.CENTER, false); + } + + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + + private static final class GroupingHeader extends DLRenderable { + + private static final int HEIGHT = 24; + + private final Component text; + + public GroupingHeader(int x, int y, int width, Component text) { + super(x, y, width, HEIGHT); + this.text = text == null ? TextUtils.empty() : text; + } + + @SuppressWarnings("resource") + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x() + 10, y() + height() / 2 - Minecraft.getInstance().font.lineHeight / 2, text, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, true); + } + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SearchOptionButton.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SearchOptionButton.java new file mode 100644 index 00000000..28016b7f --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SearchOptionButton.java @@ -0,0 +1,47 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public class SearchOptionButton extends DLButton { + + private final Supplier value; + + public SearchOptionButton(int pX, int pY, int pWidth, int pHeight, Component text, Supplier value, Consumer clickAction) { + super(pX, pY, pWidth, pHeight, text, clickAction); + this.value = value; + setRenderStyle(AreaStyle.FLAT); + setBackColor(0x00000000); + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, getBackColor(), style, isActive() ? (isFocused() || isMouseSelected() ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + + int j = active ? getFontColor() : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; + + GuiUtils.fill(graphics, x() + width() - 1, y() + 2, 1, height() - 4, DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED); + + final float scale = 0.75f; + graphics.poseStack().pushPose(); + graphics.poseStack().scale(scale, scale, 1); + graphics.poseStack().translate(x() / scale, y() / scale, 0); + GuiUtils.drawString(graphics, font, 5, 3, TextUtils.empty().append(getMessage()).withStyle(ChatFormatting.BOLD), j, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 5, 3 + font.lineHeight + 1, TextUtils.text(value.get()).withStyle(ChatFormatting.GRAY), j, EAlignment.LEFT, false); + graphics.poseStack().popPose(); + //GuiIcons.ARROW_DOWN.render(graphics, x() + width() - DROP_DOWN_BUTTON_WIDTH + 3, y() + height() / 2 - GuiIcons.ICON_SIZE / 2 + 5); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SettingsOptionWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SettingsOptionWidget.java deleted file mode 100644 index 408a1afe..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SettingsOptionWidget.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import java.util.List; -import java.util.function.Consumer; - -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; - -public class SettingsOptionWidget extends DLButton { - - public static final int WIDTH = 200; - public static final int HEIGHT = 48; - - private static final int DISPLAY_WIDTH = 190; - - private final Screen parent; - private final Font shadowlessFont; - - // Controls - private final MultiLineLabel messageLabel; - private final MutableComponent tooltipOptionText = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".global_settings.option.tooltip"); - - - public SettingsOptionWidget(Screen parent, int pX, int pY, Component title, Component description, Consumer onClick) { - super(pX, pY, 200, 48, title, onClick); - - Minecraft minecraft = Minecraft.getInstance(); - shadowlessFont = new NoShadowFontWrapper(minecraft.font); - - this.parent = parent; - this.messageLabel = MultiLineLabel.create(shadowlessFont, description, (int)((DISPLAY_WIDTH) / 0.75f)); - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - float l = isMouseOver(pMouseX, pMouseY) && isHovered ? 0.2f : 0; - GuiUtils.setTint(1 + l, 1 + l, 1 + l, 1); - GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, 0, WIDTH, HEIGHT); - - GuiUtils.drawString(graphics, shadowlessFont, x + 6, y + 5, getMessage(), 0xFFFFFF, EAlignment.LEFT, false); - graphics.poseStack().scale(0.75f, 0.75f, 0.75f); - this.messageLabel.renderLeftAligned(graphics.poseStack(), (int)((x + 6) / 0.75f), (int)((y + 20) / 0.75f), 10, 0xDBDBDB); - float s = 1 / 0.75f; - graphics.poseStack().scale(s, s, s); - } - - @Override - public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTicks) { - GuiUtils.renderTooltipWithOffset(parent, this, List.of(tooltipOptionText), parent.width, graphics, pMouseX, pMouseY, 0, (int)pPartialTicks); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java new file mode 100644 index 00000000..14995f92 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java @@ -0,0 +1,86 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.crn.registry.ModAccessorTypes.DepartureRoutesData; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Pair; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class StationDeparturesViewer extends ScrollableWidgetContainer { + + private final Screen parent; + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + + public StationDeparturesViewer(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.parent = parent; + this.scrollBar = scrollBar; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public Screen getParent() { + return parent; + } + + @SuppressWarnings("resource") + public void displayRoutes(String stationTagName, UserSettings settings) { + clearWidgets(); + contentHeight = 0; + if (stationTagName == null) { + return; + } + + DataAccessor.getFromServer(new DepartureRoutesData(stationTagName, Minecraft.getInstance().player.getUUID()), ModAccessorTypes.GET_DEPARTURE_AND_ARRIVAL_ROUTES_AT, (routesL) -> { + for (int i = 0; i < routesL.size(); i++) { + Pair route = routesL.get(i); + StationDeparturesWidget widget = new StationDeparturesWidget(parent, this, x() + 10, y() + 5 + contentHeight, width() - 20, route.getSecond(), route.getFirst()); + addRenderableWidget(widget); + contentHeight += (widget.height() + 3); + } + contentHeight += 7; + scrollBar.updateMaxScroll(contentHeight); + }); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + if (children().isEmpty()) { + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".empty_list"), 0xFFDBDBDB, EAlignment.CENTER, false); + } + + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java new file mode 100644 index 00000000..2f3317cc --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java @@ -0,0 +1,108 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import com.simibubi.create.foundation.gui.AllIcons; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.screen.TrainJourneySreen; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenu; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem.ContextMenuItemData; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class StationDeparturesWidget extends DLButton implements AutoCloseable { + + public static final int HEADER_HEIGHT = 20; + public static final int DEFAULT_LINE_HEIGHT = 12; + public static final float DEFAULT_SCALE = 0.75f; + + + private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); + private final MutableComponent trainCanceled = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); + + private final ClientRoute route; + private final boolean arrival; + + public StationDeparturesWidget(Screen parent, StationDeparturesViewer viewer, int x, int y, int width, ClientRoute route, boolean arrival) { + super(x, y, width, 32, TextUtils.empty(), (b) -> { + DLScreen.setScreen(new TrainJourneySreen(parent, route, route.getStart().getTrainId())); + }); + this.route = route; + this.arrival = arrival; + + setRenderStyle(AreaStyle.FLAT); + setMenu(new DLContextMenu(() -> GuiAreaDefinition.of(this), () -> new DLContextMenuItem.Builder() + .add(new ContextMenuItemData(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.view_details"), Sprite.empty(), true, (b) -> onPress.onPress(b), null)) + )); + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.DARK.getColor()); + + if (isMouseSelected()) { + GuiUtils.fill(graphics, x(), y(), width(), height(), 0x22FFFFFF); + } + + final float scale = 0.75f; + Component trainName = TextUtils.text(route.getStart().getTrainDisplayName()).withStyle(ChatFormatting.BOLD); + graphics.poseStack().pushPose(); + graphics.poseStack().translate(x(), y(), 0); + graphics.poseStack().scale(scale, scale, scale); + if (arrival) { + AllIcons.I_CONFIG_OPEN.render(graphics.poseStack(), 8, 5); + } else { + AllIcons.I_CONFIG_BACK.render(graphics.poseStack(), 8, 5); + } + + if (route.isAnyCancelled()) { + GuiUtils.drawString(graphics, font, (int)((x + width() - 5) / scale), (int)((y() + 15) / scale), trainCanceled, Constants.COLOR_DELAYED, EAlignment.RIGHT, false); + } else if (route.getStart().isDeparted()) { + GuiUtils.drawString(graphics, font, (int)((x + width() - 5) / scale), (int)((y() + 15) / scale), connectionInPast, Constants.COLOR_DELAYED, EAlignment.RIGHT, false); + } + + CreateDynamicWidgets.renderTextHighlighted(graphics, 30, 6, font, trainName, route.getStart().getTrainDisplayColor()); + graphics.poseStack().popPose(); + + Component platformText = TextUtils.text(route.getStart().getRealTimeStationTag().info().platform()); + int platformTextWidth = font.width(platformText); + final int maxStationNameWidth = width() - platformTextWidth - 15 - (int)((45 + font.width(trainName)) * scale); + MutableComponent stationText = arrival ? TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", route.getEnd().getClientTag().tagName()) : TextUtils.text(route.getStart().getDisplayTitle()); + if (font.width(stationText) > maxStationNameWidth) { + stationText = TextUtils.text(font.substrByWidth(stationText, maxStationNameWidth).getString()).append(TextUtils.text("...")).withStyle(stationText.getStyle()); + } + + GuiUtils.drawString(graphics, font, x() + (int)((45 + font.width(trainName)) * scale), y() + 6, stationText, 0xFFFFFF, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, x() + width() - 6, y() + 6, platformText, 0xFFFFFF, EAlignment.RIGHT, false); + GuiUtils.drawString(graphics, font, x() + (int)(30 * scale), y() + 20, ModUtils.formatTime(arrival ? route.getStart().getScheduledArrivalTime() : route.getStart().getScheduledDepartureTime(), false), 0xFFFFFF, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, x() + (int)(30 * scale) + 40, y() + 20, ModUtils.formatTime(arrival ? route.getStart().getRealTimeArrivalTime() : route.getStart().getRealTimeDepartureTime(), false), (arrival ? route.getStart().isArrivalDelayed() : route.getStart().isDepartureDelayed()) ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + + + //GuiUtils.drawString(graphics, font, x() + 6, y() + 5, route.getStart().getTag().getTagName().get(), 0xFFFFFFFF, EAlignment.LEFT, false); + + } + + @Override + public void close() { + route.closeAll(); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java new file mode 100644 index 00000000..94c138e0 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java @@ -0,0 +1,83 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.crn.debug.TrainDebugData; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class TrainDebugViewer extends ScrollableWidgetContainer { + + private final Screen parent; + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + + public TrainDebugViewer(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.parent = parent; + this.scrollBar = scrollBar; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public Screen getParent() { + return parent; + } + + @Override + protected void clearWidgets() { + super.clearWidgets(); + } + + public void reload() { + clearWidgets(); + contentHeight = 0; + + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_TRAINS_DEBUG_DATA, (debug) -> { + for (int i = 0; i < debug.size(); i++) { + TrainDebugData debugData = debug.get(i); + TrainDebugWidget widget = new TrainDebugWidget(parent, this, x() + 10, y() + 5 + contentHeight, width() - 20, debugData); + addRenderableWidget(widget); + contentHeight += (widget.height() + 3); + } + contentHeight += 7; + scrollBar.updateMaxScroll(contentHeight); + }); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + if (children().isEmpty()) { + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".empty_list"), 0xFFDBDBDB, EAlignment.CENTER, false); + } + + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugWidget.java new file mode 100644 index 00000000..a4b98bf4 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugWidget.java @@ -0,0 +1,74 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.debug.TrainDebugData; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenu; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenuItem.ContextMenuItemData; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class TrainDebugWidget extends DLButton { + + public static final int HEADER_HEIGHT = 20; + public static final int DEFAULT_LINE_HEIGHT = 12; + public static final float DEFAULT_SCALE = 0.75f; + + private final TrainDebugData data; + + public TrainDebugWidget(Screen parent, TrainDebugViewer viewer, int x, int y, int width, TrainDebugData data) { + super(x, y, width, 32, TextUtils.empty(), (b) -> {}); + this.data = data; + setRenderStyle(AreaStyle.FLAT); + setMenu(new DLContextMenu(() -> GuiAreaDefinition.of(this), () -> new DLContextMenuItem.Builder() + .add(new ContextMenuItemData(TextUtils.text("Reset Predictions"), Sprite.empty(), true, (b) -> DataAccessor.getFromServer(data.trainId(), ModAccessorTypes.TRAIN_SOFT_RESET, $ -> viewer.reload()), null)) + .addSeparator() + .add(new ContextMenuItemData(TextUtils.text("Hard Reset"), Sprite.empty(), true, (b) -> DataAccessor.getFromServer(data.trainId(), ModAccessorTypes.TRAIN_HARD_RESET, $ -> viewer.reload()), null)) + )); + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.DARK.getColor()); + + if (isMouseSelected()) { + GuiUtils.fill(graphics, x(), y(), width(), height(), 0x22FFFFFF); + } + + final float scale = 0.75f; + Component trainName = TextUtils.text(data.trainName()).withStyle(ChatFormatting.BOLD); + graphics.poseStack().pushPose(); + graphics.poseStack().translate(x(), y(), 0); + graphics.poseStack().scale(scale, scale, scale); + + Component predictionsText = TextUtils.text(data.predictionsInitialized() + " / " + data.predictionsCount()); + int platformTextWidth = font.width(predictionsText); + CreateDynamicWidgets.renderTextHighlighted(graphics, 5, 4, font, trainName, Constants.COLOR_TRAIN_BACKGROUND); + final int maxIdWidth = (int)(width() - platformTextWidth * scale - 15 - (45 + font.width(trainName)) * scale); + MutableComponent idText = TextUtils.text(data.trainId().toString()); + if (font.width(idText) > maxIdWidth) { + idText = TextUtils.text(font.substrByWidth(idText, maxIdWidth).getString()).append(TextUtils.text("...")).withStyle(idText.getStyle()); + } + GuiUtils.drawString(graphics, font, 20 + font.width(trainName), 6, idText, 0xFFFFFF, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, (int)(width() / scale) - 6, 6, predictionsText, 0xFFFFFF, EAlignment.RIGHT, false); + GuiUtils.drawString(graphics, font, 5, 20, TextUtils.text("Session: " + data.sessionId()), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 5, 30, TextUtils.text("Status: " + data.state().getName()), data.state().getColor(), EAlignment.LEFT, false); + graphics.poseStack().popPose(); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainGroupEntryWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainGroupEntryWidget.java deleted file mode 100644 index e0a17fe7..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainGroupEntryWidget.java +++ /dev/null @@ -1,408 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.HashMap; -import java.util.Map.Entry; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.simibubi.create.content.trains.station.NoShadowFontWrapper; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; -import de.mrjulsen.crn.client.gui.screen.AbstractEntryListSettingsScreen; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainGroup; -import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; -import de.mrjulsen.mcdragonlib.util.MathUtils; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; - -public class TrainGroupEntryWidget extends AbstractEntryListOptionWidget { - - private static final ResourceLocation GUI_WIDGETS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/settings_widgets.png"); - public static final int WIDTH = 200; - public static final int HEIGHT = 48; - private static final int STATION_ENTRY_HEIGHT = 20; - - private final AbstractEntryListSettingsScreen parent; - private final Font shadowlessFont; - private final Minecraft minecraft; - - private final Runnable onUpdate; - - // Data - private TrainGroup trainGroup; - private boolean expanded = false; - - // Controls - private final DLEditBox titleBox; - private final DLEditBox newEntryBox; - private final Map removeStationButtons = new HashMap<>(); - private final Map trainGroupNames = new HashMap<>(); - - private GuiAreaDefinition titleBarArea; - - private GuiAreaDefinition deleteButton; - private GuiAreaDefinition expandButton; - private GuiAreaDefinition addButton; - - private ModStationSuggestions suggestions; - - // Tooltips - private final MutableComponent tooltipDeleteAlias = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_group_settings.delete_alias.tooltip"); - private final MutableComponent tooltipDeleteStation = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_group_settings.delete_station.tooltip"); - private final MutableComponent tooltipAddStation = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_group_settings.add_station.tooltip"); - - - public TrainGroupEntryWidget(AbstractEntryListSettingsScreen parent, int pX, int pY, TrainGroup trainGroup, Runnable onUpdate, boolean expanded) { - super(pX, pY, 200, 48); - - Minecraft minecraft = Minecraft.getInstance(); - shadowlessFont = new NoShadowFontWrapper(minecraft.font); - - this.minecraft = Minecraft.getInstance(); - this.parent = parent; - this.trainGroup = trainGroup; - this.expanded = expanded; - this.onUpdate = onUpdate; - - titleBox = new DLEditBox(minecraft.font, pX + 30, pY + 10, 129, 12, TextUtils.empty()); - titleBox.setBordered(false); - titleBox.setMaxLength(32); - titleBox.setTextColor(0xFFFFFF); - titleBox.setValue(trainGroup.getGroupName()); - titleBox.withOnFocusChanged((box, focused) -> { - if (!focused) { - if (!setGroupName(box.getValue())) { - titleBox.setValue(trainGroup.getGroupName()); - } - } - }); - titleBox.visible = expanded; - addRenderableWidget(titleBox); - - - newEntryBox = new DLEditBox(minecraft.font, pX + 30, pY + 30, 129, 12, TextUtils.empty()); - newEntryBox.setBordered(false); - newEntryBox.setMaxLength(32); - newEntryBox.setTextColor(0xFFFFFF); - newEntryBox.visible = expanded; - newEntryBox.setResponder(x -> { - updateEditorSubwidgets(newEntryBox); - }); - addRenderableWidget(newEntryBox); - - setYPos(pY); - } - - public TrainGroup getTrainGroup() { - return trainGroup; - } - - public boolean isExpanded() { - return expanded; - } - - private void initStationDeleteButtons() { - removeStationButtons.clear(); - trainGroupNames.clear(); - - String[] names = trainGroup.getTrainNames().toArray(String[]::new); - for (int i = 0; i < names.length; i++) { - String name = names[i]; - removeStationButtons.put(name, new GuiAreaDefinition(x + 165, y + 26 + (i * STATION_ENTRY_HEIGHT) + 2, 16, 16)); - trainGroupNames.put(name, new GuiAreaDefinition(x + 25, y + 26 + (i * STATION_ENTRY_HEIGHT) + 2, 129, 16)); - } - - titleBarArea = new GuiAreaDefinition(x + 25, y + 6, 129, 16); - } - - @Override - public void tick() { - super.tick(); - - if (suggestions != null) { - suggestions.tick(); - - if (!newEntryBox.canConsumeInput()) { - clearSuggestions(); - } - } - } - - private void toggleExpanded() { - this.expanded = !expanded; - titleBox.visible = expanded; - newEntryBox.visible = expanded; - } - - private void deleteTrainGroup() { - GlobalSettingsManager.getInstance().getSettingsData().unregisterTrainGroup(trainGroup, onUpdate); - } - - private void addTrain(String name) { - String prevName = trainGroup.getGroupName(); - if (ClientTrainStationSnapshot.getInstance().getAllTrainNames().stream().noneMatch(x -> x.equals(name)) || newEntryBox.getValue().isBlank()) { - return; - } - - trainGroup.add(name); - trainGroup.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateTrainGroup(prevName, trainGroup, () -> { - onUpdate.run(); - initStationDeleteButtons(); - }); - - newEntryBox.setValue(""); - newEntryBox.setFocus(false); - } - - private boolean setGroupName(String name) { - String prevName = trainGroup.getGroupName(); - - if (name == null || name.isBlank()) { - return false; - } - - if (GlobalSettingsManager.getInstance().getSettingsData().getTrainGroupsList().stream().anyMatch(x -> x.getGroupName().equals(name))) { - return false; - } - - trainGroup.setGroupName(name); - trainGroup.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateTrainGroup(prevName, trainGroup, onUpdate); - return true; - } - - private void removeTrain(String name) { - String prevName = trainGroup.getGroupName(); - trainGroup.remove(name); - trainGroup.updateLastEdited(minecraft.player.getName().getString()); - GlobalSettingsManager.getInstance().getSettingsData().updateTrainGroup(prevName, trainGroup, () -> { - onUpdate.run(); - initStationDeleteButtons(); - }); - initStationDeleteButtons(); - onUpdate.run(); - } - - @Override - public int getHeight() { - return height; - } - - public int calcHeight() { - if (expanded) { - height = STATION_ENTRY_HEIGHT * trainGroup.getTrainNames().size() + 50; - } else { - height = HEIGHT; - } - return height; - } - - @Override - public void setYPos(int y) { - this.y = y; - deleteButton = new GuiAreaDefinition(x + 165, y + 6, 16, 16); - expandButton = new GuiAreaDefinition(x + 182, y + 6, 16, 16); - addButton = new GuiAreaDefinition(x + 165, y + 26 + (trainGroup.getTrainNames().size() * STATION_ENTRY_HEIGHT) + 2, 16, 16); - titleBox.y = y + 10; - initStationDeleteButtons(); - } - - @Override - public void renderSuggestions(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { - if (suggestions != null) { - poseStack.pushPose(); - poseStack.translate(0, -parent.getScrollOffset(partialTicks), 500); - suggestions.render(poseStack, mouseX, mouseY + parent.getScrollOffset(partialTicks)); - poseStack.popPose(); - } - } - - @Override - public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - RenderSystem.setShaderTexture(0, GUI_WIDGETS); - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x, y, 0, 0, WIDTH, HEIGHT); - - GuiUtils.drawTexture(GUI_WIDGETS, graphics, deleteButton.getX(), deleteButton.getY(), 232, 0, 16, 16); // delete button - GuiUtils.drawTexture(GUI_WIDGETS, graphics, expandButton.getX(), expandButton.getY(), expanded ? 216 : 200, 0, 16, 16); // expand button - - if (expanded) { - Set names = trainGroup.getTrainNames(); - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x + 25, y + 5, 0, 92, 139, 18); // textbox - newEntryBox.y = y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 6; - - for (int i = 0; i < names.size(); i++) { - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x, y + 26 + (i * STATION_ENTRY_HEIGHT), 0, 48, 200, STATION_ENTRY_HEIGHT); - } - - GuiUtils.drawTexture(GUI_WIDGETS, graphics, x, y + 26 + (names.size() * STATION_ENTRY_HEIGHT), 0, 68, 200, 24); - CreateDynamicWidgets.renderTextBox(graphics, x + 25, y + 26 + (names.size() * STATION_ENTRY_HEIGHT) + 1, 139); - GuiUtils.drawTexture(GUI_WIDGETS, graphics, addButton.getX(), addButton.getY(), 200, 16, 16, 16); // add button - - for (GuiAreaDefinition def : removeStationButtons.values()) { - GuiUtils.drawTexture(GUI_WIDGETS, graphics, def.getX(), def.getY(), 232, 0, 16, 16); // delete button - } - - int i = 0; - for (String entry : names) { - GuiUtils.drawString(graphics, shadowlessFont, x + 30, y + 26 + (i * STATION_ENTRY_HEIGHT) + 6, TextUtils.text(entry), 0xFFFFFF, EAlignment.LEFT, false); - i++; - } - - } else { - MutableComponent name = TextUtils.text(trainGroup.getGroupName()); - int maxTextWidth = 129; - if (shadowlessFont.width(name) > maxTextWidth) { - name = TextUtils.text(shadowlessFont.substrByWidth(name, maxTextWidth).getString()).append(Constants.ELLIPSIS_STRING); - } - GuiUtils.drawString(graphics, shadowlessFont, x + 30, y + 10, name, 0xFFFFFF, EAlignment.LEFT, false); - - graphics.poseStack().scale(0.75f, 0.75f, 0.75f); - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 5) / 0.75f), (int)((y + 30) / 0.75f), TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_group_settings.summary", trainGroup.getTrainNames().size()), 0xDBDBDB, EAlignment.LEFT, false); - if (trainGroup.getLastEditorName() != null && !trainGroup.getLastEditorName().isBlank()) { - GuiUtils.drawString(graphics, shadowlessFont, (int)((x + 5) / 0.75f), (int)((y + 38) / 0.75f), TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_group_settings.editor", trainGroup.getLastEditorName(), trainGroup.getLastEditedTimeFormatted()), 0xDBDBDB, EAlignment.LEFT, false); - } - float s = 1 / 0.75f; - graphics.poseStack().scale(s, s, s); - } - - super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - - // Button highlight - if (deleteButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, deleteButton.getX(), deleteButton.getY(), deleteButton.getWidth(), deleteButton.getHeight(), 0x1AFFFFFF); - } else if (expandButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, expandButton.getX(), expandButton.getY(), expandButton.getWidth(), expandButton.getHeight(), 0x1AFFFFFF); - } else if (expanded && addButton.isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, addButton.getX(), addButton.getY(), addButton.getWidth(), addButton.getHeight(), 0x1AFFFFFF); - } else if (expanded && removeStationButtons.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY))) { - for (Entry entry : removeStationButtons.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY)) { - GuiUtils.fill(graphics, entry.getValue().getX(), entry.getValue().getY(), entry.getValue().getWidth(), entry.getValue().getHeight(), 0x1AFFFFFF); - } - } - } - } - - @Override - public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { - if (suggestions != null) { - graphics.poseStack().pushPose(); - graphics.poseStack().translate(0, -parent.getScrollOffset(partialTicks), 500); - suggestions.render(graphics.poseStack(), mouseX, mouseY + parent.getScrollOffset(partialTicks)); - graphics.poseStack().popPose(); - } - - GuiUtils.renderTooltipWithOffset(parent, deleteButton, List.of(tooltipDeleteAlias), width, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - GuiUtils.renderTooltipWithOffset(parent, expandButton, List.of(expanded ? Constants.TOOLTIP_COLLAPSE : Constants.TOOLTIP_EXPAND), width, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - if (expanded) { - GuiUtils.renderTooltipWithOffset(parent, addButton, List.of(tooltipAddStation), width, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - for (Entry entry : removeStationButtons.entrySet()) { - if (GuiUtils.renderTooltipWithOffset(parent, entry.getValue(), List.of(tooltipDeleteStation), width, graphics, mouseX, mouseY, 0, parent.getScrollOffset(partialTicks))) { - break; - } - } - - for (Entry entry : trainGroupNames.entrySet()) { - if (shadowlessFont.width(entry.getKey()) > 129 && GuiUtils.renderTooltipAt(parent, entry.getValue(), List.of(TextUtils.text(entry.getKey())), width, graphics, entry.getValue().getLeft() + 1, entry.getValue().getTop() - parent.getScrollOffset(partialTicks), mouseX, mouseY, 0, parent.getScrollOffset(partialTicks))) { - break; - } - } - } else { - if (titleBarArea.isInBounds(mouseX, mouseY + parent.getScrollOffset(partialTicks)) && shadowlessFont.width(trainGroup.getGroupName()) > 129) { - GuiUtils.renderTooltipAt(parent, titleBarArea, List.of(TextUtils.text(trainGroup.getGroupName())), width, graphics, titleBarArea.getLeft() + 1, titleBarArea.getTop() - parent.getScrollOffset(partialTicks), mouseX, mouseY, 0, parent.getScrollOffset(partialTicks)); - } - } - } - - @Override - public boolean mouseClickedLoop(double pMouseX, double pMouseY, int pButton) { - //parent.unfocusAllEntries(); - - if (suggestions != null && suggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton)) - return super.mouseClicked(pMouseX, pMouseY, pButton); - - return false; - } - - @Override - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - if (suggestions != null && suggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton)) - return super.mouseClicked(pMouseX, pMouseY, pButton); - - if (deleteButton.isInBounds(pMouseX, pMouseY)) { - deleteTrainGroup(); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (expandButton.isInBounds(pMouseX, pMouseY)) { - toggleExpanded(); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (expanded && addButton.isInBounds(pMouseX, pMouseY)) { - addTrain(newEntryBox.getValue()); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } else if (expanded && removeStationButtons.values().stream().anyMatch(x -> x.isInBounds(pMouseX, pMouseY))) { - for (Entry entry : removeStationButtons.entrySet()) { - if (entry.getValue().isInBounds(pMouseX, pMouseY)) { - removeTrain(entry.getKey()); - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - } - } - return super.mouseClicked(pMouseX, pMouseY, pButton); - } - - @Override - public boolean mouseScrolledLoop(double pMouseX, double pMouseY, double pDelta) { - if (suggestions != null && suggestions.mouseScrolled(pMouseX, pMouseY, MathUtils.clamp(pDelta, -1.0D, 1.0D))) - return true; - - return false; - } - - private void clearSuggestions() { - if (suggestions != null) { - suggestions.getEditBox().setSuggestion(""); - } - suggestions = null; - } - - - protected void updateEditorSubwidgets(DLEditBox field) { - clearSuggestions(); - - suggestions = new ModStationSuggestions(minecraft, parent, field, minecraft.font, getViableTrains(field), field.getHeight() + 2 + field.y); - suggestions.setAllowSuggestions(true); - suggestions.updateCommandInfo(); - } - - private List getViableTrains(DLEditBox field) { - return ClientTrainStationSnapshot.getInstance().getAllTrainNames().stream() - .distinct() - .filter(x -> !GlobalSettingsManager.getInstance().getSettingsData().isTrainBlacklisted(x) && !trainGroup.contains(x)) - .sorted((a, b) -> a.compareTo(b)) - .toList(); - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.HOVERED; - } - - @Override - public void updateNarration(NarrationElementOutput narrationElementOutput) { - - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/WidgetContainerCollection.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/WidgetContainerCollection.java deleted file mode 100644 index c8a79d65..00000000 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/WidgetContainerCollection.java +++ /dev/null @@ -1,67 +0,0 @@ -package de.mrjulsen.crn.client.gui.widgets; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; - -public class WidgetContainerCollection { - - public final List components = new ArrayList<>(); - - private boolean enabled = true; - private boolean visible = true; - - public void performForEach(Predicate filter, Consumer consumer) { - components.stream().filter(filter).forEach(consumer); - } - - public void performForEach(Consumer consumer) { - performForEach(x -> true, consumer); - } - - public void performForEachOfType(Class clazz, Predicate filter, Consumer consumer) { - components.stream().filter(clazz::isInstance).map(clazz::cast).filter(filter).forEach(consumer); - } - - public void performForEachOfType(Class clazz, Consumer consumer) { - performForEachOfType(clazz, x -> true, consumer); - } - - - - public boolean isVisible() { - return visible; - } - - public boolean isEnabled() { - return enabled; - } - - public void setVisible(boolean v) { - this.visible = v; - performForEach(x -> x.set_visible(v)); - } - - public void setEnabled(boolean e) { - this.enabled = e; - performForEach(x -> x.set_active(e)); - } - - public void add(W widget) { - widget.set_active(enabled); - widget.set_visible(visible); - components.add(widget); - } - - public void clear() { - components.clear(); - } - - public void clear(Consumer onRemove) { - performForEach(x -> onRemove.accept(x)); - clear(); - } -} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java new file mode 100644 index 00000000..8e30a997 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java @@ -0,0 +1,82 @@ +package de.mrjulsen.crn.client.gui.widgets.create; + +import com.simibubi.create.foundation.gui.widget.ScrollInput; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class CreateTimeSelectionWidget extends WidgetContainer { + + public static final int WIDHT = 66; + public static final int HEIGHT = 18; + + private final MutableComponent transferTimeBoxText = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_settings.transfer_time"); + + protected ScrollInput transferTimeInput; + private Component transferLabel = TextUtils.empty(); + + private int value = 0; + + public CreateTimeSelectionWidget(int x, int y, int max) { + super(x, y, WIDHT, HEIGHT); + + transferTimeInput = addRenderableWidget(new ScrollInput(x() + 3, y(), width() - 6, height()) + .withRange(0, max) + .withStepFunction(a -> a.shift ? 1000 : 500) + .titled(transferTimeBoxText.copy()) + .calling((i) -> { + if (transferTimeInput == null) return; + value = transferTimeInput.getState(); + transferLabel = TextUtils.text(TimeUtils.parseDurationShort(value)); + }) + ); + setValue(0); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + CreateDynamicWidgets.renderTextBox(graphics, x(), y(), WIDHT); + GuiUtils.drawString(graphics, font, x() + 5, y() + 5, transferLabel, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, true); + } + + @Override + public void tick() { + super.tick(); + transferTimeInput.tick(); + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + DLUtils.doIfNotNull(transferTimeInput, x -> x.setState(value)); + transferLabel = TextUtils.text(TimeUtils.parseDurationShort(value)); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java new file mode 100644 index 00000000..4a1fe2a0 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java @@ -0,0 +1,63 @@ +package de.mrjulsen.crn.client.gui.widgets.flyouts; + +import java.util.function.Consumer; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.MutableComponent; + +public class FlyoutAdvancedSearchsettingsWidget extends AbstractFlyoutWidget { + + private final MutableComponent textTrainGroups = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.advanced_options").withStyle(ChatFormatting.BOLD); + //private UserSettings settings; + + public FlyoutAdvancedSearchsettingsWidget(DLScreen screen, FlyoutPointer pointer, ColorShade pointerShade, Consumer addRenderableWidgetFunc, Consumer removeWidgetFunc) { + super(screen, 1, 120, pointer, pointerShade, addRenderableWidgetFunc, removeWidgetFunc); + set_width(Math.max(150, font.width(textTrainGroups) + DLIconButton.DEFAULT_BUTTON_WIDTH + 16 + 10 + FlyoutPointer.WIDTH * 2)); + + //int top = getContentArea().getY() + 21; + //int contentHeight = getContentArea().getHeight() - 21 - 2; + + /* + addRenderableWidget(new SearchOptionButton(getContentArea().getX() + 2, top, getContentArea().getWidth() - 4, 18, TextUtils.text("Train Groups"), () -> "Here be Dragons!", (b) -> { + new FlyoutTrainGroupsWidget<>(screen, FlyoutPointer.UP, ColorShade.DARK, addRenderableWidgetFunc, (settings) -> { + return settings.navigationExcludedTrainGroups; + }, (w) -> { + removeWidgetFunc.accept(w); + }).open(b); + })); + */ + + DLIconButton resetBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), getContentArea().getX() + getContentArea().getWidth() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, getContentArea().getY() + 2, TextUtils.empty(), (b) -> { + + })); + + resetBtn.setBackColor(0x00000000); + } + + @Override + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, contentArea); + GuiUtils.drawString(graphics, font, contentArea.getX() + 8, contentArea.getY() + 8, textTrainGroups, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + + @Override + public void close() { + super.close(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java new file mode 100644 index 00000000..a544c841 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java @@ -0,0 +1,30 @@ +package de.mrjulsen.crn.client.gui.widgets.flyouts; + +import java.util.function.Consumer; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; +import de.mrjulsen.crn.client.gui.widgets.ColorPickerWidget; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; + +public class FlyoutColorPicker extends AbstractFlyoutWidget { + + private final ColorPickerWidget colorPicker; + + public FlyoutColorPicker(DLScreen screen, int initialColor, Consumer addRenderableWidgetFunc, Consumer removeWidgetFunc) { + super(screen, 100, 50, FlyoutPointer.RIGHT, ColorShade.LIGHT, addRenderableWidgetFunc, removeWidgetFunc); + colorPicker = addRenderableWidget(new ColorPickerWidget(screen, x() + 10, y() + 10, Constants.DEFAULT_TRAIN_TYPE_COLORS, 5, initialColor, (picker) -> { + closeImmediately(); + })); + set_width(colorPicker.width() + 20); + set_height(colorPicker.height() + 20); + } + + public ColorPickerWidget getColorPicker() { + return colorPicker; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java new file mode 100644 index 00000000..efb50ddc --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.client.gui.widgets.flyouts; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; +import de.mrjulsen.crn.client.gui.widgets.create.CreateTimeSelectionWidget; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.UserSettings.UserSetting; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.MutableComponent; + +public class FlyoutDepartureInWidget extends AbstractFlyoutWidget { + + private final MutableComponent textDepartureIn = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.departure_in").withStyle(ChatFormatting.BOLD); + private final UserSettings settings; + + private final CreateTimeSelectionWidget timeSelection; + private final Supplier> getUserSetting; + + public FlyoutDepartureInWidget(DLScreen screen, FlyoutPointer pointer, ColorShade pointerShade, Consumer addRenderableWidgetFunc, UserSettings settings, Supplier> getUserSetting, Consumer removeWidgetFunc) { + super(screen, 1, 60, pointer, pointerShade, addRenderableWidgetFunc, removeWidgetFunc); + set_width(Math.max(CreateTimeSelectionWidget.WIDHT + 16, font.width(textDepartureIn) + DLIconButton.DEFAULT_BUTTON_WIDTH + 16 + 10 + FlyoutPointer.WIDTH * 2)); + this.settings = settings; + this.getUserSetting = getUserSetting; + + this.timeSelection = addRenderableWidget(new CreateTimeSelectionWidget(getContentArea().getX() + 8, getContentArea().getY() + 24, DragonLib.TICKS_PER_DAY * 10)); + DLIconButton resetBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), getContentArea().getX() + getContentArea().getWidth() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, getContentArea().getY() + 2, TextUtils.empty(), (b) -> { + getUserSetting.get().setToDefault(); + timeSelection.setValue(getUserSetting.get().getValue()); + })); + + resetBtn.setBackColor(0x00000000); + + } + + @Override + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, contentArea); + GuiUtils.drawString(graphics, font, contentArea.getX() + 8, contentArea.getY() + 8, textDepartureIn, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + + @SuppressWarnings("resource") + @Override + public void open(IDragonLibWidget parent) { + this.timeSelection.setValue(getUserSetting.get().getValue()); + super.open(parent); + } + + @Override + public void close() { + DLUtils.doIfNotNull(settings, x -> { + getUserSetting.get().setValue(timeSelection.getValue()); + x.clientSave(super::close); + }); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java new file mode 100644 index 00000000..b7ca3265 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java @@ -0,0 +1,96 @@ +package de.mrjulsen.crn.client.gui.widgets.flyouts; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; +import de.mrjulsen.crn.client.gui.widgets.CRNListBox; +import de.mrjulsen.crn.client.gui.widgets.FlatCheckBox; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.UserSettings.UserSetting; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.MutableComponent; + +public class FlyoutTrainGroupsWidget extends AbstractFlyoutWidget { + + private final MutableComponent textTrainGroups = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups").withStyle(ChatFormatting.BOLD); + private final UserSettings settings; + + private final CRNListBox trainGroups; + private final Supplier>> getUserSetting; + + public FlyoutTrainGroupsWidget(DLScreen screen, FlyoutPointer pointer, ColorShade pointerShade, Consumer addRenderableWidgetFunc, UserSettings settings, Supplier>> getUserSetting, Consumer removeWidgetFunc) { + super(screen, 1, 120, pointer, pointerShade, addRenderableWidgetFunc, removeWidgetFunc); + set_width(Math.max(150, font.width(textTrainGroups) + DLIconButton.DEFAULT_BUTTON_WIDTH + 16 + 10 + FlyoutPointer.WIDTH * 2)); + this.settings = settings; + this.getUserSetting = getUserSetting; + + int top = getContentArea().getY() + 21; + int contentHeight = getContentArea().getHeight() - 21 - 2; + + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(screen, getContentArea().getX() + getContentArea().getWidth() - 7, top, contentHeight, GuiAreaDefinition.of(screen)); + this.trainGroups = addRenderableWidget(new CRNListBox<>(screen, getContentArea().getX() + 2, top, getContentArea().getWidth() - 4, contentHeight, scrollBar)); + addRenderableWidget(scrollBar); + DLIconButton resetBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), getContentArea().getX() + getContentArea().getWidth() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, getContentArea().getY() + 2, TextUtils.empty(), (b) -> { + getUserSetting.get().setToDefault(); + settings.clientSave(() -> reload(null)); + })); + + resetBtn.setBackColor(0x00000000); + } + + @Override + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, contentArea); + GuiUtils.drawString(graphics, font, contentArea.getX() + 8, contentArea.getY() + 8, textTrainGroups, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + + @Override + public void open(IDragonLibWidget parent) { + reload(() -> super.open(parent)); + } + + private void reload(Runnable andThen) { + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_TRAIN_GROUPS, (groups) -> { + trainGroups.displayData(new ArrayList<>(groups.stream().sorted((a, b) -> a.getGroupName().compareToIgnoreCase(b.getGroupName())).toList()), (group) -> { + FlatCheckBox cb = new FlatCheckBox(0, 0, 0, group.getGroupName(), getUserSetting.get().getValue().stream().noneMatch(x -> x.equals(group.getGroupName())), (b) -> {}); + return cb; + }); + DLUtils.doIfNotNull(andThen, x -> x.run()); + }); + } + + @Override + public void close() { + DLUtils.doIfNotNull(settings, x -> { + getUserSetting.get().setValue(new HashSet<>(trainGroups.getEntries().stream().filter(a -> !a.getKey().isChecked()).map(a -> a.getValue()).map(a -> a.getGroupName()).collect(Collectors.toSet()))); + x.clientSave(super::close); + }); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java new file mode 100644 index 00000000..01f6eb44 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java @@ -0,0 +1,75 @@ +package de.mrjulsen.crn.client.gui.widgets.flyouts; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; +import de.mrjulsen.crn.client.gui.widgets.create.CreateTimeSelectionWidget; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.UserSettings.UserSetting; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.MutableComponent; + +public class FlyoutTransferTimeWidget extends AbstractFlyoutWidget { + + private final MutableComponent textTransferTime = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.transfer_time").withStyle(ChatFormatting.BOLD); + private final UserSettings settings; + + private final Supplier> getUserSetting; + private final CreateTimeSelectionWidget timeSelection; + + public FlyoutTransferTimeWidget(DLScreen screen, FlyoutPointer pointer, ColorShade pointerShade, Consumer addRenderableWidgetFunc, UserSettings settings, Supplier> getUserSetting, Consumer removeWidgetFunc) { + super(screen, 1, 60, pointer, pointerShade, addRenderableWidgetFunc, removeWidgetFunc); + set_width(Math.max(CreateTimeSelectionWidget.WIDHT + 16, font.width(textTransferTime) + DLIconButton.DEFAULT_BUTTON_WIDTH + 16 + 10 + FlyoutPointer.WIDTH * 2)); + this.settings = settings; + this.getUserSetting = getUserSetting; + + this.timeSelection = addRenderableWidget(new CreateTimeSelectionWidget(getContentArea().getX() + 8, getContentArea().getY() + 24, DragonLib.TICKS_PER_DAY)); + DLIconButton resetBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), getContentArea().getX() + getContentArea().getWidth() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, getContentArea().getY() + 2, TextUtils.empty(), (b) -> { + getUserSetting.get().setToDefault(); + timeSelection.setValue(getUserSetting.get().getValue()); + })); + + resetBtn.setBackColor(0x00000000); + + } + + @Override + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, contentArea); + GuiUtils.drawString(graphics, font, contentArea.getX() + 8, contentArea.getY() + 8, textTransferTime, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + + @Override + public void open(IDragonLibWidget parent) { + this.timeSelection.setValue(getUserSetting.get().getValue()); + super.open(parent); + } + + @Override + public void close() { + DLUtils.doIfNotNull(settings, x -> { + getUserSetting.get().setValue(timeSelection.getValue()); + x.clientSave(super::close); + }); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/notifications/NotificationTrainInitialization.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/notifications/NotificationTrainInitialization.java new file mode 100644 index 00000000..44b44499 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/notifications/NotificationTrainInitialization.java @@ -0,0 +1,54 @@ +package de.mrjulsen.crn.client.gui.widgets.notifications; + +import java.util.function.Consumer; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractNotificationPopup; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.Util; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.MutableComponent; + +public class NotificationTrainInitialization extends AbstractNotificationPopup { + + private final float scale = 0.75f; + private final int maxTextWidth; + private final MutableComponent text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.train_initialization_warning"); + + public NotificationTrainInitialization(DLScreen screen, int x, int y, int width, Consumer removeWidgetFunc) { + super(screen, x, y, width, 1, ColorShade.LIGHT, removeWidgetFunc); + this.maxTextWidth = (int)(width / scale) - 10 - ModGuiIcons.ICON_SIZE - 12 - DLIconButton.DEFAULT_BUTTON_WIDTH * 2; + set_height((int)((ClientWrapper.getTextBlockHeight(font, text, maxTextWidth)) * scale) + 10); + set_y(y() - height()); + + DLIconButton closeBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.X.getAsSprite(16, 16), x() + width() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, y() + 2, DLIconButton.DEFAULT_BUTTON_WIDTH, height() - 4, TextUtils.empty(), (b) -> { + close(); + })); + DLIconButton helpBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.HELP.getAsSprite(16, 16), x() + width() - DLIconButton.DEFAULT_BUTTON_WIDTH * 2 - 2, y() + 2, DLIconButton.DEFAULT_BUTTON_WIDTH, height() - 4, TextUtils.empty(), (b) -> { + Util.getPlatform().openUri(Constants.HELP_PAGE_NAVIGATION_WARNING); + })); + closeBtn.setBackColor(0); + helpBtn.setBackColor(0); + } + + @Override + public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float partialTicks, GuiAreaDefinition contentArea) { + super.renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, contentArea); + ModGuiIcons.WARN.render(graphics, x() + 4, y() + 4); + graphics.poseStack().pushPose(); + graphics.poseStack().translate(x() + 5 + 2 + ModGuiIcons.ICON_SIZE, y() + 5, 0); + graphics.poseStack().scale(scale, scale, scale); + ClientWrapper.renderMultilineLabelSafe(graphics, 0, 0, font, text, maxTextWidth, 0xFFFF9999); + graphics.poseStack().popPose(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java new file mode 100644 index 00000000..82c18591 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java @@ -0,0 +1,166 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableList; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public abstract class AbstractDataListEntry> extends WidgetContainer { + + protected final DataListContainer parent; + + public final int CONTENT_POS_LEFT = 0; + public final int CONTENT_POS_RIGHT = 0; + public final int CONTENT_SPACING = 3; + public final int TEXT_OFFSET = 3; + + private boolean wasBuild = false; + + private int buttonsXOffset = 0; + private int sectionsXOffset = 0; + private List sections = new ArrayList<>(); + + protected final S data; + private String text; + + private final MutableComponent textDelete = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.delete"); + + public AbstractDataListEntry(DataListContainer parent, int x, int y, int width, S data) { + super(x, y, width, 20); + this.parent = parent; + this.data = data; + } + + public void setText(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public ImmutableList getSections() { + return ImmutableList.copyOf(sections); + } + + public int getCurrentButtonsXOffset() { + return buttonsXOffset; + } + + public int getCurrentSectionsXOffset() { + return sectionsXOffset; + } + + public W addWidget(W widget) { + if (wasBuild) { + throw new IllegalStateException("Cannot add elements to this widget after finishing creation."); + } + addRenderableWidget(widget); + buttonsXOffset += widget.width(); + return widget; + } + + public DLIconButton addButton(Sprite icon, Component text, DataListEntryContext onClick) { + if (wasBuild) { + throw new IllegalStateException("Cannot add elements to this widget after finishing creation."); + } + DLIconButton btn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, icon, x() + width() - DLIconButton.DEFAULT_BUTTON_WIDTH - buttonsXOffset - CONTENT_POS_RIGHT, y() + 1, TextUtils.empty(), + (b) -> { + onClick.run(b, this.parent.getData(), data, (newData) -> newData.ifPresent(a -> this.parent.displayData(a))); + })); + btn.setBackColor(0x00000000); + buttonsXOffset += btn.width(); + DLTooltip tooltip = DLTooltip.of(text).assignedTo(btn); + tooltip.setDynamicOffset(() -> (int)parent.getParentEntry().getParentList().getXScrollOffset(), () -> (int)parent.getParentEntry().getParentList().getYScrollOffset()); + parent.getTooltips().add(tooltip); + return btn; + } + + public DLIconButton addDeleteButton(DataListEntryContext onClick) { + return addButton(ModGuiIcons.DELETE.getAsSprite(16, 16), textDelete, onClick); + } + + protected E createSection(E section) { + if (wasBuild) { + throw new IllegalStateException("Cannot add elements to this widget after finishing creation."); + } + sections.add(section); + sectionsXOffset += section.width + CONTENT_SPACING; + return section; + } + + /** + * Called after creating this entry. Can be used to create edit boxes for editable sections and more. + */ + protected abstract void build(); + public final void buildInternal() { + wasBuild = true; + build(); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + renderWidgetBase(graphics, mouseX, mouseY, partialTicks); + for (E section : sections) { + int xCoord = x() + width() - CONTENT_POS_RIGHT - CONTENT_SPACING - buttonsXOffset - section.xOffset - section.width; + renderSection(graphics, mouseX, mouseY, partialTicks, section, new GuiAreaDefinition(xCoord, y() + 1, section.width, 18)); + } + int remainingWidth = width() - CONTENT_POS_LEFT - CONTENT_POS_RIGHT - CONTENT_SPACING - buttonsXOffset - sectionsXOffset; + int xCoord = x() + CONTENT_POS_LEFT; + renderMainSection(graphics, mouseX, mouseY, partialTicks, text, new GuiAreaDefinition(xCoord, y() + 1, remainingWidth, 18)); + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + protected abstract void renderWidgetBase(Graphics graphics, int mouseX, int mouseY, float partialTicks); + protected abstract void renderSection(Graphics graphics, int mouseX, int mouseY, float partialTicks, E section, GuiAreaDefinition area); + protected abstract void renderMainSection(Graphics graphics, int mouseX, int mouseY, float partialTicks, String text, GuiAreaDefinition area); + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + + @FunctionalInterface + public static interface DataListEntryContext { + void run(B btn, T data, S entry, Consumer> refreshAction); + } + + public static abstract class AbstractDataSectionDefinition { + public final int xOffset; + public final int width; + + public AbstractDataSectionDefinition(int xOffset, int width) { + this.xOffset = xOffset; + this.width = width; + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java new file mode 100644 index 00000000..26075911 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java @@ -0,0 +1,114 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class DLOptionsList extends ScrollableWidgetContainer { + + private final DLAbstractScrollBar scrollBar; + private int contentHeight = 0; + + private final Screen parent; + + public DLOptionsList(Screen parent, int x, int y, int width, int height, DLAbstractScrollBar scrollBar) { + super(x, y, width, height); + this.scrollBar = scrollBar; + this.parent = parent; + + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(height()); + scrollBar.updateMaxScroll(0); + scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } + + public void clearOptions() { + clearWidgets(); + rearrangeContent(); + } + + public int getContentWidth() { + return width() - 20; + } + + public OptionEntry addOption(Function, T> contentContainer, Component text, Component description, BiConsumer, OptionEntryHeader> onHeaderClick, Function onTitleEdited) { + OptionEntry entry = new OptionEntry(parent, this, 0, y(), width() - 20, contentContainer, text, description, x -> rearrangeContent(), onHeaderClick, onTitleEdited); + addRenderableWidget(entry); + return entry; + } + + @Override + public T addRenderableWidget(T guiEventListener) { + T t = super.addRenderableWidget(guiEventListener); + if (t instanceof IDragonLibWidget wgt) { + wgt.set_x(x() + 10); + wgt.set_width(Math.min(width() - 20, wgt.width())); + } else if (t instanceof AbstractWidget wgt) { + wgt.x = x() + 10; + wgt.setWidth(Math.min(width() - 20, wgt.getWidth())); + } + rearrangeContent(); + return t; + } + + public void rearrangeContent() { + contentHeight = 10; + for (GuiEventListener listener : children()) { + if (listener instanceof OptionEntry wgt) { + wgt.set_y(y() + contentHeight); + contentHeight += wgt.height() + (wgt.isExpanded() ? 6 : 3); + } else if (listener instanceof IDragonLibWidget wgt) { + wgt.set_y(y() + contentHeight); + contentHeight += wgt.height() + 3; + } else if (listener instanceof AbstractWidget wgt) { + wgt.y = y() + contentHeight; + contentHeight += wgt.getHeight() + 3; + } + } + contentHeight += 10; + scrollBar.updateMaxScroll(contentHeight); + if (!scrollBar.canScroll()) { + scrollBar.scrollTo(0); + } + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + if (children().isEmpty()) { + GuiUtils.drawString(graphics, font, x() + width() / 2, y() + height() / 2 - font.lineHeight / 2, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".empty_list"), DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED, EAlignment.CENTER, false); + } + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java new file mode 100644 index 00000000..42e4e0e5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java @@ -0,0 +1,247 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.lwjgl.glfw.GLFW; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; + +public class DataListContainer extends WidgetContainer { + + private static final int BORDER_WIDTH = 2; + + private final Screen parent; + private final OptionEntry parentEntry; + private T data; + private int entriesHeight; + private int addNewEntryHeight; + private int contentHeight; + + private int paddingLeft; + private int paddingRight; + private int paddingTop; + private int paddingBottom; + private boolean renderBackground = true; + private boolean bordered = true; + + private final Collection tooltips = new ArrayList<>(); + + private final Function> onGetData; + private final BiFunction, String> onCreateEntry; + private final Consumer> onContainerSizeChanged; + private final BiConsumer> createNewEntry; + + private String filterText = ""; + private BiPredicate> filter; + + + + /** + * @param parent The parent screen. + * @param x The x position. + * @param y The y position. + * @param width The width of this widget. + * @param initialData The data which this widget should display and process. + * @param dataIterator The iterator of the contents of the data. + * @param onCreateEntry Called when creating the entries. + * @param createNewEntry Called when creating the "Add New Entry" widget. Pass {@code null} if no "Add New Entry" widget should be created. + * @param onContainerSizeChanged Called when the size of the container changes to adjust the gui surrounding it. + */ + public DataListContainer(OptionEntry parentEntry, int x, int y, int width, T initialData, Function> dataIterator, BiFunction, String> onCreateEntry, BiConsumer> createNewEntry, Consumer> onContainerSizeChanged) { + super(x, y, width, 100); + this.parent = parentEntry.getParentScreen(); + this.parentEntry = parentEntry; + + this.onGetData = dataIterator; + this.onCreateEntry = onCreateEntry; + this.onContainerSizeChanged = onContainerSizeChanged; + this.createNewEntry = createNewEntry; + + displayData(initialData, false); + } + + public void setPadding(int top, int right, int bottom, int left) { + this.paddingTop = top; + this.paddingRight = right; + this.paddingBottom = bottom; + this.paddingLeft = left; + displayData(data, false); + } + + public void setFilter(BiPredicate> filter) { + this.filter = filter; + displayData(data, false); + } + + public int getPaddingLeft() { + return paddingLeft; + } + + public int getPaddingRight() { + return paddingRight; + } + + public int getPaddingTop() { + return paddingTop; + } + + public int getPaddingBottom() { + return paddingBottom; + } + + public boolean isRenderBackground() { + return renderBackground; + } + + public void setRenderBackground(boolean renderBackground) { + this.renderBackground = renderBackground; + } + + public boolean isBordered() { + return bordered; + } + + public void setBordered(boolean bordered) { + this.bordered = bordered; + } + + public int displayData(T data) { + return displayData(data, true); + } + + private int displayData(T data, boolean notifySizeChanged) { + clearWidgets(); + tooltips.clear(); + this.data = data; + contentHeight = paddingTop + BORDER_WIDTH; + entriesHeight = 0; + addNewEntryHeight = 0; + + MutableSingle searchBox = new MutableSingle<>(null); + DLUtils.doIfNotNull(filter, x -> { + searchBox.setFirst(addRenderableWidget(new DLEditBox(font, x() + BORDER_WIDTH + paddingLeft + 1, y() + 2 + contentHeight, width() - BORDER_WIDTH * 2 - paddingLeft - paddingRight - 2, 14, TextUtils.empty()) { + @Override + public boolean keyPressed(int code, int p_keyPressed_2_, int p_keyPressed_3_) { + if (code == GLFW.GLFW_KEY_ENTER) { + filterText = getValue(); + displayData(data, true); + return true; + } + return super.keyPressed(code, p_keyPressed_2_, p_keyPressed_3_); + } + })); + searchBox.getFirst().withHint(DragonLib.TEXT_SEARCH); + searchBox.getFirst().setValue(filterText); + int h = searchBox.getFirst().height() + 4; + contentHeight += h; + entriesHeight += h; + addNewEntryHeight += h; + }); + + Iterator content = onGetData.apply(data); + while (content.hasNext()) { + final S current = content.next(); + if (filter != null) { + if (!filter.test(current, () -> searchBox.getFirst() == null ? "" : searchBox.getFirst().getValue())) { + continue; + } + } + SimpleDataListEntry entry = addRenderableWidget(new SimpleDataListEntry<>(this, x() + BORDER_WIDTH + paddingLeft, y() + contentHeight, width() - BORDER_WIDTH * 2 - paddingLeft - paddingRight, current)); + entry.setText(onCreateEntry.apply(current, entry)); + entry.buildInternal(); + contentHeight += entry.height(); + entriesHeight += entry.height(); + } + + DLUtils.doIfNotNull(createNewEntry, x -> { + SimpleDataListNewEntry entry = addRenderableWidget(new SimpleDataListNewEntry<>(this, x() + BORDER_WIDTH + paddingLeft, y() + contentHeight, width() - BORDER_WIDTH * 2 - paddingLeft - paddingRight)); + createNewEntry.accept(data, entry); + entry.buildInternal(); + contentHeight += entry.height(); + addNewEntryHeight = entry.height(); + }); + + contentHeight += paddingBottom + BORDER_WIDTH; + set_height(contentHeight); + + if (notifySizeChanged) { + onContainerSizeChanged.accept(this); + } + return height(); + } + + public Screen getParentScreen() { + return parent; + } + + public OptionEntry getParentEntry() { + return parentEntry; + } + + public T getData() { + return data; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + if (isRenderBackground()) { + if (isBordered()) { + if (createNewEntry == null) { + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.LIGHT); + } else { + CreateDynamicWidgets.renderDuoShadeWidget(graphics, x(), y(), width(), BORDER_WIDTH + paddingTop + entriesHeight, ColorShade.LIGHT, BORDER_WIDTH + paddingBottom + addNewEntryHeight, ColorShade.DARK); + } + } else { + GuiUtils.fill(graphics, x(), y(), width(), height(), ColorShade.LIGHT.getColor()); + if (createNewEntry != null) { + GuiUtils.fill(graphics, x(), y() + BORDER_WIDTH + paddingTop + entriesHeight, width(), BORDER_WIDTH + paddingBottom + addNewEntryHeight, ColorShade.DARK.getColor()); + } + } + } + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + public Collection getTooltips() { + return tooltips; + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + tooltips.stream().forEach(x -> x.render(parent, graphics, mouseX, mouseY)); + + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java new file mode 100644 index 00000000..8412ca17 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java @@ -0,0 +1,132 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.function.Function; +import java.util.List; +import java.util.function.Supplier; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.DLWidgetsCollection; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Pair; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.MutableComponent; + +public class NewEntryWidget extends WidgetContainer { + + private final DLEditBox nameBox; + private final DLIconButton addBtn; + private final DLWidgetsCollection collection = new DLWidgetsCollection(); + private final Screen parent; + private final Supplier> scrollOffset; + + private final MutableComponent textAdd = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".new_entry.add"); + private final MutableComponent textNew = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".new_entry.new").append(" "); + + private boolean expanded = false; + + public NewEntryWidget(Screen parent, Supplier> scrollOffset, Function onAcceptNewName, int x, int y, int width) { + super(x, y, width, 26); + this.parent = parent; + this.scrollOffset = scrollOffset; + + int w = width() - 12 - DLIconButton.DEFAULT_BUTTON_WIDTH * 2 - font.width(textNew); + nameBox = addRenderableWidget(new DLEditBox(font, x() + 10 + font.width(textNew), y() + 9, w - 8, font.lineHeight, TextUtils.empty())); + nameBox.setBordered(false); + nameBox.setMaxLength(StationTag.MAX_NAME_LENGTH); + + addBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.ADD.getAsSprite(16, 16), x(), y() + height() / 2 - DLIconButton.DEFAULT_BUTTON_WIDTH / 2, TextUtils.empty(), + (b) -> { + expanded = true; + b.set_visible(false); + collection.setVisible(true); + nameBox.setValue(""); + }) { + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.DARK); + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + }); + addBtn.setBackColor(0x00000000); + + DLIconButton acceptButton = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.CHECKMARK.getAsSprite(16, 16), x() + width() - DLIconButton.DEFAULT_BUTTON_WIDTH * 2 - 5, y() + 4, TextUtils.empty(), + (b) -> { + if (nameBox.getValue() == null || nameBox.getValue().isBlank()) { + return; + } + DLUtils.doIfNotNull(onAcceptNewName, a -> { + if (a.apply(nameBox.getValue())) { + collapse(); + } + }); + })); + acceptButton.setBackColor(0x00000000); + + DLIconButton denyButton = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.X.getAsSprite(16, 16), x() + width() - DLIconButton.DEFAULT_BUTTON_WIDTH - 4, y() + 4, TextUtils.empty(), + (b) -> { + collapse(); + })); + denyButton.setBackColor(0x00000000); + + collection.add(nameBox); + collection.add(acceptButton); + collection.add(denyButton); + collection.setVisible(false); + } + + private void collapse() { + expanded = false; + collection.setVisible(false); + addBtn.set_visible(true); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + if (expanded) { + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.DARK); + int w = width() - 12 - DLIconButton.DEFAULT_BUTTON_WIDTH * 2 - font.width(textNew); + CreateDynamicWidgets.renderTextBox(graphics, x() + 5 + font.width(textNew), y() + 4, w); + GuiUtils.drawString(graphics, font, x() + 5, y() + height() / 2 - font.lineHeight / 2, textNew, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (addBtn.visible() && addBtn.isMouseSelected()) { + GuiUtils.renderTooltipAt(parent, GuiAreaDefinition.of(parent), List.of(textAdd), parent.width / 4, graphics, addBtn.x() + addBtn.width() + 5 + scrollOffset.get().getFirst().intValue(), addBtn.y() + 1 + scrollOffset.get().getSecond().intValue(), mouseX, mouseY, 0, 0); + } + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java new file mode 100644 index 00000000..8af99032 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java @@ -0,0 +1,245 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import com.mojang.blaze3d.systems.RenderSystem; + +import java.util.List; + +import de.mrjulsen.crn.client.gui.Animator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.Style; + +public class OptionEntry extends WidgetContainer { + + public static void expandOrCollapse(OptionEntry entry) { + if (entry.isExpanded()) + entry.collapse(); + else + entry.expand(); + } + + private int btnX = 0; + private final Collection additionalButtons = new ArrayList<>(); + private final Collection tooltips = new ArrayList<>(); + private List descriptionTooltips; + + private final int initialHeight = OptionEntryHeader.DEFAULT_HEIGHT; + private final Component description; + private Component text; + + private final Screen parent; + private final DLOptionsList parentList; + private final T contentContainer; + private final Consumer> onSizeChanged; + private final OptionEntryHeader header; + private DLEditBox editBox; + private final Animator animator = addRenderableOnly(new Animator()); + + private boolean expanded; + + public OptionEntry(Screen parent, DLOptionsList parentList, int x, int y, int width, Function, T> contentContainer, Component text, Component description, Consumer> onSizeChanged, BiConsumer, OptionEntryHeader> onHeaderClick, Function onTitleEdited) { + super(x, y, width, OptionEntryHeader.DEFAULT_HEIGHT); + this.parent = parent; + this.parentList = parentList; + this.text = text; + this.contentContainer = contentContainer != null ? contentContainer.apply(this) : null; + this.description = description; + this.onSizeChanged = onSizeChanged; + this.setTooltip(font.getSplitter().splitLines(description, width, Style.EMPTY)); + + header = addRenderableWidget(new OptionEntryHeader(this, x(), y(), width(), text, (b) -> onHeaderClick.accept(this, b))); + DLUtils.doIfNotNull(this.contentContainer, a -> { + addRenderableWidget(a); + a.set_visible(false); + }); + + if (onTitleEdited != null) { + editBox = addRenderableWidget(new DLEditBox(font, x() + 5, y() + 6, width() - 5 - 25, font.lineHeight, text) { + @Override + public void setMouseSelected(boolean selected) { + super.setMouseSelected(selected); + if (selected) { + header.setMouseSelected(true); + } + } + }); + editBox.setValue(text.getString()); + editBox.setBordered(false); + editBox.setMaxLength(StationTag.MAX_NAME_LENGTH); + editBox.set_visible(false); + editBox.withOnFocusChanged((box, focus) -> { + if (!focus) { + if (onTitleEdited.apply(box.getValue())) { + this.text = TextUtils.text(box.getValue()); + header.setMessage(this.text); + } + } + }); + + } + + animator.start(4, null, null, null); + } + + public void addAdditionalButton(Sprite icon, Component text, BiConsumer, DLIconButton> onClick) { + DLIconButton btn = new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, icon, x() + width() - 2 - 18 - btnX - 16, y() + 2, 16, OptionEntryHeader.DEFAULT_HEIGHT - 4, TextUtils.empty(), x -> onClick.accept(this, x)) { + @Override + public void setMouseSelected(boolean selected) { + super.setMouseSelected(selected); + if (selected) { + header.setMouseSelected(true); + } + } + }; + btn.setBackColor(0x00000000); + DLTooltip tooltip = DLTooltip.of(text).assignedTo(btn); + tooltip.setDynamicOffset(() -> (int)parentList.getXScrollOffset(), () -> (int)parentList.getYScrollOffset()); + tooltips.add(tooltip); + btnX += btn.width(); + addRenderableWidget(btn); + additionalButtons.add(btn); + } + + public OptionEntry(Screen parent, DLOptionsList parentList, int x, int y, int width, Function, T> contentContainer, Component text, Component description, Consumer> onExpandedChanged, BiConsumer, OptionEntryHeader> onHeaderClick) { + this(parent, parentList, x, y, width, contentContainer, text, description, onExpandedChanged, onHeaderClick, null); + } + + public GuiAreaDefinition getContentSpace() { + return new GuiAreaDefinition(x() + 3, y() + initialHeight, width() - 6, height() - initialHeight - 2); + } + + public void collapse() { + expanded = false; + set_height(initialHeight); + DLUtils.doIfNotNull(editBox, a -> { + a.setVisible(false); + a.setWidth(width() - 5 - 5 - 18 - btnX); + }); + DLUtils.doIfNotNull(contentContainer, a -> a.set_visible(false)); + onSizeChanged.accept(this); + } + + public void expand() { + expanded = true; + DLUtils.doIfNotNull(contentContainer, a -> { + set_height(initialHeight + a.height() + 2); + a.set_visible(true); + }); + DLUtils.doIfNotNull(editBox, a -> a.setVisible(true)); + onSizeChanged.accept(this); + } + + public void notifyContentSizeChanged() { + DLUtils.doIfNotNull(contentContainer, a -> { + set_height(initialHeight + a.height() + 2); + }); + onSizeChanged.accept(this); + } + + public int getInitialHeight() { + return initialHeight; + } + + public Component getText() { + return text; + } + + public Component getDescription() { + return description; + } + + public boolean isExpanded() { + return expanded; + } + + public T getContentContainer() { + return contentContainer; + } + + public void setTooltip(List tooltip) { + this.descriptionTooltips = tooltip; + } + + public List getTooltips() { + return descriptionTooltips; + } + + public DLOptionsList getParentList() { + return parentList; + } + + public Screen getParentScreen() { + return parent; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + graphics.poseStack().pushPose(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + + if (animator.isRunning()) { + graphics.poseStack().translate(-animator.getTotalTicks() * 5 + animator.getCurrentTicksSmooth() * 5, 0, 0); + GuiUtils.setTint(1, 1, 1, animator.getPercentage()); + } + + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x() + 1, y(), width() - 2, height(), ColorShade.LIGHT); + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + DLUtils.doIfNotNull(editBox, a -> { + if (a.visible()) { + CreateDynamicWidgets.renderTextBox(graphics, x() + 1, y() + 1, width() - 2 - 2 - 20 - btnX); + //GuiUtils.fill(graphics, x() + 2, y() + 2, width() - 2 - 2 - 18 - btnX, OptionEntryHeader.DEFAULT_HEIGHT - 4, 0xFF000000); + editBox.render(graphics.poseStack(), mouseX, mouseY, partialTicks); + } + }); + if (isExpanded() && contentContainer != null) { + GuiUtils.fillGradient(graphics, x() + 3, y() + 20, 0, width() - 6, 10, 0x77000000, 0x00000000); + } + graphics.poseStack().popPose(); + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + tooltips.stream().forEach(x -> x.render(parent, graphics, mouseX, mouseY)); + + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntryHeader.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntryHeader.java new file mode 100644 index 00000000..89bb6d0a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntryHeader.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.function.Consumer; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.GuiIcons; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; + +public class OptionEntryHeader extends DLButton { + +public static final int DEFAULT_HEIGHT = 20; + + private final OptionEntry parent; + + public OptionEntryHeader(OptionEntry parent, int pX, int pY, int pWidth, Component pMessage, Consumer pOnPress) { + super(pX, pY, pWidth, DEFAULT_HEIGHT, pMessage, pOnPress); + this.parent = parent; + setRenderStyle(AreaStyle.FLAT); + setBackColor(0x00000000); + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), height(), ColorShade.LIGHT); + CreateDynamicWidgets.renderSingleShadeWidget(graphics, x(), y(), width(), 20, ColorShade.DARK); + DynamicGuiRenderer.renderArea(graphics, x(), y(), width, height, getBackColor(), style, isActive() ? (isFocused() || isMouseSelected() ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + int j = active ? (isMouseSelected() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_HIGHLIGHT : DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE) : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; + GuiUtils.drawString(graphics, font, x() + 5, this.y() + (20 - 8) / 2, this.getMessage(), j, EAlignment.LEFT, false); + if (parent.isExpanded()) { + GuiIcons.ARROW_UP.render(graphics, x() + width() - 2 - GuiIcons.ICON_SIZE, y() + 2); + } else { + if (parent.getContentContainer() == null) { + GuiIcons.ARROW_RIGHT.render(graphics, x() + width() - 2 - GuiIcons.ICON_SIZE, y() + 2); + } else { + GuiIcons.ARROW_DOWN.render(graphics, x() + width() - 2 - GuiIcons.ICON_SIZE, y() + 2); + } + } + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + + if (isMouseSelected()) { + renderDescriptionTooltip(graphics, mouseX, mouseY, partialTicks); + } + } + + protected void renderDescriptionTooltip(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + DLUtils.doIfNotNull(parent.getTooltips(), t -> { + final float scale = 0.75f; + graphics.poseStack().pushPose(); + graphics.poseStack().translate(mouseX, mouseY, 0); + graphics.poseStack().scale(scale, scale, 1); + int maxLineWidth = t.stream().mapToInt(x -> font.width(x)).max().orElse(0); + GuiUtils.fill(graphics, 8, 12, maxLineWidth + 6, (font.lineHeight + 2) * t.size() + 4, 0xAA000000); + for (int i = 0; i < t.size(); i++) { + FormattedText line = t.get(i); + GuiUtils.drawString(graphics, font, 12, 16 + (font.lineHeight + 2) * i, line, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + graphics.poseStack().popPose(); + }); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListEntry.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListEntry.java new file mode 100644 index 00000000..2801d8a8 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListEntry.java @@ -0,0 +1,113 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.widgets.options.AbstractDataListEntry.AbstractDataSectionDefinition; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; + +public class SimpleDataListEntry extends AbstractDataListEntry> { + + public SimpleDataListEntry(DataListContainer parent, int x, int y, int width, S data) { + super(parent, x, y, width, data); + } + + /** + * Adds a new data section. + * @param width The width of the sections + * @param displayName The display text of the section. + * @param onEdit Called when editing this value. {@code null} if this value should not be editable. + */ + public void addDataSection(int width, Function displayName, EAlignment alignment, DataListEntryEditContext onEdit) { + createSection(new DisplayableDataSectionDefinition<>(getCurrentSectionsXOffset(), width, displayName.apply(data), alignment, onEdit)); + } + + @Override + protected void build() { + for (DisplayableDataSectionDefinition section : getSections()) { + if (section.onEdit == null) { + continue; + } + + final DisplayableDataSectionDefinition dataSection = section; + int xCoord = x() + width() - CONTENT_POS_RIGHT - CONTENT_SPACING - getCurrentButtonsXOffset() - section.xOffset - section.width; + DLEditBox modifyPlatformInput = addRenderableWidget(new DLEditBox(font, xCoord + 1, y() + 2, section.width - 2, height() - 4, TextUtils.empty())); + DLButton modifyPlatformBtn = addRenderableWidget(new DLButton(xCoord, y() + 1, section.width, height() - 2, TextUtils.empty(), + (b) -> { + b.set_visible(false); + modifyPlatformInput.setValue(dataSection.displayName); + modifyPlatformInput.set_visible(true); + })); + modifyPlatformBtn.setRenderStyle(AreaStyle.FLAT); + modifyPlatformBtn.setBackColor(0x00000000); + + modifyPlatformInput.setValue(""); + modifyPlatformInput.set_visible(false); + modifyPlatformInput.withOnFocusChanged((box, focus) -> { + if (box.visible() && !focus) { + dataSection.onEdit.run(parent.getData(), data, box.getValue(), (newData) -> newData.ifPresent(a -> this.parent.displayData(a))); + modifyPlatformInput.setValue(""); + modifyPlatformInput.set_visible(false); + modifyPlatformBtn.set_visible(true); + } + }); + } + } + + @Override + protected void renderWidgetBase(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + } + + @Override + protected void renderSection(Graphics graphics, int mouseX, int mouseY, float partialTicks, DisplayableDataSectionDefinition section, GuiAreaDefinition area) { + CreateDynamicWidgets.renderTextSlotOverlay(graphics, area.getX(), area.getY(), area.getWidth(), area.getHeight()); + switch (section.alignment) { + case RIGHT: + GuiUtils.drawString(graphics, font, area.getX() + area.getWidth() - TEXT_OFFSET, area.getY() + area.getHeight() / 2 - font.lineHeight / 2, GuiUtils.ellipsisString(font, TextUtils.text(section.displayName), area.getWidth() - TEXT_OFFSET * 2), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.RIGHT, false); + break; + case CENTER: + GuiUtils.drawString(graphics, font, area.getX() + TEXT_OFFSET + (area.getWidth() - TEXT_OFFSET * 2) / 2, area.getY() + area.getHeight() / 2 - font.lineHeight / 2, GuiUtils.ellipsisString(font, TextUtils.text(section.displayName), area.getWidth() - TEXT_OFFSET * 2), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.CENTER, false); + break; + default: + case LEFT: + GuiUtils.drawString(graphics, font, area.getX() + TEXT_OFFSET, area.getY() + area.getHeight() / 2 - font.lineHeight / 2, GuiUtils.ellipsisString(font, TextUtils.text(section.displayName), area.getWidth() - TEXT_OFFSET * 2), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + break; + } + } + + @Override + protected void renderMainSection(Graphics graphics, int mouseX, int mouseY, float partialTicks, String text, GuiAreaDefinition area) { + CreateDynamicWidgets.renderTextSlotOverlay(graphics, area.getX(), area.getY(), area.getWidth(), area.getHeight()); + GuiUtils.drawString(graphics, font, area.getX() + TEXT_OFFSET, area.getY() + area.getHeight() / 2 - font.lineHeight / 2, GuiUtils.ellipsisString(font, TextUtils.text(text), area.getWidth() - TEXT_OFFSET * 2), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + } + + + + @FunctionalInterface + public static interface DataListEntryEditContext { + void run(T data, S entry, String newValue, Consumer> refreshAction); + } + + public static class DisplayableDataSectionDefinition extends AbstractDataSectionDefinition { + private final String displayName; + private final EAlignment alignment; + private final DataListEntryEditContext onEdit; + + public DisplayableDataSectionDefinition(int xOffset, int width, String displayName, EAlignment alignment, DataListEntryEditContext onEdit) { + super(xOffset, width); + this.displayName = displayName; + this.alignment = alignment; + this.onEdit = onEdit; + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListNewEntry.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListNewEntry.java new file mode 100644 index 00000000..27f525b5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/SimpleDataListNewEntry.java @@ -0,0 +1,128 @@ +package de.mrjulsen.crn.client.gui.widgets.options; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableMap; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.widgets.options.AbstractDataListEntry.AbstractDataSectionDefinition; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; + +public class SimpleDataListNewEntry extends AbstractDataListEntry> { + + public static final String MAIN_INPUT_KEY = "name"; + + public Map> values = new HashMap<>(); + + private Consumer onCreateMainEditBox; + private Function onSetNameEditBoxTooltip; + + public SimpleDataListNewEntry(DataListContainer parent, int x, int y, int width) { + super(parent, x, y, width, null); + } + + /** + * Adds a new data section. + * @param width The width of the sections + * @param displayName The display text of the section. + * @param onEdit Called when editing this value. {@code null} if this value should not be editable. + */ + public void addDataSection(int width, String key, Component text, Consumer onCreateEditBox) { + createSection(new InputDataSectionDefinition<>(getCurrentSectionsXOffset(), width, key, text, onCreateEditBox)); + } + + public void addAddButton(Sprite icon, Component description, DataListAddNewEntryContext onClick) { + addButton(icon, description, + (btn, data, entry, refreshAction) -> { + if (onClick.run(btn, parent.getData(), ImmutableMap.copyOf(values), refreshAction)) { + children().stream().filter(x -> x instanceof EditBox).map(x -> (EditBox)x).forEach(x -> x.setValue("")); + } + }); + } + + public void editNameEditBox(Consumer onCreateMainEditBox) { + this.onCreateMainEditBox = onCreateMainEditBox; + } + + public void setNameEditBoxTooltip(Function onSetNameEditBoxTooltip) { + this.onSetNameEditBoxTooltip = onSetNameEditBoxTooltip; + } + + @Override + protected void build() { + values.clear(); + for (InputDataSectionDefinition section : getSections()) { + int xCoord = x() + width() - CONTENT_POS_RIGHT - CONTENT_SPACING - getCurrentButtonsXOffset() - section.xOffset - section.width; + DLEditBox inputBox = addRenderableWidget(new DLEditBox(font, xCoord + 4, y() + 5, section.width - 8, height() - 10, TextUtils.empty())); + inputBox.setValue(""); + inputBox.setBordered(false); + DLUtils.doIfNotNull(section.onCreateEditBox, x -> x.accept(inputBox)); + values.put(section.key, () -> inputBox.getValue()); + DLTooltip tooltip = DLTooltip.of(section.text).assignedTo(inputBox); + tooltip.setDynamicOffset(() -> (int)parent.getParentEntry().getParentList().getXScrollOffset(), () -> (int)parent.getParentEntry().getParentList().getYScrollOffset()); + parent.getTooltips().add(tooltip); + } + + int remainingWidth = width() - CONTENT_POS_LEFT - CONTENT_POS_RIGHT - CONTENT_SPACING - getCurrentButtonsXOffset() - getCurrentSectionsXOffset(); + int xCoord = x() + CONTENT_POS_LEFT; + DLEditBox inputBox = addRenderableWidget(new DLEditBox(font, xCoord + 4, y() + 5, remainingWidth - 8, height() - 10, TextUtils.empty())); + inputBox.setValue(""); + inputBox.setBordered(false); + DLUtils.doIfNotNull(onCreateMainEditBox, x -> x.accept(inputBox)); + DLUtils.doIfNotNull(onSetNameEditBoxTooltip, x -> { + Component text = x.apply(inputBox); + DLTooltip tooltip = DLTooltip.of(text).assignedTo(inputBox); + tooltip.setDynamicOffset(() -> (int)parent.getParentEntry().getParentList().getXScrollOffset(), () -> (int)parent.getParentEntry().getParentList().getYScrollOffset()); + parent.getTooltips().add(tooltip); + }); + values.put(MAIN_INPUT_KEY, () -> inputBox.getValue()); + } + + @Override + protected void renderWidgetBase(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + } + + @Override + protected void renderSection(Graphics graphics, int mouseX, int mouseY, float partialTicks, InputDataSectionDefinition section, GuiAreaDefinition area) { + CreateDynamicWidgets.renderTextBox(graphics, area.getX(), area.getY(), area.getWidth()); + } + + @Override + protected void renderMainSection(Graphics graphics, int mouseX, int mouseY, float partialTicks, String text, GuiAreaDefinition area) { + CreateDynamicWidgets.renderTextBox(graphics, area.getX(), area.getY(), area.getWidth()); + } + + + @FunctionalInterface + public static interface DataListAddNewEntryContext { + boolean run(B btn, T data, ImmutableMap> inputValues, Consumer> refreshAction); + } + + public static class InputDataSectionDefinition extends AbstractDataSectionDefinition { + private final String key; + private final Component text; + private final Consumer onCreateEditBox; + + public InputDataSectionDefinition(int xOffset, int width, String key, Component text, Consumer onCreateEditBox) { + super(xOffset, width); + this.key = key; + this.text = text; + this.onCreateEditBox = onCreateEditBox; + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java new file mode 100644 index 00000000..f9fb08a7 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java @@ -0,0 +1,55 @@ +package de.mrjulsen.crn.client.gui.widgets.routedetails; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.navigation.TransferConnection; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; + +public class RouteDetailsTransferWidget extends DLRenderable { + + private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.transfer"); + + protected static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/widgets.png"); + protected static final int GUI_TEXTURE_WIDTH = 256; + protected static final int GUI_TEXTURE_HEIGHT = 256; + protected static final int ENTRY_WIDTH = 225; + + private final MutableComponent textConnectionEndangered = ELanguage.translate("gui.createrailwaysnavigator.route_overview.connection_endangered").withStyle(ChatFormatting.GOLD).withStyle(ChatFormatting.BOLD); + private final MutableComponent textConnectionMissed = ELanguage.translate("gui.createrailwaysnavigator.route_overview.connection_missed").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD); + + private final TransferConnection connection; + + public RouteDetailsTransferWidget(int x, int y, int width, TransferConnection connection) { + super(x, y, width, 24); + this.connection = connection; + } + + @SuppressWarnings("resource") + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + + long time = connection.getDepartureStation().getScheduledDepartureTime() - connection.getArrivalStation().getScheduledArrivalTime(); + GuiUtils.drawTexture(GUI, graphics, x(), y(), ENTRY_WIDTH, height(), 0, 155, ENTRY_WIDTH, height(), GUI_TEXTURE_WIDTH, GUI_TEXTURE_HEIGHT); + + if (connection.isConnectionMissed()) { + ModGuiIcons.CROSS.render(graphics, x() + 24, y + 4); + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x() + 28 + ModGuiIcons.ICON_SIZE + 2, y + 8, textConnectionMissed, 0xFFFFFFFF, EAlignment.LEFT, false); + } else if (connection.isConnectionEndangered()) { + ModGuiIcons.WARN.render(graphics, x() + 24, y + 4); + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x() + 28 + ModGuiIcons.ICON_SIZE + 2, y + 8, textConnectionEndangered, 0xFFFFFFFF, EAlignment.LEFT, false); + } else { + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x() + 32, y + 8, TextUtils.text(transferText.getString() + " " + (time < 0 ? "" : "(" + TimeUtils.parseDuration(time) + ")")), 0xFFFFFF, EAlignment.LEFT, false); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java new file mode 100644 index 00000000..14117750 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java @@ -0,0 +1,114 @@ +package de.mrjulsen.crn.client.gui.widgets.routedetails; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.screen.ScheduleBoardScreen; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.ResourceLocation; + +public class RoutePartEntryWidget extends DLButton { + + protected static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/widgets.png"); + protected static final int GUI_TEXTURE_WIDTH = 256; + protected static final int GUI_TEXTURE_HEIGHT = 256; + protected static final int ENTRY_WIDTH = 225; + + private final ClientRoutePart part; + private final ClientTrainStop stop; + private final TrainStopType type; + private boolean valid; + + public RoutePartEntryWidget(Screen parent, ClientRoutePart part, ClientTrainStop stop, int pX, int pY, int width, TrainStopType type, boolean valid) { + super(pX, pY, width, type.h, TextUtils.empty(), (b) -> { + DLScreen.setScreen(new ScheduleBoardScreen(parent, stop.getClientTag())); + }); + this.part = part; + this.stop = stop; + this.type = type; + this.valid = valid; + } + + public void setValid(boolean b) { + this.valid = b; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + GuiUtils.drawTexture(GUI, graphics, x(), y(), ENTRY_WIDTH, height(), 0, type.v, ENTRY_WIDTH, height(), GUI_TEXTURE_WIDTH, GUI_TEXTURE_HEIGHT); + renderData(graphics, y() + type.dy); + + if (isMouseSelected()) { + GuiUtils.fill(graphics, x() + 24, y() + type.dy - 1, 199, 20, 0x22FFFFFF); + } + } + + protected void renderData(Graphics graphics, int y) { + final float scale = 0.75f; + String platformText = stop.getClientTag().info().platform(); + String nameText = stop.getClientTag().tagName(); + int maxStationNameWidth = 138 - 8 - font.width(platformText) - 6; + + if (font.width(nameText) > maxStationNameWidth) { + GuiUtils.drawString(graphics, font, x() + 80, y + 5, TextUtils.text(font.substrByWidth(TextUtils.text(stop.getClientTag().tagName()), maxStationNameWidth).getString()).append(Constants.ELLIPSIS_STRING), 0xFFFFFFFF, EAlignment.LEFT, false); + } else { + GuiUtils.drawString(graphics, font, x() + 80, y + 5, nameText, 0xFFFFFFFF, EAlignment.LEFT, false); + } + GuiUtils.drawString(graphics, font, x() + 213, y + 5, platformText, 0xFFFFFFFF, EAlignment.RIGHT, false); + + graphics.poseStack().pushPose(); + graphics.poseStack().scale(scale, scale, 1); + + int precision = ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(); + + if (this.type == TrainStopType.TRANSIT) { + graphics.poseStack().translate((x() + 28) / scale, (y + 2) / scale, 0); + GuiUtils.drawString(graphics, font, 00, 00, TextUtils.text(TimeUtils.parseTime(stop.getScheduledArrivalTime() + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 00, 12, TextUtils.text(TimeUtils.parseTime(stop.getScheduledDepartureTime() + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); + + if (stop.shouldRenderRealTime() && !part.isCancelled() && valid) { + GuiUtils.drawString(graphics, font, 30, 00, TimeUtils.parseTime(stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 30, 12, TimeUtils.parseTime(stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + } + } else { + graphics.poseStack().translate((x() + 28) / scale, (y + 6) / scale, 0); + GuiUtils.drawString(graphics, font, 00, 00, TextUtils.text(TimeUtils.parseTime((type == TrainStopType.START ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime()) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); + if (stop.shouldRenderRealTime() && !part.isCancelled() && valid) { + long realTime = type == TrainStopType.START ? stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) : stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision); + GuiUtils.drawString(graphics, font, 30, 00, TimeUtils.parseTime(realTime + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), (type == TrainStopType.START ? stop.isDepartureDelayed() : stop.isArrivalDelayed()) ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + } + } + + graphics.poseStack().popPose(); + //GuiUtils.drawString(graphics, font, x(), y(), stop.getState().name() + ", Running: " + !stop.isClosed(), 0xFFFF0000, EAlignment.LEFT, false); + //GuiUtils.drawString(graphics, font, x(), y() + height() - font.lineHeight, "" + stop.getScheduleIndex() + ": " + stop.getScheduledCycle() + " / " + stop.getSimulationTime() + " / " + stop.getScheduledArrivalTime() + " / " + stop.debug_test + (stop.isSimulated() ? "s" : ""), 0xFFFF0000, EAlignment.LEFT, false); + //GuiUtils.drawString(graphics, font, x(), y() + height() - font.lineHeight, String.valueOf(stop.getTag().getId()), 0xFFFF0000, EAlignment.LEFT, false); + } + + public static enum TrainStopType { + START(48, 24, 4), + TRANSIT(72, 21, 1), + END(122, 33, 11); + + private int v; + private int h; + private int dy; + + TrainStopType(int v, int h, int dy) { + this.v = v; + this.h = h; + this.dy = dy; + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java new file mode 100644 index 00000000..40b6c797 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java @@ -0,0 +1,192 @@ +package de.mrjulsen.crn.client.gui.widgets.routedetails; + +import java.io.Closeable; +import java.util.Set; + +import com.simibubi.create.content.trains.entity.TrainIconType; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.screen.TrainJourneySreen; +import de.mrjulsen.crn.client.gui.widgets.routedetails.RoutePartWidget.RoutePartDetailsActionBuilder; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.events.RouteDetailsActionsEvent; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.GuiIcons; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.util.DLWidgetsCollection; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Single; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +public class RoutePartTrainDetailsWidget extends WidgetContainer implements Closeable { + + protected static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/widgets.png"); + protected static final int GUI_TEXTURE_WIDTH = 256; + protected static final int GUI_TEXTURE_HEIGHT = 256; + protected static final int ENTRY_WIDTH = 225; + protected static final int DEFAULT_HEIGHT = 28; + protected static final int REASON_WIDTH = RoutePartWidget.ACTION_BTN_WIDTH - 8; + protected static final int V = 92; + + private final ClientTrainStop stop; + private final ClientRoutePart part; + private Set status = Set.of(); + + public static final int ACTION_BTN_WIDTH = 140; + public static final int ACTION_BTN_HEIGHT = 14; + private int actionIndex; + private int currentHeight; + private final DLWidgetsCollection actionButtons = new DLWidgetsCollection(); + private final RoutePartWidget container; + + public RoutePartTrainDetailsWidget(Screen parent, RoutePartWidget container, ClientRoute route, ClientRoutePart part, ClientTrainStop firstStop, int pX, int pY, int width) { + super(pX, pY, width, DEFAULT_HEIGHT); + this.stop = firstStop; + this.part = part; + this.container = container; + + part.listen(ClientRoutePart.EVENT_UPDATE, this, (data) -> { + int oldHeight = currentHeight; + currentHeight = DEFAULT_HEIGHT; + updateStatus(); + currentHeight += actionIndex * (ACTION_BTN_HEIGHT + 1); + int diff = currentHeight - oldHeight; + actionButtons.performForEach(x -> x.set_y(x.y() + diff)); + set_height(currentHeight); + }); + + currentHeight = DEFAULT_HEIGHT; + updateStatus(); + + addAction(new RoutePartDetailsActionBuilder(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.title"), Sprite.empty(), (b) -> Minecraft.getInstance().setScreen(new TrainJourneySreen(parent, route, part.getTrainId())))); + CRNEventsManager.getEventOptional(RouteDetailsActionsEvent.class).ifPresent(x -> x.run(route, part, container.isExpanded()).forEach(this::addAction)); + if (!part.getStopovers().isEmpty()) { + addAction(new RoutePartDetailsActionBuilder(container.isExpanded() ? Constants.TOOLTIP_COLLAPSE : Constants.TOOLTIP_EXPAND, (container.isExpanded() ? GuiIcons.ARROW_UP : GuiIcons.ARROW_DOWN).getAsSprite(16, 16), (b) -> container.setExpanded(!container.isExpanded()))); + } + + currentHeight += actionIndex * (ACTION_BTN_HEIGHT + 1); + set_height(currentHeight); + } + + private void updateStatus() { + this.status = part.getStatus(); + currentHeight += status.stream().mapToInt(x -> Math.max(9, (int)(ClientWrapper.getTextBlockHeight(font, x.text(), (int)(REASON_WIDTH / 0.75f)) * 0.75f) + 2)).sum() /* TODO */ + 4; + } + + public void addAction(RoutePartDetailsActionBuilder builder) { + DLIconButton btn2 = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, builder.icon(), x() + 76, y() + currentHeight + (actionIndex * (ACTION_BTN_HEIGHT + 1)), ACTION_BTN_WIDTH, ACTION_BTN_HEIGHT, builder.text(), builder.onClick()) { + @Override + public void mouseMoved(double mouseX, double mouseY) { + setFontColor(isInBounds(mouseX, mouseY) ? DragonLib.NATIVE_BUTTON_FONT_COLOR_HIGHLIGHT : DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE); + super.mouseMoved(mouseX, mouseY); + } + }); + btn2.setFontColor(DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE); + btn2.setBackColor(0x00000000); + actionButtons.add(btn2); + actionIndex++; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + GuiUtils.drawTexture(GUI, graphics, x(), y(), ENTRY_WIDTH, DEFAULT_HEIGHT, 0, V, ENTRY_WIDTH, DEFAULT_HEIGHT, GUI_TEXTURE_WIDTH, GUI_TEXTURE_HEIGHT); + renderData(graphics, y() + 1); + super.renderMainLayer(graphics, mouseX, mouseY, partialTick); + } + + protected void renderData(Graphics graphics, int y) { + final float scale = 0.75f; + final float mul = 1 / scale; + final float maxWidth = 140; + + //GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y, 0, V, ENTRY_WIDTH, DEFAULT_HEIGHT); + GuiUtils.drawTexture(Constants.GUI_WIDGETS, graphics, x, y + DEFAULT_HEIGHT - 1, ENTRY_WIDTH, height() - DEFAULT_HEIGHT, 0, V + DEFAULT_HEIGHT, ENTRY_WIDTH, 1, GUI_TEXTURE_WIDTH, GUI_TEXTURE_HEIGHT); + stop.getTrainIcon().render(TrainIconType.ENGINE, graphics.poseStack(), x + 80, y + 7); + + graphics.poseStack().pushPose(); + graphics.poseStack().scale(scale, scale, scale); + Component trainName = TextUtils.text(part.getLastStop().getTrainDisplayName()).withStyle(ChatFormatting.BOLD); + CreateDynamicWidgets.renderTextHighlighted(graphics, (int)((x() + 80 + 24) / scale), (int)((y + 4) / scale), font, trainName, part.getLastStop().getTrainDisplayColor()); + GuiUtils.drawString(graphics, font, (int)((x() + 80 + 24) / scale) + font.width(trainName) + 10, (int)((y + 6) / scale), GuiUtils.ellipsisString(font, TextUtils.text(String.format("%s (%s)", stop.getTrainName(), stop.getTrainId().toString().split("-")[0])), (int)((maxWidth - font.width(trainName) - 15) / scale)), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, (int)((x() + 80 + 24) / scale), (int)((y + 18) / scale), GuiUtils.ellipsisString(font, TextUtils.text(stop.getDisplayTitle()), (int)((maxWidth - 24) / scale)), 0xFFDBDBDB, EAlignment.LEFT, false); + graphics.poseStack().scale(mul, mul, mul); + graphics.poseStack().popPose(); + + // render reasons + int reasonsY = 0; + for (CompiledTrainStatus trainInformation : status) { + reasonsY += trainInformation.render(graphics, Single.of(font), x() + 76, y() + DEFAULT_HEIGHT + 2 + reasonsY, REASON_WIDTH); + } + } + + @Override + public void set_height(int h) { + super.set_height(h); + container.updateHeight(); + } + + public static enum TrainStopType { + START(48, 30, 8), + TRANSIT(78, 21, 1), + END(142, 44, 14); + + private int v; + private int h; + private int dy; + + TrainStopType(int v, int h, int dy) { + this.v = v; + this.h = h; + this.dy = dy; + } + + public int getV() { + return v; + } + + public int getH() { + return h; + } + + public int getDy() { + return dy; + } + } + + @Override + public void close() { + part.stopListeningAll(this); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java new file mode 100644 index 00000000..5daab598 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java @@ -0,0 +1,163 @@ +package de.mrjulsen.crn.client.gui.widgets.routedetails; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +import de.mrjulsen.crn.client.gui.widgets.routedetails.RoutePartEntryWidget.TrainStopType; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.util.DLWidgetsCollection; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class RoutePartWidget extends WidgetContainer { + + private final ClientRoutePart part; + private final ClientRoute route; + private int stackLayoutY; + + private boolean expanded; + private boolean canExpandCollapse = true; + private boolean showTrainDetails = true; + private boolean showJourney = false; + private Consumer onGuiChanged; + private final DLWidgetsCollection stationWidgets = new DLWidgetsCollection(); + + private final Screen parent; + + public static final int ACTION_BTN_WIDTH = 140; + public static final int ACTION_BTN_HEIGHT = 14; + + public RoutePartWidget(Screen parent, int x, int y, int width, ClientRoute route, ClientRoutePart part) { + super(x, y, width, 1); + this.part = part; + this.route = route; + this.parent = parent; + initGui(); + } + + public void initGui() { + children().stream().filter(x -> x instanceof Closeable).forEach(x -> { + try { + ((Closeable)x).close(); + } catch (IOException e) {} + }); + clearWidgets(); + stackLayoutY = 0; + stationWidgets.clear(); + boolean valid = route.isPartReachable(part); + + List stops = showJourney ? part.getAllJourneyClientStops() : part.getAllClientStops(); + + addToStackLayout(new RoutePartEntryWidget(parent, part, stops.get(0), x(), y() + stackLayoutY, width(), TrainStopType.START, valid)); + if (showTrainDetails()) { + RoutePartTrainDetailsWidget details = new RoutePartTrainDetailsWidget(parent, this, route, part, stops.get(0), x(), y() + stackLayoutY, width()); + addToStackLayout(details); + } + + if (this.expanded) { + for (int i = 1; i < stops.size() - 1; i++) { + ClientTrainStop stop = stops.get(i); + addToStackLayout(new RoutePartEntryWidget(parent, part, stop, x(), y() + stackLayoutY, width(), TrainStopType.TRANSIT, valid)); + } + } + addToStackLayout(new RoutePartEntryWidget(parent, part, stops.get(stops.size() - 1), x(), y() + stackLayoutY, width(), TrainStopType.END, valid)); + + set_height(stackLayoutY); + + DLUtils.doIfNotNull(onGuiChanged, x -> x.accept(this)); + } + + public RoutePartWidget withOnGuiChangedEvent(Consumer onGuiChanged) { + this.onGuiChanged = onGuiChanged; + return this; + } + + public void updateHeight() { + stackLayoutY = 0; + for (IDragonLibWidget c : stationWidgets.components) { + c.set_y(y() + stackLayoutY); + stackLayoutY += c.height(); + } + } + + + private void addToStackLayout(T widget) { + addRenderableWidget(widget); + stationWidgets.add(widget); + stackLayoutY += widget.height(); + } + + public boolean isExpanded() { + return expanded; + } + + public void setExpanded(boolean b) { + this.expanded = b; + initGui(); + } + + public boolean canExpandCollapse() { + return canExpandCollapse; + } + + public void setCanExpandCollapse(boolean canExpandCollapse) { + this.canExpandCollapse = canExpandCollapse; + } + + public boolean showTrainDetails() { + return showTrainDetails; + } + + public void setShowTrainDetails(boolean showTrainDetails) { + this.showTrainDetails = showTrainDetails; + } + + public boolean isShowingJourney() { + return showJourney; + } + + public void setShowJourney(boolean showJourney) { + this.showJourney = showJourney; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + //GuiUtils.drawString(graphics, font, x() + 22, y(), "State: " + part.getProgressState() + ", " + part.getNextStop().getTag().getTagName().get(), 0xFFFF0000, EAlignment.LEFT, false); + } + + @Override + public void tick() { + super.tick(); + boolean valid = route.isPartReachable(part); + stationWidgets.performForEach(x -> x instanceof RoutePartEntryWidget, x -> ((RoutePartEntryWidget)x).setValid(valid)); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + + public static record RoutePartDetailsActionBuilder(Component text, Sprite icon, Consumer onClick) {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java b/common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java index a0c4970d..3bc57dab 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java +++ b/common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java @@ -17,7 +17,11 @@ public enum ELanguage implements StringRepresentable { SAXON("saxon", "sxu"), BAVARIAN("bavarian", "bar"), SPANISH("spanish", "es_es"), - RUSSIAN("russian", "ru_ru"); + RUSSIAN("russian", "ru_ru"), + FRENCH("french", "fr_fr"), + KOREAN("korean", "ko_kr"), + SWEDISH("swedish", "sv_se"), + PORTUGUESE("portuguese", "pt_pt"); private String name; private String code; diff --git a/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java b/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java new file mode 100644 index 00000000..dffbc443 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java @@ -0,0 +1,105 @@ +package de.mrjulsen.crn.cmd; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.debug.DebugOverlay; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.Util; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.Commands.CommandSelection; + +public class DebugCommand { + + private static final String CMD_NAME = CreateRailwaysNavigator.MOD_ID; + + private static final String SUB_DEBUG = "debug"; + private static final String SUB_DISCORD = "discord"; + private static final String SUB_GITHUB = "github"; + + private static final String SUB_RESET = "resetTrainPredictions"; + private static final String SUB_HARD_RESET = "hardResetTrainPredictions"; + private static final String SUB_TRAIN_DEBUG_OVERLAY = "trainDebugOverlay"; + private static final String SUB_TRAIN_OVERVIEW = "trainOverview"; + private static final String SUB_TEST = "test"; + + @SuppressWarnings("all") + public static void register(CommandDispatcher dispatcher, CommandSelection selection) { + + LiteralArgumentBuilder builder = Commands.literal(CMD_NAME) + .then(Commands.literal(SUB_DEBUG) + .requires(x -> x.hasPermission(3)) + .then(Commands.literal(SUB_HARD_RESET) + .executes(x -> hardReset(x.getSource())) + ) + .then(Commands.literal(SUB_RESET) + .executes(x -> reset(x.getSource())) + ) + .then(Commands.literal(SUB_TRAIN_DEBUG_OVERLAY) + .executes(x -> showTrainObservationOverlay(x.getSource())) + ) + .then(Commands.literal(SUB_TRAIN_OVERVIEW) + .executes(x -> showTrainDebugScreen(x.getSource())) + ) + .then(Commands.literal(SUB_TEST) + .executes(x -> printAllSignals(x.getSource())) + ) + ) + .then(Commands.literal(SUB_DISCORD) + .executes(x -> discord(x.getSource())) + ) + .then(Commands.literal(SUB_GITHUB) + .executes(x -> github(x.getSource())) + ) + ; + + dispatcher.register(builder); + } + + private static int discord(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.text("Redirecting to the discord server..."), false); + Util.getPlatform().openUri(CreateRailwaysNavigator.DISCORD); + return 1; + } + + private static int github(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.text("Redirecting to the github repository..."), false); + Util.getPlatform().openUri(CreateRailwaysNavigator.GITHUB); + return 1; + } + + private static int hardReset(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.text("All train predictions have been deleted."), false); + TrainListener.data.clear(); + TrainListener.data.values().forEach(x -> x.hardResetPredictions()); + return 1; + } + + private static int reset(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.text("All train predictions have been reset."), false); + TrainListener.data.values().forEach(x -> x.resetPredictions()); + return 1; + } + + private static int showTrainObservationOverlay(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.text("Visibility of the train debug overlay has been toggled."), false); + DebugOverlay.toggle(); + return 1; + } + + private static int showTrainDebugScreen(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.empty(), false); + DataAccessor.getFromClient(cmd.getPlayerOrException(), null, ModAccessorTypes.SHOW_TRAIN_DEBUG_SCREEN, $ -> {}); + return 1; + } + + private static int printAllSignals(CommandSourceStack cmd) throws CommandSyntaxException { + cmd.sendSuccess(TextUtils.empty(), false); + return 1; + } +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java b/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java index 0afb01e2..d347c20e 100644 --- a/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java +++ b/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java @@ -1,8 +1,5 @@ package de.mrjulsen.crn.config; -import java.util.ArrayList; -import java.util.List; - import de.mrjulsen.crn.client.gui.overlay.OverlayPosition; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.util.ESpeedUnit; @@ -15,21 +12,15 @@ public class ModClientConfig { public static final ForgeConfigSpec.ConfigValue REALTIME_PRECISION_THRESHOLD; - public static final ForgeConfigSpec.ConfigValue DEVIATION_THRESHOLD; public static final ForgeConfigSpec.ConfigValue NEXT_STOP_ANNOUNCEMENT; public static final ForgeConfigSpec.ConfigValue DISPLAY_LEAD_TIME; - public static final ForgeConfigSpec.ConfigValue REALTIME_EARLY_ARRIVAL_THRESHOLD; public static final ForgeConfigSpec.ConfigValue OVERLAY_SCALE; - public static final ForgeConfigSpec.ConfigValue TRANSFER_TIME; - public static final ForgeConfigSpec.ConfigValue> TRAIN_GROUP_FILTER_BLACKLIST; - public static final ForgeConfigSpec.ConfigValue ROUTE_NARRATOR; public static final ForgeConfigSpec.ConfigValue ROUTE_NOTIFICATIONS; public static final ForgeConfigSpec.ConfigValue ROUTE_OVERLAY_POSITION; public static final ForgeConfigSpec.ConfigValue TIME_FORMAT; public static final ForgeConfigSpec.ConfigValue LANGUAGE; public static final ForgeConfigSpec.ConfigValue SPEED_UNIT; - public static final int MAX_TRANSFER_TIME = 24000; public static final double MIN_SCALE = 0.25f; public static final double MAX_SCALE = 2.0f; @@ -37,28 +28,18 @@ public class ModClientConfig { BUILDER.push("Create Railways Navigator Config"); /* CONFIGS */ - DEVIATION_THRESHOLD = BUILDER.comment("The value indicates how much deviation from the schedule a train should be considered delayed. Delayed trains are marked red in the real-time display. (Default: 500, 30 in-game minutes)") - .defineInRange("general.deviation_threshold", 500, 1, MAX_TRANSFER_TIME); - REALTIME_EARLY_ARRIVAL_THRESHOLD = BUILDER.comment("If a train departs earlier from the scheduled time than specified here, no real-time data will be displayed. Trains that depart earlier than the scheduled departure time minus the minimum transfer time are intentionally \"missed\" in order to continue to ensure the connection. (Default: 500, 30 in-game minutes)") - .defineInRange("general.realtime_early_arrival_threshold", 500, 1, MAX_TRANSFER_TIME); - NEXT_STOP_ANNOUNCEMENT = BUILDER.comment("The next stop or information about the start of the journey is announced in the specified number of ticks before the scheduled arrival at the next station. (Default: 500, 30 real life seconds)") + NEXT_STOP_ANNOUNCEMENT = BUILDER.comment(new String[] {"[in Ticks]", "The next stop or information about the start of the journey is announced in the specified number of ticks before the scheduled arrival at the next station. (Default: 500, 30 real life seconds)"}) .defineInRange("general.next_stop_announcement", 500, 100, 1000); - REALTIME_PRECISION_THRESHOLD = BUILDER.comment("This value (in ticks) indicates how accurately the real-time data should be displayed. By default, only deviations over 10 in-game minutes (167 ticks, approx. 8 real life seconds) are displayed. The lower the value, the more accurate the real-time data but also the more often deviations from the schedule occur. (Default: 167, 10 in-game minutes)") - .defineInRange("general.realtime_precision_threshold", 167, 1, 1000); - DISPLAY_LEAD_TIME = BUILDER.comment("This value indicates when departing trains should be displayed on advanced displays. By default 1000 ticks (1 in-game hour, 50 real life seconds) before departure is imminent.") - .defineInRange("general.display_lead_time", 1000, 200, MAX_TRANSFER_TIME); + REALTIME_PRECISION_THRESHOLD = BUILDER.comment(new String[] {"[in Ticks]", "This value indicates how accurately the real-time data should be displayed. By default, only deviations above 10 in-game minutes (167 ticks, approx. 8 real life seconds) are displayed. The lower the value, the more accurate the real-time data but also the more often deviations from the schedule occur. (Default: 167, 10 in-game minutes)"}) + .defineInRange("general.realtime_precision_threshold", 167, 1, 1000); + DISPLAY_LEAD_TIME = BUILDER.comment(new String[] {"[in Ticks]", "How early a train should be shown on the display. (Default: 1000, 1 in-game hour)"}) + .defineInRange("general.display_lead_time", 1000, 100, 24000); OVERLAY_SCALE = BUILDER.comment("Scale of the route overlay UI. (Default: 0.75)") .defineInRange("route_overlay.scale", 0.75f, MIN_SCALE, MAX_SCALE); - ROUTE_NARRATOR = BUILDER.comment("If active, events during the journey (e.g. the next stop) are announced. (Default: OFF)") - .define("route_overlay.narrator", false); ROUTE_NOTIFICATIONS = BUILDER.comment("If active, you will receive short toasts about important events on your trip, e.g. delays, changes, ... (Default: ON)") .define("route_overlay.notifications", true); ROUTE_OVERLAY_POSITION = BUILDER.comment("The position on your screen where you want the overlay to appear. (Default: Top Left)") .defineEnum("route_overlay.position", OverlayPosition.TOP_LEFT); - TRANSFER_TIME = BUILDER.comment("Specifies the minimum amount of time (in ticks) that must be available for changing the train. Only trains that depart later than the specified value at the transfer station will be selected. (Default: 1000, 1 in-game hour, approx. 50 real life seconds)") - .defineInRange("search_settings.transfer_time", 1000, 0, MAX_TRANSFER_TIME); - TRAIN_GROUP_FILTER_BLACKLIST = BUILDER.comment("List of train groups that should NOT be used in navigation. (Default: )") - .defineList("search_settings.train_group_blacklist", new ArrayList(), x -> x instanceof String); LANGUAGE = BUILDER.comment("The language that should be used for announcements of the navigator. Can be different from the game's language settings. (Default: Default)") .defineEnum("language", ELanguage.DEFAULT); @@ -72,7 +53,5 @@ public class ModClientConfig { } public static void resetSearchSettings() { - TRANSFER_TIME.set(1000); - TRAIN_GROUP_FILTER_BLACKLIST.set(List.of()); } } diff --git a/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java b/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java index 62192b0c..150ae845 100644 --- a/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java +++ b/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java @@ -8,17 +8,39 @@ public class ModCommonConfig { public static final ForgeConfigSpec SPEC; public static final ForgeConfigSpec.ConfigValue GLOBAL_SETTINGS_PERMISSION_LEVEL; - public static final ForgeConfigSpec.ConfigValue TRAIN_WATCHER_INTERVALL; + public static final ForgeConfigSpec.ConfigValue TOTAL_DURATION_BUFFER_SIZE; + public static final ForgeConfigSpec.ConfigValue SCHEDULE_DEVIATION_THRESHOLD; + public static final ForgeConfigSpec.ConfigValue AUTO_RESET_TIMINGS; + public static final ForgeConfigSpec.ConfigValue TRANSFER_COST; + public static final ForgeConfigSpec.ConfigValue TOTAL_DURATION_DEVIATION_THRESHOLD; + public static final ForgeConfigSpec.ConfigValue CUSTOM_TRANSIT_TIME_CALCULATION; + public static final ForgeConfigSpec.ConfigValue EXCLUDE_TRAINS; static { BUILDER.push(CreateRailwaysNavigator.MOD_ID + "_common_config"); - - TRAIN_WATCHER_INTERVALL = BUILDER.comment("The time interval (in ticks) in which the Train Listener collects tick data of all trains. Higher values may result in less precise time information in the route details view, while lower values may result in higher CPU usage. Default: 100 (5s)") - .defineInRange("train_watcher_intervall", 100, 100, 1000); - GLOBAL_SETTINGS_PERMISSION_LEVEL = BUILDER.comment("Minimum permission level required to edit the global navigator settings. 0 allows everyone to edit these settings.") - .defineInRange("global_settings_permission_level", 0, 0, 4); + GLOBAL_SETTINGS_PERMISSION_LEVEL = BUILDER.comment("Minimum permission level required to edit the global navigator settings. 0 allows everyone to edit these settings. (Default: 0)") + .defineInRange("permissions.global_settings_permission_level", 0, 0, 4); + + + EXCLUDE_TRAINS = BUILDER.comment("If activated, used trains are excluded from the route search for all following route parts. This prevents the same train from being suggested multiple times in the same route and forces the navigator to use other trains instead. Normally, however, there are no problems, so this option can be left off if in doubt. (Default: OFF)") + .define("navigation.exclude_trains", false); + TRANSFER_COST = BUILDER.comment("How much transfers should be avoided. Higher values try to use fewer transfers, even if this increases the travel time. (Default: 5000)") + .defineInRange("navigation.transfer_cost", 10000, 1000, Integer.MAX_VALUE); + + + CUSTOM_TRANSIT_TIME_CALCULATION = BUILDER.comment("When activated, CRN calculates the transit times of the trains and does not use the calculations from Create. CRN is much more accurate, while Create calculates an average. (Default: ON)") + .define("train_data_calculation.custom_transit_time_calculation", true); + TOTAL_DURATION_BUFFER_SIZE = BUILDER.comment(new String[] {"[in Cycles]", "How often the calculated time for a route section between two stations must deviate from the current reference value before the reference value is updated. (Default: 3)"}) + .defineInRange("train_data_calculation.total_duration_deviation_buffer_size", 3, 1, 16); + TOTAL_DURATION_DEVIATION_THRESHOLD = BUILDER.comment(new String[] {"[in Ticks]", "Deviations of the calculated time for a route section between two stations from the reference value that are smaller than the threshold value are not taken into account. (Default: 50)"}) + .defineInRange("train_data_calculation.total_duration_deviation_threshold", 50, 0, 1000); + SCHEDULE_DEVIATION_THRESHOLD = BUILDER.comment(new String[] {"[in Ticks]", "How many ticks the real-time can deviate from the scheduled time before the train is considered delayed. (Default: 500)"}) + .defineInRange("train_data_calculation.schedule_deviation_threshold", 500, 100, 24000); + AUTO_RESET_TIMINGS = BUILDER.comment(new String[] {"[In Cycles]", "(ONLY WORKS FOR TRAINS WITH DYNAMIC DELAYS! Trains without dynamic delays do this every new schedule section by default.)", " ", "Every X cycles the scheduled times are updated to the current real-time data. (Default: 2; Disabled: 0)"}) + .defineInRange("train_data_calculation.auto_reset_timings", 2, 0, Integer.MAX_VALUE); + BUILDER.pop(); SPEC = BUILDER.build(); } diff --git a/common/src/main/java/de/mrjulsen/crn/core/navigation/Edge.java b/common/src/main/java/de/mrjulsen/crn/core/navigation/Edge.java deleted file mode 100644 index eecbb174..00000000 --- a/common/src/main/java/de/mrjulsen/crn/core/navigation/Edge.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.mrjulsen.crn.core.navigation; - -import java.util.Objects; -import java.util.UUID; - -public class Edge { - - private final UUID id; - private UUID node1Id; - private UUID node2Id; - - private int cost = -1; - private UUID scheduleId; - - public Edge(Node node1, Node node2, UUID id, UUID scheduleId) { - this.id = id; - this.node1Id = node1.getId(); - this.node2Id = node2.getId(); - this.scheduleId = scheduleId; - } - - public Edge withCost(int cost, boolean overwrite) { - this.cost = cost < 0 || !overwrite ? cost : (cost + cost) / 2; - return this; - } - - public UUID getId() { - return id; - } - - public UUID getFirstNodeId() { - return node1Id; - } - - public UUID getSecondNodeId() { - return node2Id; - } - - public UUID getScheduleId() { - return scheduleId; - } - - public int getCost() { - return cost; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Edge other) { - return getFirstNodeId().equals(other.getFirstNodeId()) && getSecondNodeId().equals(other.getSecondNodeId()) && getCost() == other.getCost(); - } - - return false; - } - - public boolean similar(Edge other) { - return getFirstNodeId().equals(other.getFirstNodeId()) && getSecondNodeId().equals(other.getSecondNodeId()); - } - - @Override - public int hashCode() { - return 43 * Objects.hash(getFirstNodeId(), getSecondNodeId()); - } - - @Override - public String toString() { - return String.format("%s [%s -> %s, Cost: %s]", getId(), getFirstNodeId(), getSecondNodeId(), getCost()); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/core/navigation/Graph.java b/common/src/main/java/de/mrjulsen/crn/core/navigation/Graph.java deleted file mode 100644 index 1dabf768..00000000 --- a/common/src/main/java/de/mrjulsen/crn/core/navigation/Graph.java +++ /dev/null @@ -1,423 +0,0 @@ -package de.mrjulsen.crn.core.navigation; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.PriorityQueue; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.GlobalSettings; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.GlobalTrainData; -import de.mrjulsen.crn.data.Route; -import de.mrjulsen.crn.data.RoutePart; -import de.mrjulsen.crn.data.SimpleTrainSchedule; -import de.mrjulsen.crn.data.SimulatedTrainSchedule; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.crn.data.UserSettings; -import de.mrjulsen.crn.event.listeners.TrainListener; -import de.mrjulsen.crn.util.TrainUtils; -import net.minecraft.world.level.Level; - -public class Graph { - - protected static final int MIN_START_TIME = 200; - - private Map nodesById; - private Map nodesByStation; - - private Map edgesById; - private Map>> edgesByNode; - - private Map schedulesById; - private Map schedulesByTrainId; - private Map> trainIdsBySchedule; - private Map scheduleIdByTrainId; - - private final GlobalSettings globalSettings; - private final UserSettings settings; - - private final long lastUpdated; - private final Level level; - - public Graph(Level level, UserSettings settings) { - long startTime = System.currentTimeMillis(); - lastUpdated = level.getDayTime(); - this.settings = settings; - this.level = level; - GlobalTrainData.makeSnapshot(lastUpdated); - - this.nodesById = new HashMap<>(); - this.nodesByStation = new HashMap<>(); - this.edgesById = new HashMap<>(); - this.edgesByNode = new HashMap<>(); - this.schedulesById = new HashMap<>(); - this.schedulesByTrainId = new HashMap<>(); - this.trainIdsBySchedule = new HashMap<>(); - this.scheduleIdByTrainId = new HashMap<>(); - - final int[] trainCounter = new int[] { 0 }; - globalSettings = GlobalSettingsManager.getInstance().getSettingsData(); - - TrainUtils.getAllTrains().stream().filter(x -> TrainUtils.isTrainValid(x) && !globalSettings.isTrainBlacklisted(x) && !settings.isTrainExcluded(x, globalSettings)).forEach(x -> { - addTrain(x, globalSettings); - trainCounter[0]++; - }); - - long estimatedTime = System.currentTimeMillis() - startTime; - CreateRailwaysNavigator.LOGGER.info(String.format("Graph generated. Took %sms. Contains %s nodes, %s edges and %s schedules. %s train processed.", - estimatedTime, - nodesById.size(), - edgesById.size(), - schedulesById.size(), - trainCounter.length - )); - } - - public UserSettings getSettings() { - return settings; - } - - protected Node addNode(TrainStationAlias alias, Train train) { - if (nodesByStation.containsKey(alias)) { - Node node = nodesByStation.get(alias); - node.addTrain(train.id); - return node; - } - - UUID id = UUID.randomUUID(); - while (nodesById.containsKey(id)) { - id = UUID.randomUUID(); - } - - Node node = new Node(alias, id); - node.addTrain(train.id); - nodesById.put(id, node); - nodesByStation.put(alias, node); - return node; - } - - protected Edge addEdge(Node node1, Node node2, UUID scheduleId) { - UUID id = UUID.randomUUID(); - while (edgesById.containsKey(id)) { - id = UUID.randomUUID(); - } - - Edge edge = new Edge(node1, node2, id, scheduleId); - if (putConnection(node1, node2, edge)) { - edgesById.put(id, edge); - } - return edge; - } - - protected TrainSchedule addTrain(Train train, GlobalSettings settingsInstance) { - - if (schedulesByTrainId.containsKey(train.id)) { - return schedulesByTrainId.get(train.id); - } - - UUID id = UUID.randomUUID(); - while (edgesById.containsKey(id)) { - id = UUID.randomUUID(); - } - - TrainSchedule schedule = new TrainSchedule(train, id, settingsInstance); - if (!schedule.addToGraph(this, train)) { - return null; - } - - if (trainIdsBySchedule.containsKey(schedule)) { - TrainSchedule sched = schedulesByTrainId.get(trainIdsBySchedule.get(schedule).stream().findFirst().get()); - trainIdsBySchedule.get(schedule).add(train.id); - schedulesByTrainId.put(train.id, sched); - scheduleIdByTrainId.put(train.id, sched.getId()); - - sched.getEdges().forEach(x -> { - Optional matchingEdge = schedule.getEdges().stream().filter(a -> a.equals(x)).findFirst(); - - if (matchingEdge.isPresent()) { - x.withCost(matchingEdge.get().getCost(), false); - } - }); - - return sched; - } - - schedulesById.put(id, schedule); - schedulesByTrainId.put(train.id, schedule); - trainIdsBySchedule.put(schedule, new HashSet<>(Set.of(train.id))); - scheduleIdByTrainId.put(train.id, schedule.getId()); - - return schedule; - } - - public boolean putConnection(Node node1, Node node2, Edge edge) { - Map> connections = edgesByNode.computeIfAbsent(node1, n -> new IdentityHashMap<>()); - if (connections.containsKey(node2)) { - if (connections.get(node2).contains(edge)) { - return false; - } - return connections.get(node2).add(edge); - } - - return connections.put(node2, new HashSet<>(Set.of(edge))) == null; - } - - public Set getNodes() { - return new HashSet<>(nodesById.values()); - } - - public Node getNode(TrainStationAlias alias) { - return nodesByStation.get(alias); - } - - public Node getNode(UUID id) { - return nodesById.get(id); - } - - public Map> getEdges(Node node) { - return edgesByNode.get(node); - } - - public Set getEdges() { - return new HashSet<>(edgesById.values()); - } - - public Edge getEdge(UUID id) { - return edgesById.get(id); - } - - public Map> getConnectionsFrom(Node node) { - if (node == null) { - return null; - } - return edgesByNode.getOrDefault(node, new HashMap<>()); - } - - public Collection navigate(TrainStationAlias start, TrainStationAlias end, boolean avoidTransfers) { - return searchTrains(searchRoute(start, end, avoidTransfers)).stream().filter(x -> !x.isEmpty()).sorted(Comparator.comparingInt(x -> x.getStartStation().getPrediction().getTicks())).toList(); - } - - public List searchRoute(TrainStationAlias start, TrainStationAlias end, boolean avoidTransfers) { - - if (!nodesByStation.containsKey(start) || !nodesByStation.containsKey(end)) { - return List.of(); - } - - Map nodes = dijkstra(start, avoidTransfers); - Node endNode = nodes.get(end); - - if (endNode == null) { - return List.of(); - } - - endNode.setIsTransferPoint(true); - List route = new ArrayList<>(); - - Node currentNode = endNode; - while (!currentNode.getStationAlias().equals(start)) { - route.add(0, currentNode); - - if (currentNode.getPreviousEdge() != null) { - if (currentNode.getPreviousNode().getPreviousEdge() == null) { - currentNode.getPreviousNode().setIsTransferPoint(false); - } else { - currentNode.getPreviousNode().setIsTransferPoint(!currentNode.getPreviousEdge().getScheduleId().equals(currentNode.getPreviousNode().getPreviousEdge().getScheduleId())); - } - } - currentNode = currentNode.getPreviousNode(); - } - currentNode.getPreviousNode().setIsTransferPoint(true); - route.add(0, currentNode.getPreviousNode()); - - return route; - } - - private Map generateTrainSchedules() { - return GlobalTrainData.getInstance().getAllTrains().stream().filter(x -> - TrainUtils.isTrainValid(x) && - !globalSettings.isTrainBlacklisted(x) && - !settings.isTrainExcluded(x, globalSettings) - ).collect(Collectors.toMap(x -> x.id, x -> new SimpleTrainSchedule(x))); - } - - public Collection searchTrains(List routeNodes) { - Map schedulesByTrain = generateTrainSchedules(); - Collection routes = new ArrayList<>(); - routes.add(new Route(lastUpdated)); - - int timer = MIN_START_TIME; - final Node[] filteredTransferNodes = routeNodes.stream().filter(x -> x.isTransferPoint()).toArray(Node[]::new); - - if (filteredTransferNodes.length < 2) { - return routes; - } - - - final Node lastTransfer = filteredTransferNodes[0]; - final Map lastTransferByTrain = new HashMap<>(); - final Node lastNode = lastTransfer; - final int simulationTime = timer; - - Collection trainPredictions = GlobalTrainData.getInstance().getDepartingTrainsAt(lastNode.getStationAlias()).stream() - .filter(x -> { - if (globalSettings.isTrainBlacklisted(x.getTrain()) || settings.isTrainExcluded(x.getTrain(), globalSettings)) { - return false; - } - - SimpleTrainSchedule schedule = schedulesByTrain.get(x.getTrain().id); - for (int i = filteredTransferNodes.length - 1; i > 0; i--) { - Node nde = filteredTransferNodes[i]; - final int j = i; - if (nde.getTrainIds().contains(x.getTrain().id)) { - lastTransferByTrain.put(x.getTrain().id, j); - break; - } - } - - boolean b = lastTransferByTrain.containsKey(x.getTrain().id) && - schedule.hasStationAlias(filteredTransferNodes[lastTransferByTrain.get(x.getTrain().id)].getStationAlias()) && - TrainUtils.isTrainValid(x.getTrain()); - - return b; - }).map(x -> { - int t = x.getTicks() + TrainListener.getInstance().getDepartmentTime(level, x.getTrain()); - return schedulesByTrain.get(x.getTrain().id).simulate(x.getTrain(), t > simulationTime ? 0 : simulationTime, lastNode.getStationAlias()); - }).sorted(Comparator.comparingInt(x -> x.getSimulationData().simulationCorrection())).toList(); - - SimulatedTrainSchedule selectedPrediction = trainPredictions.stream().filter(x -> x.isInDirection(lastNode.getStationAlias(), filteredTransferNodes[lastTransferByTrain.get(x.getSimulationData().train().id)].getStationAlias())).findFirst().orElse(trainPredictions.stream().findFirst().orElse(null)); - - if (selectedPrediction == null) { - CreateRailwaysNavigator.LOGGER.warn("Unable to find route from " + lastNode.getStationAlias().getAliasName()); - return routes; - } - - Collection filteredSchedules = trainPredictions.stream().collect(Collectors.toMap(x -> x.getSimulationData().train().id, x -> x, (o, n) -> { - if (n.isInDirection(lastNode.getStationAlias(), filteredTransferNodes[lastTransferByTrain.get(o.getSimulationData().train().id)].getStationAlias())) { - return n; - } else if (o.isInDirection(lastNode.getStationAlias(), filteredTransferNodes[lastTransferByTrain.get(o.getSimulationData().train().id)].getStationAlias())) { - return o; - } - return o.getSimulationData().simulationCorrection() < n.getSimulationData().simulationCorrection() ? o : n; - })).values(); - if (filteredSchedules.isEmpty()) { - filteredSchedules = List.of(selectedPrediction); - } - - for (SimulatedTrainSchedule sched : filteredSchedules) { - Route r = new Route(lastUpdated); - int t = sched.getFirstStopOf(lastNode.getStationAlias()).get().getPrediction().getTicks() + TrainListener.getInstance().getDepartmentTime(level, sched.getSimulationData().train()); - RoutePart part = new RoutePart(level, sched.getSimulationData().train(), lastNode.getStationAlias(), filteredTransferNodes[lastTransferByTrain.get(sched.getSimulationData().train().id)].getStationAlias(), t > simulationTime ? 0 : simulationTime); - r.addPart(part); - timer = part.getEndStation().getPrediction().getTicks() + settings.getTransferTime(); - Set excludedSchedules = new HashSet<>(); - excludedSchedules.add(schedulesByTrain.get(part.getTrain().id)); - - Collection parts = searchTrainsInternal(schedulesByTrain, new HashSet<>(excludedSchedules), filteredTransferNodes, lastTransferByTrain.get(sched.getSimulationData().train().id) + 1, timer, filteredTransferNodes[lastTransferByTrain.get(sched.getSimulationData().train().id)]); - parts.forEach(x -> r.addPart(x)); - routes.add(r); - } - - return routes; - } - - public Collection searchTrainsInternal(Map schedulesByTrain, Set excludedSchedules, Node[] filteredTransferNodes, int startIdx, int timer, Node lastTransfer) { - List routes = new ArrayList<>(); - - final int len = filteredTransferNodes.length; - for (int i = startIdx; i < len; i++) { - Node node = filteredTransferNodes[i]; - - if (lastTransfer != null) { - final Node lastNode = lastTransfer; - final int simulationTime = timer; - - Collection trainPredictions = GlobalTrainData.getInstance().getDepartingTrainsAt(lastNode.getStationAlias()).stream() - .filter(x -> { - if (globalSettings.isTrainBlacklisted(x.getTrain()) || settings.isTrainExcluded(x.getTrain(), globalSettings)) { - return false; - } - - SimpleTrainSchedule schedule = schedulesByTrain.get(x.getTrain().id); - - boolean b = !excludedSchedules.contains(schedule) && - schedule.hasStationAlias(node.getStationAlias()) && - TrainUtils.isTrainValid(x.getTrain()); - return b; - }).map(x -> { - return schedulesByTrain.get(x.getTrain().id).simulate(x.getTrain(), simulationTime, lastNode.getStationAlias()); - }).sorted(Comparator.comparingInt(x -> x.getSimulationData().simulationCorrection())).toList(); - - SimulatedTrainSchedule selectedPrediction = trainPredictions.stream().filter(x -> x.isInDirection(lastNode.getStationAlias(), node.getStationAlias())).findFirst().orElse(trainPredictions.stream().findFirst().orElse(null)); - - if (selectedPrediction == null) { - CreateRailwaysNavigator.LOGGER.warn("Route aborted! No train was found at " + lastNode.getStationAlias().getAliasName()); - return routes; - } - - RoutePart part = new RoutePart(level, selectedPrediction.getSimulationData().train(), lastNode.getStationAlias(), node.getStationAlias(), simulationTime); - routes.add(part); - timer = part.getEndStation().getPrediction().getTicks() + settings.getTransferTime(); - excludedSchedules.add(schedulesByTrain.get(part.getTrain().id)); - } - - lastTransfer = node; - } - - return routes; - } - - - protected Map dijkstra(TrainStationAlias start, boolean avoidTransfers) { - nodesById.values().forEach(x -> x.init()); - Node startNode = nodesByStation.get(start); - startNode.setCost(0); - startNode.setPrevious(startNode, null); - startNode.setIsTransferPoint(true); - - PriorityQueue queue = new PriorityQueue<>(); - Map excludedNodes = new HashMap<>(); - queue.add(startNode); - - while (!queue.isEmpty()) { - Node currentNode = queue.poll(); - Map> reachableNodes = edgesByNode.get(currentNode); - - reachableNodes.entrySet().stream().filter(x -> !excludedNodes.containsKey(x.getKey().getStationAlias())).forEach(y -> { - final Node node = y.getKey(); - y.getValue().forEach(x -> { - Edge edge = x; - boolean isTransfer = currentNode.getPreviousEdge() != null && !currentNode.getPreviousEdge().getScheduleId().equals(edge.getScheduleId()); - - TrainSchedule sched = schedulesById.get(edge.getScheduleId()); - int avgTransferTime = (int)trainIdsBySchedule.get(sched).stream().mapToInt(a -> TrainListener.getInstance().getApproximatedTrainDuration(a)).average().getAsDouble(); - - long newCost = currentNode.getCost() + edge.getCost() + (isTransfer && avoidTransfers ? avgTransferTime + 1000 : 0); - if (newCost > node.getCost()) { - return; - } - - node.setCost(newCost); - node.setPrevious(currentNode, edge); - - queue.add(node); - }); - }); - - excludedNodes.put(currentNode.getStationAlias(), currentNode); - } - - return excludedNodes; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/core/navigation/Node.java b/common/src/main/java/de/mrjulsen/crn/core/navigation/Node.java deleted file mode 100644 index cba74922..00000000 --- a/common/src/main/java/de/mrjulsen/crn/core/navigation/Node.java +++ /dev/null @@ -1,101 +0,0 @@ -package de.mrjulsen.crn.core.navigation; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; - -import de.mrjulsen.crn.data.TrainStationAlias; - -public class Node implements Comparable { - private TrainStationAlias name; - private final UUID id; - private final Set trainIds = new HashSet<>(); - - // calc - private long cost = Long.MAX_VALUE; - private Node previousNode = null; - private Edge previousEdge = null; - private boolean isTransferPoint = false; - - public Node(TrainStationAlias alias, UUID id) { - this.id = id; - this.name = alias; - } - - public UUID getId() { - return id; - } - - public Set getTrainIds() { - return trainIds; - } - - public void addTrain(UUID id) { - trainIds.add(id); - } - - public TrainStationAlias getStationAlias() { - return name; - } - - public void init() { - this.cost = Long.MAX_VALUE; - this.previousNode = null; - this.previousEdge = null; - this.isTransferPoint = false; - } - - public long getCost() { - return cost; - } - - public void setCost(long cost) { - this.cost = cost; - } - - public Node getPreviousNode() { - return previousNode; - } - - public Edge getPreviousEdge() { - return previousEdge; - } - - public boolean isTransferPoint() { - return isTransferPoint; - } - - public void setPrevious(Node node, Edge viaEdge) { - this.previousNode = node; - this.previousEdge = viaEdge; - } - - public void setIsTransferPoint(boolean b) { - this.isTransferPoint = b; - } - - - @Override - public boolean equals(Object obj) { - if (obj instanceof Node other) { - return getStationAlias().equals(other.getStationAlias()); - } - return false; - } - - @Override - public String toString() { - return String.format("%s (%s) %s", getStationAlias(), getId(), isTransferPoint() ? "(Transfer)" : ""); - } - - @Override - public int hashCode() { - return 37 + Objects.hash(getStationAlias()); - } - - @Override - public int compareTo(Node o) { - return Long.compare(getCost(), o.getCost()); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/core/navigation/TrainData.java b/common/src/main/java/de/mrjulsen/crn/core/navigation/TrainData.java deleted file mode 100644 index ec2930e6..00000000 --- a/common/src/main/java/de/mrjulsen/crn/core/navigation/TrainData.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.mrjulsen.crn.core.navigation; - -import java.util.UUID; - -import com.simibubi.create.content.trains.entity.Train; - -public class TrainData { - private UUID trainId; - private String name; - - public TrainData(Train train) { - this.trainId = train.id; - this.name = train.name.getString(); - } - - public UUID getId() { - return trainId; - } - - public String getName() { - return name; - } - - -} diff --git a/common/src/main/java/de/mrjulsen/crn/core/navigation/TrainSchedule.java b/common/src/main/java/de/mrjulsen/crn/core/navigation/TrainSchedule.java deleted file mode 100644 index b08e0bda..00000000 --- a/common/src/main/java/de/mrjulsen/crn/core/navigation/TrainSchedule.java +++ /dev/null @@ -1,105 +0,0 @@ -package de.mrjulsen.crn.core.navigation; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.crn.data.GlobalSettings; -import de.mrjulsen.crn.data.GlobalTrainData; -import de.mrjulsen.crn.data.TrainStop; -import de.mrjulsen.crn.event.listeners.TrainListener; - -public class TrainSchedule { - - private final UUID id; - private Set nodes; - private Set edges; - - private List stops; - - public TrainSchedule(Train train, UUID id, GlobalSettings settingsInstance) { - this.id = id; - nodes = ConcurrentHashMap.newKeySet(); - edges = ConcurrentHashMap.newKeySet(); - makeSchedule(train, settingsInstance); - } - - private void makeSchedule(Train train, GlobalSettings settingsInstance) { - this.stops = new ArrayList<>(GlobalTrainData.getInstance().getAllStopsSorted(train).stream().filter(x -> !settingsInstance.isBlacklisted(x.getPrediction().getStationName())).toList()); - } - - public boolean addToGraph(Graph graph, Train train) { - if (stops.isEmpty()) { - return false; - } - - final int cycleDuration = TrainListener.getInstance().getApproximatedTrainDuration(train); - - final int size = stops.size(); - TrainStop lastStop = stops.get(size - 1); - - for (int i = 0; i < size; i++) { - TrainStop stop = stops.get(i); - - int duration = i == 0 ? cycleDuration - lastStop.getPrediction().getTicks() + stop.getPrediction().getTicks() : stop.getPrediction().getTicks() - lastStop.getPrediction().getTicks(); - Node node1 = graph.addNode(lastStop.getStationAlias(), train); - Node node2 = graph.addNode(stop.getStationAlias(), train); - Edge edge = graph.addEdge(node1, node2, getId()).withCost(duration, false); - - nodes.add(node1); - nodes.add(node2); - edges.add(edge); - - lastStop = stop; - } - - return true; - } - - public UUID getId() { - return id; - } - - public Set getNodes() { - return nodes; - } - - public Set getEdges() { - return edges; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof TrainSchedule other) { - boolean b = getNodes().size() == other.getNodes().size() && getEdges().size() == other.getEdges().size(); - return b && getNodes().containsAll(other.getNodes()) && getEdges().containsAll(other.getEdges()); - } - - return false; - } - - @Override - public int hashCode() { - return 41 * Objects.hash(getEdges(), getNodes()); - } - - public void debugPrint() { - System.out.println(String.format("TRAIN SCHEDULE DETAILS (%s nodes, %s edges)", nodes.size(), edges.size())); - System.out.println("Nodes"); - for (Node node : nodes) { - System.out.println(" -> " + node); - } - - System.out.println("Edges"); - for (Edge edge : edges) { - System.out.println(" -> " + edge); - } - - System.out.println("--- END OF SCHEDULE ---"); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/ClientTrainStationSnapshot.java b/common/src/main/java/de/mrjulsen/crn/data/ClientTrainStationSnapshot.java deleted file mode 100644 index b104669e..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/ClientTrainStationSnapshot.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.ArrayList; -import java.util.Collection; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.TrackStationsRequestPacket; - -public class ClientTrainStationSnapshot { - private static ClientTrainStationSnapshot instance; - - private final Collection stationNames; - private final Collection trainNames; - - private final int listeningTrainCount; - private final int totalTrainCount; - - private ClientTrainStationSnapshot(Collection stationNames, Collection trainNames, int listeningTrainCount, int totalTrainCount) { - this.stationNames = stationNames; - this.trainNames = trainNames; - this.listeningTrainCount = listeningTrainCount; - this.totalTrainCount = totalTrainCount; - } - - public static ClientTrainStationSnapshot makeNew(Collection stationNames, Collection trainNames, int listeningTrainCount, int totalTrainCount) { - return instance = new ClientTrainStationSnapshot(stationNames, trainNames, listeningTrainCount, totalTrainCount); - } - - public static ClientTrainStationSnapshot getInstance() { - if (instance == null) { - makeNew(new ArrayList<>(), new ArrayList<>(), 0, 0); - } - return instance; - } - - public Collection getAllTrainStations() { - return stationNames; - } - - public Collection getAllTrainNames() { - return trainNames; - } - - public static void syncToClient(Runnable then) { - long id = InstanceManager.registerClientResponseReceievedAction(then); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new TrackStationsRequestPacket(id)); - } - - - public int getListeningTrainCount() { - return listeningTrainCount; - } - - public int getTrainCount() { - return totalTrainCount; - } - - public int getStationCount() { - return stationNames.size(); - } - - public void dispose() { - instance = null; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/DeparturePrediction.java b/common/src/main/java/de/mrjulsen/crn/data/DeparturePrediction.java deleted file mode 100644 index 68aac1ea..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/DeparturePrediction.java +++ /dev/null @@ -1,185 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.Arrays; -import java.util.Objects; -import java.util.UUID; - -import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; -import de.mrjulsen.crn.event.listeners.TrainListener; -import net.minecraft.nbt.CompoundTag; - -public class DeparturePrediction { - - private Train train; - private int ticks; - private String scheduleTitle; - private String nextStop; - private StationInfo info; - - // Special data - private TrainExitSide exit = TrainExitSide.UNKNOWN; - - private int cycle; - - public DeparturePrediction(Train train, int ticks, String scheduleTitle, String nextStop, int cycle, StationInfo info) { - this.train = train; - this.ticks = ticks; - this.scheduleTitle = scheduleTitle; - this.nextStop = nextStop; - this.cycle = cycle; - this.info = info; - } - - public DeparturePrediction(TrainDeparturePrediction prediction) { - this(prediction.train, prediction.ticks, prediction.scheduleTitle.getString(), prediction.destination, 0, GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(prediction.destination).getInfoForStation(prediction.destination)); - } - - public DeparturePrediction copy() { - return new DeparturePrediction(getTrain(), getTicks(), getScheduleTitle(), getStationName(), getCycle(), getInfo()); - } - - public static DeparturePrediction withNextCycleTicks(DeparturePrediction current) { - int cycle = current.getCycle() + 1; - return new DeparturePrediction(current.getTrain(), (getTrainCycleDuration(current.getTrain()) * cycle) + current.getTicks(), current.getScheduleTitle(), current.getStationName(), cycle, current.getInfo()); - } - - public static DeparturePrediction withCycleTicks(DeparturePrediction current, int cycles) { - return new DeparturePrediction(current.getTrain(), (getTrainCycleDuration(current.getTrain()) * cycles) + current.getTicks(), current.getScheduleTitle(), current.getStationName(), cycles, current.getInfo()); - } - - public Train getTrain() { - return train; - } - - public int getTicks() { - return ticks; - } - - public String getScheduleTitle() { - return scheduleTitle; - } - - public String getStationName() { - return nextStop; - } - - public StationInfo getInfo() { - return info; - } - - public TrainStationAlias getNextStop() { - return GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(nextStop); - } - - public int getCycle() { - return cycle; - } - - public int getTrainCycleDuration() { - return getTrainCycleDuration(getTrain()); - } - - public static int getTrainCycleDuration(Train train) { - return TrainListener.getInstance().getApproximatedTrainDuration(train); - } - - public TrainExitSide getExitSide() { - return this.exit; - } - - public void setExit(TrainExitSide exit) { - this.exit = exit; - } - - public SimpleDeparturePrediction simplify() { - return new SimpleDeparturePrediction(getNextStop().getAliasName().get(), nextStop, getTicks(), getScheduleTitle(), getTrain().name.getString(), getTrain().id, getInfo(), getExitSide()); - } - - - @Override - public boolean equals(Object obj) { - if (obj instanceof DeparturePrediction other) { - return getTrain().id == other.getTrain().id && getTicks() == other.getTicks() && getScheduleTitle().equals(other.getScheduleTitle()) && getStationName().equals(other.getStationName()); - } - - return false; - } - - @Override - public int hashCode() { - return 19 * Objects.hash(getTrain().id, getTicks(), getScheduleTitle(), getStationName()); - } - - public boolean similar(DeparturePrediction other) { - return getTicks() == other.getTicks() && getStationName().equals(other.getStationName()); - } - - @Override - public String toString() { - return String.format("%s, Next stop: %s in %st", getTrain().name.getString(), getNextStop().getAliasName(), getTicks()); - } - - public static enum TrainExitSide { - UNKNOWN((byte)0), - RIGHT((byte)1), - LEFT((byte)-1); - - private byte side; - - TrainExitSide(byte side) { - this.side = side; - } - - public byte getAsByte() { - return side; - } - - public static TrainExitSide getFromByte(byte side) { - return Arrays.stream(values()).filter(x -> x.getAsByte() == side).findFirst().orElse(UNKNOWN); - } - - public TrainExitSide getOpposite() { - return getFromByte((byte)-getAsByte()); - } - } - - public static record SimpleDeparturePrediction(String stationTagName, String stationName, int departureTicks, String scheduleTitle, String trainName, UUID trainId, StationInfo stationInfo, TrainExitSide exitSide) { - - private static final String NBT_STATION = "station"; - private static final String NBT_STATION_NAME = "stationName"; - private static final String NBT_TICKS = "ticks"; - private static final String NBT_SCHEDULE_TITLE = "title"; - private static final String NBT_TRAIN_NAME = "tname"; - private static final String NBT_ID = "id"; - private static final String NBT_EXIT_SIDE = "exit"; - - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putString(NBT_STATION, stationTagName); - nbt.putString(NBT_STATION_NAME, stationName); - nbt.putInt(NBT_TICKS, departureTicks); - nbt.putString(NBT_SCHEDULE_TITLE, scheduleTitle); - nbt.putString(NBT_TRAIN_NAME, trainName); - nbt.putUUID(NBT_ID, trainId); - stationInfo().writeNbt(nbt); - nbt.putByte(NBT_EXIT_SIDE, exitSide.getAsByte()); - return nbt; - } - - public static SimpleDeparturePrediction fromNbt(CompoundTag nbt) { - return new SimpleDeparturePrediction( - nbt.getString(NBT_STATION), - nbt.getString(NBT_STATION_NAME), - nbt.getInt(NBT_TICKS), - nbt.getString(NBT_SCHEDULE_TITLE), - nbt.getString(NBT_TRAIN_NAME), - nbt.getUUID(NBT_ID), - StationInfo.fromNbt(nbt), - TrainExitSide.getFromByte(nbt.getByte(NBT_EXIT_SIDE)) - ); - } - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/EFilterCriteria.java b/common/src/main/java/de/mrjulsen/crn/data/EFilterCriteria.java deleted file mode 100644 index 0919e98c..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/EFilterCriteria.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.mrjulsen.crn.data; - -import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; -import net.minecraft.util.StringRepresentable; - -public enum EFilterCriteria implements StringRepresentable, ITranslatableEnum { - TRANSFER_COUNT(0, "transfer_count"), - DURATION(1, "duration"), - STOPOVERS(2, "stopovers"); - - private String name; - private int count; - - private EFilterCriteria(int count, String name) { - this.name = name; - this.count = count; - } - - public String getCriteriaName() { - return this.name; - } - - public int getId() { - return this.count; - } - - public static EFilterCriteria getCriteriaById(int count) { - for (EFilterCriteria shape : EFilterCriteria.values()) { - if (shape.getId() == count) { - return shape; - } - } - return EFilterCriteria.TRANSFER_COUNT; - } - - @Override - public String getSerializedName() { - return name; - } - - public static int getDataFromRoute(EFilterCriteria criteria, Route route) { - switch (criteria) { - case DURATION: - return route.getTotalDuration(); - case STOPOVERS: - return route.getStationCount(); - case TRANSFER_COUNT: - default: - return route.getTransferCount(); - } - } - - @Override - public String getEnumName() { - return "filter_criteria"; - } - - @Override - public String getEnumValueName() { - return this.name; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/EResultCount.java b/common/src/main/java/de/mrjulsen/crn/data/EResultCount.java deleted file mode 100644 index 745e5bb5..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/EResultCount.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.mrjulsen.crn.data; - -import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; -import net.minecraft.util.StringRepresentable; - -public enum EResultCount implements StringRepresentable, ITranslatableEnum { - ALL(0, "all"), - BEST(1, "best"), - FIXED_AMOUNT(2, "fixed_amount"); - - private String name; - private int count; - - private EResultCount(int count, String name) { - this.name = name; - this.count = count; - } - - public String getCriteriaName() { - return this.name; - } - - public int getId() { - return this.count; - } - - public static EResultCount getCriteriaById(int count) { - for (EResultCount shape : EResultCount.values()) { - if (shape.getId() == count) { - return shape; - } - } - return EResultCount.ALL; - } - - @Override - public String getSerializedName() { - return name; - } - - @Override - public String getEnumName() { - return "result_count"; - } - - @Override - public String getEnumValueName() { - return this.name; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/GlobalSettings.java b/common/src/main/java/de/mrjulsen/crn/data/GlobalSettings.java deleted file mode 100644 index 78f8648f..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/GlobalSettings.java +++ /dev/null @@ -1,358 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.Collection; -import java.util.Map; -import java.util.ArrayList; -import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; - -import com.simibubi.create.content.trains.entity.Train; -import com.simibubi.create.content.trains.station.GlobalStation; - -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; -import de.mrjulsen.crn.network.packets.cts.GlobalSettingsUpdatePacket; -import de.mrjulsen.crn.network.packets.cts.GlobalSettingsUpdatePacket.EGlobalSettingsAction; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.StringTag; -import net.minecraft.nbt.Tag; - -public class GlobalSettings { - - private static final String NBT_ALIAS_REGISTRY = "RegisteredAliasData"; - private static final String NBT_BLACKLIST = "StationBlacklist"; - private static final String NBT_TRAIN_BLACKLIST = "TrainBlacklist"; - private static final String NBT_TRAIN_GROUP_REGISTRY = "RegisteredTrainGroups"; - - private final Collection registeredAlias = new CopyOnWriteArrayList<>(); - private final Collection registeredTrainGroups = new CopyOnWriteArrayList<>(); - private final Collection blacklist = new CopyOnWriteArrayList<>(); - private final Collection trainsBlacklist = new CopyOnWriteArrayList<>(); - - - protected GlobalSettings() { - } - - public CompoundTag toNbt(CompoundTag pCompoundTag) { - if (registeredAlias != null && !registeredAlias.isEmpty()) { - ListTag aliasTag = new ListTag(); - aliasTag.addAll(registeredAlias.stream().map(x -> x.toNbt()).toList()); - pCompoundTag.put(NBT_ALIAS_REGISTRY, aliasTag); - } - - if (registeredTrainGroups != null && !registeredTrainGroups.isEmpty()) { - ListTag aliasTag = new ListTag(); - aliasTag.addAll(registeredTrainGroups.stream().map(x -> x.toNbt()).toList()); - pCompoundTag.put(NBT_TRAIN_GROUP_REGISTRY, aliasTag); - } - - if (blacklist != null && !blacklist.isEmpty()) { - ListTag blacklistTag = new ListTag(); - blacklistTag.addAll(blacklist.stream().map(x -> StringTag.valueOf(x)).toList()); - pCompoundTag.put(NBT_BLACKLIST, blacklistTag); - } - - if (trainsBlacklist != null && !trainsBlacklist.isEmpty()) { - ListTag blacklistTag = new ListTag(); - blacklistTag.addAll(trainsBlacklist.stream().map(x -> StringTag.valueOf(x)).toList()); - pCompoundTag.put(NBT_TRAIN_BLACKLIST, blacklistTag); - } - - return pCompoundTag; - } - - public static GlobalSettings fromNbt(CompoundTag tag) { - Collection aliasData = new ArrayList<>(); - Collection trainGroupData = new ArrayList<>(); - Collection blacklistData = new ArrayList<>(); - Collection trainBlacklistData = new ArrayList<>(); - - if (tag.contains(NBT_ALIAS_REGISTRY)) { - aliasData = tag.getList(NBT_ALIAS_REGISTRY, Tag.TAG_COMPOUND).stream().map(x -> TrainStationAlias.fromNbt((CompoundTag)x)).toList(); - } - - if (tag.contains(NBT_TRAIN_GROUP_REGISTRY)) { - trainGroupData = tag.getList(NBT_TRAIN_GROUP_REGISTRY, Tag.TAG_COMPOUND).stream().map(x -> TrainGroup.fromNbt((CompoundTag)x)).toList(); - } - - if (tag.contains(NBT_BLACKLIST)) { - blacklistData = tag.getList(NBT_BLACKLIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList(); - } - - if (tag.contains(NBT_TRAIN_BLACKLIST)) { - trainBlacklistData = tag.getList(NBT_TRAIN_BLACKLIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList(); - } - - GlobalSettings instance = new GlobalSettings(); - instance.registeredAlias.addAll(aliasData); - instance.blacklist.addAll(blacklistData); - instance.trainsBlacklist.addAll(trainBlacklistData); - instance.registeredTrainGroups.addAll(trainGroupData); - - return instance; - } - - - - // Setter methods for client - public boolean registerAlias(TrainStationAlias alias, Runnable then) { - GlobalSettingsUpdatePacket.send(alias, EGlobalSettingsAction.REGISTER_ALIAS, then); - return true; - } - - public boolean updateAlias(AliasName name, TrainStationAlias newData, Runnable then) { - GlobalSettingsUpdatePacket.send(new Object[] { name.get(), newData }, EGlobalSettingsAction.UPDATE_ALIAS, then); - return true; - } - - public boolean unregisterAlias(String name, Runnable then) { - GlobalSettingsUpdatePacket.send(name, EGlobalSettingsAction.UNREGISTER_ALIAS_STRING, then); - return true; - } - - public boolean unregisterAlias(TrainStationAlias alias, Runnable then) { - GlobalSettingsUpdatePacket.send(alias, EGlobalSettingsAction.UNREGISTER_ALIAS, then); - return true; - } - - - - public boolean registerTrainGroup(TrainGroup group, Runnable then) { - GlobalSettingsUpdatePacket.send(group, EGlobalSettingsAction.REGISTER_TRAIN_GROUP, then); - return true; - } - - public boolean updateTrainGroup(String name, TrainGroup newData, Runnable then) { - GlobalSettingsUpdatePacket.send(new Object[] { name, newData }, EGlobalSettingsAction.UPDATE_TRAIN_GROUP, then); - return true; - } - - public boolean unregisterTrainGroupTrain(String trainName, Runnable then) { - GlobalSettingsUpdatePacket.send(trainName, EGlobalSettingsAction.UNREGISTER_TRAIN_GROUP_TRAIN, then); - return true; - } - - public boolean unregisterTrainGroup(TrainGroup group, Runnable then) { - GlobalSettingsUpdatePacket.send(group, EGlobalSettingsAction.UNREGISTER_TRAIN_GROUP, then); - return true; - } - - - - public boolean addToBlacklist(String station, Runnable then) { - GlobalSettingsUpdatePacket.send(station, EGlobalSettingsAction.ADD_TO_BLACKLIST, then); - return true; - } - - public boolean removeFromBlacklist(String name, Runnable then) { - GlobalSettingsUpdatePacket.send(name, EGlobalSettingsAction.REMOVE_FROM_BLACKLIST, then); - return true; - } - - public boolean addTrainToBlacklist(String trainName, Runnable then) { - GlobalSettingsUpdatePacket.send(trainName, EGlobalSettingsAction.ADD_TRAIN_TO_BLACKLIST, then); - return true; - } - - public boolean removeTrainFromBlacklist(String name, Runnable then) { - GlobalSettingsUpdatePacket.send(name, EGlobalSettingsAction.REMOVE_TRAIN_FROM_BLACKLIST, then); - return true; - } - - - public boolean registerAliasForStationNames(String name, Collection stations, Runnable then) { - //TODO - return registerAlias(new TrainStationAlias(AliasName.of(name), stations.stream().collect(Collectors.toMap(x -> x, x -> StationInfo.empty()))), then); - } - - public boolean registerAlias(String name, Collection stations, Runnable then) { - //TODO - return registerAlias(new TrainStationAlias(AliasName.of(name), stations.stream().collect(Collectors.toMap(x -> x.name, x -> StationInfo.empty()))), then); - } - - - // Setter methods for server - public boolean registerAliasServer(TrainStationAlias alias) { - if (!registeredAlias.contains(alias)) { - registeredAlias.add(alias); - return true; - } - return false; - } - - public boolean updateAliasServer(AliasName name, TrainStationAlias newData) { - if (!registeredAlias.stream().anyMatch(x -> x.getAliasName().equals(name))) { - return false; - } - registeredAlias.stream().filter(x -> x.getAliasName().equals(name)).forEach(x -> x.update(newData)); - return true; - } - - public boolean unregisterAliasServer(String name) { - boolean b = registeredAlias.removeIf(x -> compareAliasAndString(x, name)); - return b; - } - - public boolean unregisterAliasServer(TrainStationAlias alias) { - boolean b = registeredAlias.removeIf(x -> x.equals(alias)); - return b; - } - - public boolean registerAliasForStationNamesServer(String name, Collection stations) { - //TODO - return registerAliasServer(new TrainStationAlias(AliasName.of(name), stations.stream().collect(Collectors.toMap(x -> x, x -> StationInfo.empty())))); - } - - public boolean registerAliasServer(String name, Collection stations) { - //TODO - return registerAliasServer(new TrainStationAlias(AliasName.of(name), stations.stream().collect(Collectors.toMap(x -> x.name, x -> StationInfo.empty())))); - } - - - - public boolean registerTrainGroupServer(TrainGroup group) { - if (!registeredTrainGroups.contains(group)) { - registeredTrainGroups.add(group); - return true; - } - return false; - } - - public boolean updateTrainGroupServer(String name, TrainGroup newData) { - if (!registeredTrainGroups.stream().anyMatch(x -> x.getGroupName().equals(name))) { - return false; - } - registeredTrainGroups.stream().filter(x -> x.getGroupName().equals(name)).forEach(x -> x.update(newData)); - return true; - } - - public boolean unregisterTrainGroupServer(String name) { - boolean b = registeredTrainGroups.removeIf(x -> x.getGroupName().equals(name)); - return b; - } - - public boolean unregisterAliasServer(TrainGroup group) { - boolean b = registeredTrainGroups.removeIf(x -> x.equals(group)); - return b; - } - - - public boolean addToBlacklistServer(String station) { - if (!blacklist.contains(station)) { - blacklist.add(station); - return true; - } - return false; - } - - public boolean removeFromBlacklistServer(String name) { - boolean b = blacklist.removeIf(x -> x.equals(name)); - return b; - } - - public boolean addTrainToBlacklistServer(String trainName) { - if (!trainsBlacklist.contains(trainName)) { - trainsBlacklist.add(trainName); - return true; - } - return false; - } - - public boolean removeTrainFromBlacklistServer(String trainName) { - boolean b = trainsBlacklist.removeIf(x -> x.equals(trainName)); - return b; - } - - - //### Getters and testers - // tags - public boolean isAliasRegistered(String stationName) { - return registeredAlias.stream().anyMatch(x -> x.contains(stationName)); - } - - public boolean isAliasRegistered(GlobalStation station) { - return isAliasRegistered(station.name); - } - - private TrainStationAlias getOrCreateAliasFor(String stationName) { - if (stationName.contains("*")) { - return getOrCreateAliasForWildcard(stationName); - } - - Optional a = registeredAlias.stream().filter(x -> x.contains(stationName)).findFirst(); - if (a.isPresent()) { - return a.get(); - } - - return new TrainStationAlias(AliasName.of(stationName), Map.of(stationName, StationInfo.empty())); - } - - private TrainStationAlias getOrCreateAliasForWildcard(String stationName) { - String regex = stationName.isBlank() ? stationName : "\\Q" + stationName.replace("*", "\\E.*\\Q") + "\\E"; - Optional a = registeredAlias.stream().filter(x -> x.getAllStationNames().stream().anyMatch(y -> y.matches(regex))).findFirst(); - if (a.isPresent()) { - return a.get(); - } - - return new TrainStationAlias(AliasName.of(stationName), Map.of(stationName, StationInfo.empty())); - - } - - private Optional getAlias(String stationName) { - Optional a = registeredAlias.stream().filter(x -> x.getAliasName().equals(AliasName.of(stationName))).findFirst(); - return a; - } - - public Collection getAliasList() { - return registeredAlias; - } - - public Collection getTrainGroupsList() { - return registeredTrainGroups; - } - - public TrainStationAlias getAliasFor(String stationName) { - Optional a = getAlias(stationName); - - if (!a.isPresent()) { - return getOrCreateAliasFor(stationName); - } - - return a.get(); - } - - private boolean compareAliasAndString(TrainStationAlias alias, String name) { - return alias.getAliasName().get().equals(name); - } - - - - // station blacklist - public boolean isBlacklisted(String stationName) { - return blacklist.stream().anyMatch(x -> x.equals(stationName)); - } - - public boolean isBlacklisted(TrainStationAlias station) { - return station.getAllStationNames().stream().allMatch(x -> isBlacklisted(x)); - } - - public Collection getBlacklist() { - return blacklist; - } - - - // train blacklist - public boolean isTrainBlacklisted(Train train) { - return trainsBlacklist.stream().anyMatch(x -> x.equals(train.name.getString())); - } - - public boolean isTrainBlacklisted(String trainName) { - return trainsBlacklist.stream().anyMatch(x -> x.equals(trainName)); - } - - public Collection getTrainBlacklist() { - return trainsBlacklist; - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/GlobalSettingsManager.java b/common/src/main/java/de/mrjulsen/crn/data/GlobalSettingsManager.java deleted file mode 100644 index 7115758f..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/GlobalSettingsManager.java +++ /dev/null @@ -1,82 +0,0 @@ -package de.mrjulsen.crn.data; - -import de.mrjulsen.crn.CRNPlatformSpecific; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.GlobalSettingsRequestPacket; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.saveddata.SavedData; - -public class GlobalSettingsManager extends SavedData { - - private static final String FILE_ID = CreateRailwaysNavigator.MOD_ID + "_global_settings"; - private static volatile GlobalSettingsManager instance; - - private GlobalSettings settingsData; - - private GlobalSettingsManager() { - settingsData = new GlobalSettings(); - CreateRailwaysNavigator.LOGGER.info("Created new Create Railways Navigator settings instance."); - } - - private GlobalSettingsManager(GlobalSettings settings) { - instance = this; - settingsData = settings; - } - - - public static GlobalSettingsManager createClientInstance() { - if (instance == null) { - instance = new GlobalSettingsManager(); - } - return instance; - } - - public static GlobalSettingsManager getInstance() { - if (instance == null) { - MinecraftServer server = CRNPlatformSpecific.getServer(); - if (server == null) { - // execute on client - instance = new GlobalSettingsManager(); - } else { - // execute on server - CreateRailwaysNavigator.LOGGER.debug("Create Instance"); - ServerLevel level = server.overworld(); - instance = level.getDataStorage().computeIfAbsent(GlobalSettingsManager::load, GlobalSettingsManager::new, FILE_ID); - } - } - return instance; - } - - @Override - public CompoundTag save(CompoundTag pCompoundTag) { - return settingsData.toNbt(pCompoundTag); - } - - private static GlobalSettingsManager load(CompoundTag tag) { - GlobalSettings settings = GlobalSettings.fromNbt(tag); - return new GlobalSettingsManager(settings); - - } - - public final GlobalSettings getSettingsData() { - return settingsData; - } - - public void updateSettingsData(GlobalSettings settings) { - this.settingsData = settings; - setDirty(); - } - - public static void syncToClient(Runnable then) { - long id = InstanceManager.registerClientResponseReceievedAction(then); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new GlobalSettingsRequestPacket(id)); - } - - public static void close() { - instance = null; - CreateRailwaysNavigator.LOGGER.info("Closed current Create Railways Navigator settings instance."); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/GlobalTrainData.java b/common/src/main/java/de/mrjulsen/crn/data/GlobalTrainData.java deleted file mode 100644 index dce7e60e..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/GlobalTrainData.java +++ /dev/null @@ -1,268 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -import com.simibubi.create.content.trains.entity.Train; -import com.simibubi.create.content.trains.station.GlobalStation; - -import de.mrjulsen.crn.util.TrainUtils; - -public class GlobalTrainData { - - private final Collection trains; - private final Map> stationByName; - private final Map> aliasPredictions = new HashMap<>(); - private final Map> trainPredictions = new HashMap<>(); - private final long updateTime; - - private static GlobalTrainData instance = null; - - private GlobalTrainData(long updateTime) { - instance = this; - trains = TrainUtils.getAllTrains(); - - stationByName = TrainUtils.getAllStations().stream().collect(Collectors.groupingBy(x -> x.name, Collectors.toSet())); - /* - for (GlobalStation sta : TrainUtils.getAllStations()) { - stationByName.computeIfAbsent(sta.name, x -> new HashSet<>()).add(sta); - if (stationByName.containsKey(sta.name)) { - stationByName.get(sta.name).add(sta); - } else { - stationByName.put(sta.name, new HashSet<>(Set.of(sta))); - } - } - */ - TrainUtils.getMappedDeparturePredictions(aliasPredictions, trainPredictions); - this.updateTime = updateTime; - } - - public static GlobalTrainData makeSnapshot(long updateTime) { - return new GlobalTrainData(updateTime); - } - - public static GlobalTrainData getInstance() { - return instance; - } - - public long getUpdateTime() { - return updateTime; - } - - - public boolean stationHasDepartingTrains(TrainStationAlias alias) { - return aliasPredictions.containsKey(alias.getAliasName().get()); - } - - public final Collection getAllTrains() { - return trains; - } - - public final Set getAllStations() { - return stationByName.keySet(); - } - - public final Set getStationData(String stationName) { - return stationByName.get(stationName); - } - - - - - public Collection getPredictionsOfTrain(Train train) { - return trainPredictions.get(train.id).stream().filter(x -> !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(x.getStationName())).toList(); - } - - public Collection getPredictionsOfTrainChronologically(Train train) { - return getPredictionsOfTrain(train).parallelStream().sorted(Comparator.comparingInt(x -> x.getTicks())).toList(); - } - - public Optional getNextStop(Train train) { - return getPredictionsOfTrainChronologically(train).stream().findFirst(); - } - - - public boolean trainStopsAt(Train train, TrainStationAlias station) { - return getPredictionsOfTrain(train).parallelStream().anyMatch(x -> x.getNextStop().equals(station)); - } - - - public Collection getTrainStopDataAt(Train train, TrainStationAlias station) { - Collection predictions = getPredictionsOfTrain(train); - return predictions.parallelStream().filter(x -> x.getNextStop().equals(station)).toList(); - } - - public Collection getSortedTrainStopDataAt(Train train, TrainStationAlias station) { - return getTrainStopDataAt(train, station).parallelStream().sorted(Comparator.comparingInt(x -> x.getTicks())).toList(); - } - - public Optional getNextTrainStopDataAt(Train train, TrainStationAlias station) { - return getTrainStopDataAt(train, station).stream().findFirst(); - } - - public Collection getAllStops(Train train) { - return getPredictionsOfTrain(train).parallelStream().map(x -> new TrainStop(x.getNextStop(), x)).toList(); - } - - public List getAllStopsSorted(Train train) { - return getAllStops(train).parallelStream().sorted(Comparator.comparingInt(x -> x.getPrediction().getTicks())).toList(); - } - - public SimpleTrainSchedule getTrainSimpleSchedule(Train train) { - return new SimpleTrainSchedule(train); - } - - public List getAllStopoversOfTrainSortedNew(Train train, TrainStationAlias start, TrainStationAlias end, boolean includeStartEnd, boolean correctStart) { - Collection stops = getAllStopsFrom(train, start, false, true).getAllStops(); - - if (stops.parallelStream().noneMatch(x -> x.isStationAlias(start) || x.isStationAlias(end))) { - return new ArrayList<>(); - } - - DeparturePrediction startPrediction = null; - DeparturePrediction endPrediction = null; - int ticksStart = -1; - int ticksStop = -1; - - Collection startStopDatas = getSortedTrainStopDataAt(train, start); - Collection endStopDatas = getSortedTrainStopDataAt(train, end); - - Optional firstStartData = startStopDatas.stream().findFirst(); - - if (firstStartData.isPresent()) { - ticksStart = firstStartData.get().getTicks(); - } - final int ftmpTicksStart = ticksStart; - - if (endStopDatas != null && endStopDatas.size() > 0) { - // Die erste Endstation, die nach der ersten Startstation kommt. - Optional endStopData = endStopDatas.stream() - .filter(x -> x.getTicks() >= ftmpTicksStart) - .findFirst(); - - if (endStopData.isPresent()) { - ticksStop = (endPrediction = endStopData.get()).getTicks(); - } else { - endStopData = endStopDatas.stream().findFirst(); - if (endStopData.isPresent()) { - ticksStop = (endPrediction = endStopData.get()).getTicks(); - } - } - } - final int fTicksStop = ticksStop; - - if (startStopDatas != null && startStopDatas.size() > 0) { - DeparturePrediction startStopData = correctStart ? - startStopDatas.stream() - .filter(x -> x.getTicks() <= fTicksStop) - .reduce((a, b) -> b).orElse(null) - : startPrediction; - - if (startStopData != null) { - ticksStart = (startPrediction = startStopData).getTicks(); - } else { - startStopData = startStopDatas.stream().reduce((a, b) -> b).orElse(null); - if (startStopData != null) { - ticksStart = (startPrediction = startStopData).getTicks(); - } - } - } - - if (correctStart) { - - } - final int fTicksStart = ticksStart; - - List filteredStops = new ArrayList<>(); - if (fTicksStart <= fTicksStop) { - filteredStops.addAll(stops.stream().filter(x -> (x.getPrediction().getTicks() > fTicksStart && x.getPrediction().getTicks() < fTicksStop)).toList()); - } else { - filteredStops.addAll(stops.stream().filter(x -> (x.getPrediction().getTicks() < fTicksStop || x.getPrediction().getTicks() > fTicksStart)).map(x -> { - if (x.getPrediction().getTicks() < fTicksStop) { - return new TrainStop(x.getStationAlias(), DeparturePrediction.withNextCycleTicks(x.getPrediction())); - } - return x; - }).toList()); - } - - if (includeStartEnd) { - filteredStops.add(new TrainStop(end, fTicksStop < fTicksStart ? DeparturePrediction.withNextCycleTicks(endPrediction) : endPrediction)); - filteredStops.add(0, new TrainStop(start, startPrediction)); - } - - return filteredStops; - } - - // TODO unused - /** - * Creates a {@code SimpleTrainSchedule} which contains all stations the train will arrive. - * @param train The train of this schedule. - * @param alias The station alias to start at. This is the first stop in the schedule. - * @param preventDuplicates If {@code true}, every station exists once. - * @param noLoop If {@code true}, the schedule will continue beyond the last stop. - * @return A new {@code SimpleTrainSchedule} - */ - public SimpleTrainSchedule getAllStopsFrom(Train train, TrainStationAlias alias, boolean preventDuplicates, boolean loop) { - List newList = new ArrayList<>(); - int idx = 0; - for (TrainStop stop : getAllStopsSorted(train)) { - if (preventDuplicates && newList.contains(stop)) { - if (loop) { - continue; - } else { - break; - } - } - - if (stop.getStationAlias().equals(alias)) { - idx = 0; - } - - newList.add(idx, stop); - idx++; - } - return SimpleTrainSchedule.of(newList); - } - - - public SimpleTrainSchedule getDirectionalSchedule(Train train) { - List newList = new ArrayList<>(); - boolean isRepeating = false; - for (TrainStop stop : getAllStopsSorted(train)) { - if (newList.contains(stop)) { - isRepeating = true; - continue; - } - - if (isRepeating) { - newList.add(0, stop); - } else { - newList.add(stop); - } - } - return SimpleTrainSchedule.of(newList); - } - - public Collection getDepartingTrainsAt(TrainStationAlias station) { - return aliasPredictions.getOrDefault(station.getAliasName().get(), Collections.emptyList()).parallelStream().sorted(Comparator.comparingInt(x -> x.getTicks())).toList(); - } - - public Optional getNextDepartingTrainAt(TrainStationAlias station) { - Collection predictions = getDepartingTrainsAt(station); - - if (predictions == null || predictions.isEmpty()) { - return Optional.empty(); - } - - return predictions.stream().findFirst(); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java b/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java new file mode 100644 index 00000000..781efc63 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java @@ -0,0 +1,28 @@ +package de.mrjulsen.crn.data; + +import java.util.List; + +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.data.Pair; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public interface ISaveableNavigatorData { + + /** All lines with information to be displayed in the overview. */ + List getOverviewData(); + /** Content of the title line. */ + SaveableNavigatorDataLine getTitle(); + /** The value (usually the time at which the corresponding entry is relevant) by which the items are sorted and grouped. */ + long timeOrderValue(); + default long dayOrderValue() { + return (timeOrderValue() + DragonLib.DAYTIME_SHIFT) / DragonLib.TICKS_PER_DAY; + } + /** Custom value used for grouping with custom label. Default: {@code null} (grouped by time) */ + default Pair customGroup() { + return null; + } + + public static record SaveableNavigatorDataLine(Component text, Sprite icon) {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/ITranslatableEnum.java b/common/src/main/java/de/mrjulsen/crn/data/ITranslatableEnum.java deleted file mode 100644 index 4752e76f..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/ITranslatableEnum.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.mrjulsen.crn.data; - -import de.mrjulsen.crn.CreateRailwaysNavigator; - -public interface ITranslatableEnum { - String getNameOfEnum(); - String getValue(); - - default String getTranslationKey() { - return String.format("gui.%s.%s.%s", CreateRailwaysNavigator.MOD_ID, this.getNameOfEnum(), this.getValue()); - } - default String getDescriptionTranslationKey() { - return String.format("gui.%s.%s.description", CreateRailwaysNavigator.MOD_ID, this.getNameOfEnum()); - } - default String getInfoTranslationKey() { - return String.format("gui.%s.%s.info.%s", CreateRailwaysNavigator.MOD_ID, this.getNameOfEnum(), this.getValue()); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/NearestTrackStationResult.java b/common/src/main/java/de/mrjulsen/crn/data/NearestTrackStationResult.java index 4de841f8..5c979c42 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/NearestTrackStationResult.java +++ b/common/src/main/java/de/mrjulsen/crn/data/NearestTrackStationResult.java @@ -4,40 +4,42 @@ import com.simibubi.create.content.trains.station.GlobalStation; -import net.minecraft.network.FriendlyByteBuf; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import net.minecraft.nbt.CompoundTag; public class NearestTrackStationResult { + + public static final String NBT_DISTANCE = "Distance"; + public static final String NBT_TAG = "TagName"; + + public final double distance; - public final Optional aliasName; + public final Optional tagName; public NearestTrackStationResult(Optional station, double distance) { - this(station.isPresent() ? GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(station.get().name) : null, distance); + this(station.isPresent() ? GlobalSettings.getInstance().getOrCreateStationTagFor(station.get().name).getTagName() : null, distance); } - private NearestTrackStationResult(TrainStationAlias station, double distance) { + private NearestTrackStationResult(TagName tagName, double distance) { this.distance = distance; - this.aliasName = station != null ? Optional.of(station) : Optional.empty(); + this.tagName = Optional.ofNullable(tagName); } public static NearestTrackStationResult empty() { return new NearestTrackStationResult(Optional.empty(), 0); } - public void serialize(FriendlyByteBuf buffer) { - buffer.writeBoolean(aliasName.isPresent()); - if (aliasName.isPresent()) { - buffer.writeNbt(aliasName.get().toNbt()); - } - buffer.writeDouble(distance); + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putDouble(NBT_DISTANCE, distance); + tagName.ifPresent(x -> nbt.putString(NBT_TAG, tagName.get().get())); + return nbt; } - public static NearestTrackStationResult deserialize(FriendlyByteBuf buffer) { - boolean valid = buffer.readBoolean(); - TrainStationAlias alias = null; - if (valid) { - alias = TrainStationAlias.fromNbt(buffer.readNbt()); - } - double distance = buffer.readDouble(); - return new NearestTrackStationResult(alias, distance); + public static NearestTrackStationResult fromNbt(CompoundTag nbt) { + return new NearestTrackStationResult( + nbt.contains(NBT_TAG) ? TagName.of(nbt.getString(NBT_TAG)) : null, + nbt.getDouble(NBT_DISTANCE) + ); } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/Route.java b/common/src/main/java/de/mrjulsen/crn/data/Route.java deleted file mode 100644 index 4dd6a081..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/Route.java +++ /dev/null @@ -1,101 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -public class Route { - private Collection parts = new ArrayList<>(); - private final long refreshTime; - - public Route(Collection initialValues, long refreshTime) { - this(refreshTime); - this.parts.addAll(initialValues); - } - - public Route(long refreshTime) { - this.refreshTime = refreshTime; - } - - public void addPart(RoutePart part) { - if (!contains(part)) { - parts.add(part); - } - } - - public boolean isEmpty() { - return parts.size() <= 0; - } - - public long getRefreshTime() { - return refreshTime; - } - - public boolean contains(RoutePart part) { - return parts.contains(part); - } - - public Collection getParts() { - return parts; - } - - public int getStationCount() { - return parts.stream().mapToInt(x -> x.getStationCount(false)).sum() + parts.size() + 1; - } - - public int getTransferCount() { - return getParts().size() - 1; - } - - public int getTotalDuration() { - return getEndStation().getPrediction().getTicks() - getStartStation().getPrediction().getTicks(); - } - - public TrainStop getStartStation() { - return parts.stream().findFirst().get().getStartStation(); - } - - public TrainStop getEndStation() { - return parts.stream().reduce((a, b) -> b).get().getEndStation(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Route other) { - return getParts().size() == other.getParts().size() && getParts().stream().allMatch(x -> other.getParts().stream().anyMatch(y -> y.equals(x))); - } - return false; - } - - public boolean exactEquals(Object obj, boolean respectOrder, boolean respectTrains) { - if (obj instanceof Route other) { - if (getParts().size() != other.getParts().size()) { - return false; - } - - RoutePart[] a = getParts().toArray(RoutePart[]::new); - RoutePart[] b = other.getParts().toArray(RoutePart[]::new); - - for (int i = 0; i < a.length; i++) { - if ((respectOrder && !a[i].exactEquals(b[i], respectTrains)) || (!respectOrder && !a[i].equals(b[i]))) { - return false; - } - } - return true; - } - return false; - } - - public String toName() { - return String.format("%s - %s", - parts.stream().findFirst().get().getStartStation().getStationAlias().getAliasName(), - parts.stream().reduce((a, b) -> b).get().getEndStation().getStationAlias().getAliasName() - ); - } - - @Override - public String toString() { - return Arrays.toString(getParts().toArray()); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/RoutePart.java b/common/src/main/java/de/mrjulsen/crn/data/RoutePart.java deleted file mode 100644 index c9787e3c..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/RoutePart.java +++ /dev/null @@ -1,126 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Optional; -import java.util.List; - -import com.simibubi.create.content.trains.entity.Train; - -import net.minecraft.world.level.Level; - -public class RoutePart { - private Train train; - private TrainStop start; - private TrainStop end; - private Collection stops; - - public RoutePart(Level level, Train train, TrainStationAlias start, TrainStationAlias end, int startTicks) { - this.train = train; - List stops = new ArrayList<>(GlobalTrainData.getInstance().getAllStopoversOfTrainSortedNew(train, start, end, true, true)); - - TrainStop startStop = stops.get(0); - if (startStop.getPrediction().getTicks() < startTicks && startStop.getPrediction().getTrainCycleDuration() > 0) { - int diffTicks = startTicks - startStop.getPrediction().getTicks(); - int mul = diffTicks / startStop.getPrediction().getTrainCycleDuration() + 1; - - stops = new ArrayList<>(stops.stream().map(x -> new TrainStop(x.getStationAlias(), DeparturePrediction.withCycleTicks(x.getPrediction(), mul))).toList()); - } - - this.end = stops.remove(stops.size() - 1); - this.start = stops.remove(0); - - this.stops = stops; - } - - public Train getTrain() { - return train; - } - - public TrainStop getStartStation() { - return start; - } - - public TrainStop getEndStation() { - return end; - } - - public Collection getStopovers() { - return stops; - } - - public int arrivesAtStartStationIn() { - Optional data = GlobalTrainData.getInstance().getNextTrainStopDataAt(getTrain(), getStartStation().getStationAlias()); - if (data.isPresent()) { - return data.get().getTicks(); - } - return 0; - } - - public int arrivesAtEndStationIn() { - Optional data = GlobalTrainData.getInstance().getNextTrainStopDataAt(getTrain(), getEndStation().getStationAlias()); - if (data.isPresent()) { - return data.get().getTicks(); - } - return 0; - } - - public int getStationCount(boolean includeStartEnd) { - return getStopovers().size() + (includeStartEnd ? 2 : 0); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof RoutePart other) { - return getStartStation().equals(other.getStartStation()) && - getEndStation().equals(other.getEndStation()) && - getStopovers().size() == other.getStopovers().size() && - getStopovers().stream().allMatch(x -> other.getStopovers().stream().anyMatch(y -> y.equals(x))); - } - return false; - } - - public boolean exactEquals(Object obj, boolean respectTrains) { - if (obj instanceof RoutePart other) { - if (!getStartStation().equals(other.getStartStation()) || - !getEndStation().equals(other.getEndStation()) || - getStopovers().size() != other.getStopovers().size()) - return false; - - TrainStop[] a = getStopovers().toArray(TrainStop[]::new); - TrainStop[] b = other.getStopovers().toArray(TrainStop[]::new); - - for (int i = 0; i < a.length; i++) { - if (!a[i].equals(b[i])) - return false; - } - return !respectTrains || train.equals(other.train); - } - return false; - } - - @Override - public String toString() { - return String.format("%s, From: %s (%s), To: %s (%s), via: %s", - getTrain().name.getString(), - getStartStation().getStationAlias().getAliasName(), - arrivesAtStartStationIn(), - getEndStation().getStationAlias().getAliasName(), - arrivesAtEndStationIn(), - Arrays.toString(stops.toArray()) - ); - - - } - - public String getString() { - return String.format("Train: %s, From: %s in %st, To: %s in %s, Stops: %s", - getTrain().name.getString(), - getStartStation().getStationAlias().getAliasName(), - arrivesAtStartStationIn(), - getEndStation().getStationAlias().getAliasName(), - arrivesAtEndStationIn(), - Arrays.toString(stops.toArray())); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/SavedRoutesManager.java b/common/src/main/java/de/mrjulsen/crn/data/SavedRoutesManager.java new file mode 100644 index 00000000..55bc325e --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/SavedRoutesManager.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.data; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.nbt.CompoundTag; + +public final class SavedRoutesManager { + private static final LinkedHashSet savedRoutes = new LinkedHashSet<>(); + private static MutableSingle isSynchronizing = new MutableSingle(false); + + public static void saveRoute(ClientRoute route) { + route.addListener(); + savedRoutes.add(route); + } + + public static void removeRoute(ClientRoute route) { + route.close(); + savedRoutes.remove(route); + } + + public static void removeAllRoutes() { + savedRoutes.forEach(x -> x.closeAll()); + savedRoutes.clear(); + } + + public static boolean isSaved(ClientRoute route) { + return savedRoutes.contains(route); + } + + public static List getAllSavedRoutes() { + return new ArrayList<>(savedRoutes); + } + + @SuppressWarnings("resource") + public static void push(boolean clear, Runnable andThen) { + isSynchronizing.setFirst(true); + DataAccessor.getFromServer(Minecraft.getInstance().player.getUUID(), ModAccessorTypes.GET_USER_SETTINGS, (settings) -> { + Set currentValue = clear ? new HashSet<>() : settings.savedRoutes.getValue(); + currentValue.addAll(savedRoutes.stream().map(x -> x.toNbt()).toList()); + settings.savedRoutes.setValue(currentValue); + settings.clientSave(() -> { + isSynchronizing.setFirst(false); + DLUtils.doIfNotNull(andThen, Runnable::run); + }); + }); + } + + @SuppressWarnings("resource") + public static void pull(boolean clear, Runnable andThen) { + isSynchronizing.setFirst(true); + DataAccessor.getFromServer(Minecraft.getInstance().player.getUUID(), ModAccessorTypes.GET_USER_SETTINGS, (settings) -> { + Set currentValue = settings.savedRoutes.getValue().stream().map(x -> ClientRoute.fromNbt(x, true)).collect(Collectors.toSet()); + if (clear) { + savedRoutes.clear(); + } + savedRoutes.addAll(currentValue); + isSynchronizing.setFirst(false); + DLUtils.doIfNotNull(andThen, Runnable::run); + }); + } + + public static boolean isSynchronizing() { + return isSynchronizing.getFirst(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/SimpleRoute.java b/common/src/main/java/de/mrjulsen/crn/data/SimpleRoute.java deleted file mode 100644 index 05f2d764..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/SimpleRoute.java +++ /dev/null @@ -1,508 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.UUID; - -import com.simibubi.create.content.trains.entity.TrainIconType; - -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; -import de.mrjulsen.crn.event.listeners.IJourneyListenerClient; -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; -import net.minecraft.resources.ResourceLocation; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class SimpleRoute { - - private static final String NBT_PARTS = "Parts"; - private static final String NBT_REFRESH_TIME = "RefreshTime"; - - protected long refreshTime; - protected final List parts; - - // Cache - protected int stationCount = -1; - protected StationEntry startStation = null; - protected StationEntry endStation = null; - protected StationEntry[] stationArray = null; - protected boolean valid = true; - protected String invalidationReason = ""; - protected String invalidationTrainName = ""; - - // listener - protected UUID listenerId; - - public SimpleRoute(Route route) { - this(route.getParts().stream().map(x -> new SimpleRoutePart(x, route.getRefreshTime())).toList(), route.getRefreshTime()); - } - - protected SimpleRoute(List parts, long refreshTime) { - this.parts = new ArrayList<>(parts); - this.refreshTime = refreshTime; - - parts.forEach(x -> x.setParent(this)); - tagAll(); - } - - public UUID listen(IJourneyListenerClient initialListener) { - return listenerId = JourneyListenerManager.getInstance().create(this, initialListener); - } - - public UUID getListenerId() { - return listenerId; - } - - public List getParts() { - return parts; - } - - public long getRefreshTime() { - return refreshTime; - } - - protected void tagAll() { - final int maxIndex = getParts().size() - 1; - int partIndex = 0; - int stationIndex = 0; - - for (SimpleRoutePart part : getParts()) { - final int idx = partIndex; - final StationTrainDetails trainDetails = new StationTrainDetails(part.getTrainName(), part.getTrainID(), part.getScheduleTitle()); - - part.getStartStation().train = trainDetails; - part.getStartStation().tag = idx <= 0 ? StationTag.START : StationTag.PART_START; - part.getStartStation().index = stationIndex; - stationIndex++; - - for (StationEntry station : part.getStopovers()) { - station.train = trainDetails; - station.tag = StationTag.TRANSIT; - station.index = stationIndex; - stationIndex++; - } - - part.getEndStation().train = trainDetails; - part.getEndStation().tag = idx >= maxIndex ? StationTag.END : StationTag.PART_END; - part.getEndStation().index = stationIndex; - - stationIndex++; - partIndex++; - } - } - - public void shiftTime(int amount) { - parts.forEach(x -> x.shiftTime(amount)); - refreshTime += amount; - } - - public int getStationCount(boolean countTransfersTwice) { - return stationCount < 0 ? stationCount = parts.stream().mapToInt(x -> x.getStationCount(false)).sum() + (countTransfersTwice ? parts.size() * 2 : parts.size() + 1) : stationCount; - } - - public int getTransferCount() { - return getParts().size() - 1; - } - - public int getTotalDuration() { - return getEndStation().getTicks() - getStartStation().getTicks(); - } - - protected void invalidate(String reason, String trainName) { - this.valid = false; - this.invalidationReason = reason; - this.invalidationTrainName = trainName; - } - - public boolean isValid() { - return valid; - } - - public String getInvalidationReason() { - return invalidationReason; - } - - public String getInvalidationTrainName() { - return invalidationTrainName; - } - - public StationEntry getStartStation() { - return startStation == null ? startStation = getParts().stream().findFirst().get().getStartStation() : startStation; - } - - public StationEntry getEndStation() { - return endStation == null ? endStation = getParts().stream().reduce((a, b) -> b).get().getEndStation() : endStation; - } - - public String getName() { - return String.format("%s - %s", getStartStation().getStationName(), getEndStation().getStationName()); - } - - public StationEntry[] getStationArray() { - return stationArray == null ? stationArray = getParts().stream().flatMap(x -> x.getStations().stream()).toArray(StationEntry[]::new) : stationArray; - } - - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putLong(NBT_REFRESH_TIME, getRefreshTime()); - ListTag partsTag = new ListTag(); - partsTag.addAll(getParts().stream().map(x -> x.toNbt()).toList()); - nbt.put(NBT_PARTS, partsTag); - return nbt; - } - - public static SimpleRoute fromNbt(CompoundTag nbt) { - long refreshTime = nbt.getLong(NBT_REFRESH_TIME); - List parts = new ArrayList<>(nbt.getList(NBT_PARTS, Tag.TAG_COMPOUND).stream().map(x -> SimpleRoutePart.fromNbt((CompoundTag)x, refreshTime)).toList()); - SimpleRoute route = new SimpleRoute(parts, refreshTime); - parts.forEach(x -> x.setParent(route)); - return route; - } - - public static class SimpleRoutePart { - private static final String NBT_TRAIN_NAME = "TrainName"; - private static final String NBT_TRAIN_ID = "TrainId"; - private static final String NBT_TRAIN_ICON_ID = "TrainIconId"; - private static final String NBT_SCHEDULE_TITLE = "ScheduleTitle"; - private static final String NBT_START_STATION = "StartStation"; - private static final String NBT_END_STATION = "EndStation"; - private static final String NBT_STOPOVERS = "Stopovers"; - - protected SimpleRoute parent; - - protected final String trainName; - protected final UUID trainId; - protected final ResourceLocation trainIconId; - protected final String scheduleTitle; - protected final StationEntry start; - protected final StationEntry end; - protected final Collection stopovers; - - // Cache - protected List allStations = null; - - public SimpleRoutePart(RoutePart part, long refreshTime) { - this( - part.getTrain().name.getString(), - part.getTrain().id, - part.getTrain().icon.getId(), - part.getStartStation().getPrediction().getScheduleTitle(), - new StationEntry(part.getStartStation(), refreshTime), - new StationEntry(part.getEndStation(), refreshTime), - part.getStopovers().stream().map(x -> new StationEntry(x, refreshTime)).toList() - ); - } - - protected SimpleRoutePart(String trainName, UUID trainId, ResourceLocation trainIconId, String scheduleTitle, StationEntry start, StationEntry end, Collection stopovers) { - this.trainName = trainName; - this.trainId = trainId; - this.trainIconId = trainIconId; - this.scheduleTitle = scheduleTitle; - this.start = start; - this.end = end; - this.stopovers = stopovers; - - getStations().forEach(x -> x.setParent(this)); - } - - public void shiftTime(int amount) { - getStations().forEach(x -> x.shiftTime(amount)); - } - - protected void setParent(SimpleRoute parent) { - this.parent = parent; - } - - protected SimpleRoute getParent() { - return parent; - } - - public String getTrainName() { - return trainName; - } - - public UUID getTrainID() { - return trainId; - } - - public TrainIconType getTrainIcon() { - return TrainIconType.byId(trainIconId); - } - - public String getScheduleTitle() { - return scheduleTitle; - } - - public StationEntry getStartStation() { - return start; - } - - public StationEntry getEndStation() { - return end; - } - - public Collection getStopovers() { - return stopovers; - } - - public List getStations() { - if (allStations != null) { - return allStations; - } - allStations = new ArrayList<>(stopovers); - allStations.add(0, getStartStation()); - allStations.add(getEndStation()); - return allStations; - } - - public int getStationCount(boolean includeStartEnd) { - return getStopovers().size() + (includeStartEnd ? 2 : 0); - } - - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putString(NBT_TRAIN_NAME, getTrainName()); - nbt.putString(NBT_SCHEDULE_TITLE, getScheduleTitle()); - nbt.putUUID(NBT_TRAIN_ID, getTrainID()); - nbt.putString(NBT_TRAIN_ICON_ID, trainIconId.toString()); - nbt.put(NBT_START_STATION, getStartStation().toNbt()); - nbt.put(NBT_END_STATION, getEndStation().toNbt()); - ListTag stopoversTag = new ListTag(); - stopoversTag.addAll(stopovers.stream().map(x -> x.toNbt()).toList()); - nbt.put(NBT_STOPOVERS, stopoversTag); - return nbt; - } - - public static SimpleRoutePart fromNbt(CompoundTag nbt, long refreshTime) { - String trainName = nbt.getString(NBT_TRAIN_NAME); - String scheduleTitle = nbt.getString(NBT_SCHEDULE_TITLE); - UUID trainId = nbt.getUUID(NBT_TRAIN_ID); - ResourceLocation trainIconId = new ResourceLocation(nbt.getString(NBT_TRAIN_ICON_ID)); - StationEntry start = StationEntry.fromNbt(nbt.getCompound(NBT_START_STATION), refreshTime); - StationEntry end = StationEntry.fromNbt(nbt.getCompound(NBT_END_STATION), refreshTime); - Collection stopovers = nbt.getList(NBT_STOPOVERS, Tag.TAG_COMPOUND).stream().map(x -> StationEntry.fromNbt((CompoundTag)x, refreshTime)).toList(); - - SimpleRoutePart part = new SimpleRoutePart(trainName, trainId, trainIconId, scheduleTitle, start, end, stopovers); - start.setParent(part); - end.setParent(part); - stopovers.forEach(x -> x.setParent(part)); - return part; - } - } - - public static class StationEntry { - private static final String NBT_NAME = "Name"; - private static final String NBT_TICKS = "Ticks"; - - protected final String stationName; - protected final StationInfo info; - protected int ticks; - protected long refreshTime; - - protected int index; - protected StationTrainDetails train; - protected StationTag tag; - - protected boolean departed = false; - protected boolean willMiss = false; - protected boolean trainCanceled = false; - - protected int timeShift = 0; - - protected int currentTicks; - protected long currentRefreshTime; - protected StationInfo updatedStationInfo; - protected boolean wasUpdated = false; - - protected SimpleRoutePart parent; - - public StationEntry(TrainStop stop, long refreshTime) { - this( - stop.getStationAlias().getAliasName().get(), - stop.getStationAlias().getInfoForStation(stop.getPrediction().getStationName()), - stop.getPrediction().getTicks(), refreshTime - ); - } - - protected StationEntry(String stationName, StationInfo info, int ticks, long refreshTime) { - this.stationName = stationName; - this.info = info; - this.ticks = ticks; - this.refreshTime = refreshTime; - this.currentTicks = ticks; - this.currentRefreshTime = refreshTime; - this.updatedStationInfo = info; - } - - public void shiftTime(int amount) { - timeShift += amount; - refreshTime += amount; - currentRefreshTime += amount; - - } - - public int getTimeShift() { - return timeShift; - } - - protected void setParent(SimpleRoutePart parent) { - this.parent = parent; - } - - public SimpleRoutePart getParent() { - return parent; - } - - public String getStationName() { - return stationName; - } - - public int getTicks() { - return ticks; - } - - public long getRefreshTime() { - return refreshTime; - } - - public int getCurrentTicks() { - return currentTicks; - } - - public long getCurrentRefreshTime() { - return currentRefreshTime; - } - - public long getCurrentTime() { - return currentRefreshTime + currentTicks; - } - - public StationInfo getInfo() { - return info; - } - - public boolean stationInfoChanged() { - return !getInfo().equals(getUpdatedInfo()); - } - - public StationInfo getUpdatedInfo() { - return updatedStationInfo; - } - - public void updateRealtimeData(int ticks, long refreshTime, StationInfo info, Runnable onDelayed) { - this.wasUpdated = true; - - this.currentRefreshTime = refreshTime; - this.currentTicks = ticks; - this.updatedStationInfo = info; - } - - public long getScheduleTime() { - return getRefreshTime() + getTicks(); - } - - public long getEstimatedTime() { - return getCurrentRefreshTime() + getCurrentTicks(); - } - - public long getDifferenceTime() { - return getEstimatedTime() - getScheduleTime(); - } - - public long getEstimatedTimeWithThreshold() { - return getScheduleTime() + ((long)(getDifferenceTime() / ModClientConfig.REALTIME_PRECISION_THRESHOLD.get()) * ModClientConfig.REALTIME_PRECISION_THRESHOLD.get()); - } - - public StationTrainDetails getTrain() { - return train; - } - - public int getIndex() { - return index; - } - - public StationTag getTag() { - return tag; - } - - public boolean isDelayed() { - return getEstimatedTime() - ModClientConfig.DEVIATION_THRESHOLD.get() > getScheduleTime(); - } - - - - public void setDeparted(boolean b) { - this.departed = this.departed || b; - } - - public void setWillMiss(boolean b) { - this.willMiss = b; - } - - public void setTrainCanceled(boolean b, String reason, String trainName) { - if (b) { - getParent().getParent().invalidate(reason, trainName); - } - this.trainCanceled = b; - } - - public boolean isDeparted() { - return this.departed; - } - - public boolean willMissStop() { - return willMiss; - } - - public boolean isTrainCanceled() { - return trainCanceled; - } - - /** - * Returns true if this station is reachable. Returns false if the train at this station already departed. - * @param safeConnection If true this station will no longer be considered as reachable if it may not be reachable due to a delay. But this doesn't mean that the train already departed. - * @return - */ - public boolean reachable(boolean safeConnection) { - return (!safeConnection || !willMissStop()) && !isDeparted() && !isTrainCanceled(); - } - - public boolean shouldRenderRealtime() { - return !isDeparted() && !isTrainCanceled() && relatimeWasUpdated() && (getEstimatedTime() + ModClientConfig.TRANSFER_TIME.get() + ModClientConfig.REALTIME_EARLY_ARRIVAL_THRESHOLD.get() > getScheduleTime()); - } - - public boolean relatimeWasUpdated() { - return wasUpdated; - } - - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putString(NBT_NAME, getStationName()); - nbt.putInt(NBT_TICKS, getTicks()); - getInfo().writeNbt(nbt); - return nbt; - } - - public static StationEntry fromNbt(CompoundTag nbt, long refreshTime) { - String stationName = nbt.getString(NBT_NAME); - int ticks = nbt.getInt(NBT_TICKS); - StationInfo info = StationInfo.fromNbt(nbt); - return new StationEntry(stationName, info, ticks, refreshTime); - } - } - - public record StationTrainDetails(String trainName, UUID trainId, String scheduleTitle) {} - - public enum StationTag { - TRANSIT, - START, - PART_START, - PART_END, - END; - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/data/SimpleTrainSchedule.java b/common/src/main/java/de/mrjulsen/crn/data/SimpleTrainSchedule.java deleted file mode 100644 index 7b2279ef..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/SimpleTrainSchedule.java +++ /dev/null @@ -1,265 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import com.simibubi.create.content.trains.entity.Train; -import com.simibubi.create.content.trains.station.GlobalStation; - -import de.mrjulsen.crn.data.SimulatedTrainSchedule.SimulationData; -import de.mrjulsen.crn.event.listeners.TrainListener; - -public class SimpleTrainSchedule { - private Collection stops; - - public SimpleTrainSchedule(Train train) { - this(GlobalTrainData.getInstance().getAllStopsSorted(train)); - } - - private SimpleTrainSchedule(Collection stations) { - this.stops = stations; - } - - public static SimpleTrainSchedule of(Collection stations) { - return new SimpleTrainSchedule(stations); - } - - public Collection getAllStops() { - return stops; - } - - public Collection getAllStopsFrom(TrainStationAlias alias) { - final boolean[] startFound = new boolean[] { false }; - - return stops.stream().dropWhile(x -> { - if (x.getStationAlias().equals(alias)) { - startFound[0] = true; - } - return !startFound[0]; - }).toList(); - } - - public SimpleTrainSchedule copy() { - return new SimpleTrainSchedule(stops.stream().map(x -> x.copy()).toList()); - } - - public SimpleTrainSchedule makeScheduleUntilNextRepeat() { - List newList = new ArrayList<>(); - for (TrainStop stop : getAllStops()) { - if (newList.contains(stop)) { - break; - } - newList.add(stop); - } - return SimpleTrainSchedule.of(newList); - } - - public SimpleTrainSchedule makeScheduleFrom(TrainStationAlias alias, boolean preventDuplicates) { - List newList = new ArrayList<>(); - int idx = 0; - for (TrainStop stop : getAllStops()) { - if (preventDuplicates && newList.contains(stop)) { - continue; - } - - if (stop.getStationAlias().equals(alias)) { - idx = 0; - } - - newList.add(idx, stop); - idx++; - } - return SimpleTrainSchedule.of(newList); - } - - public SimpleTrainSchedule makeDirectionalScheduleFrom(TrainStationAlias alias) { - List newList = new ArrayList<>(); - for (TrainStop stop : makeScheduleFrom(alias, false).getAllStops()) { - if (newList.contains(stop)) { - break; - } - - newList.add(stop); - } - return SimpleTrainSchedule.of(newList); - } - - public SimpleTrainSchedule makeDirectionalSchedule() { - List newList = new ArrayList<>(); - boolean isRepeating = false; - for (TrainStop stop : getAllStops()) { - if (newList.contains(stop)) { - isRepeating = true; - continue; - } - - if (isRepeating) { - newList.add(0, stop); - } else { - newList.add(stop); - } - } - return SimpleTrainSchedule.of(newList); - } - - public boolean hasStation(GlobalStation station) { - return getAllStops().stream().anyMatch(x -> x.isStation(station)); - } - - public boolean hasStationAlias(TrainStationAlias station) { - return getAllStops().stream().anyMatch(x -> x.isStationAlias(station)); - } - - public Optional getLastStop(TrainStationAlias start) { - Optional lastStop = this.getAllStops().stream().reduce((a, b) -> b); - return lastStop.get().getStationAlias().equals(start) ? getFirstStop() : lastStop; - } - - public Optional getFirstStop() { - return this.getAllStops().stream().findFirst(); - } - - public Optional getNextStop() { - return this.getAllStops().stream().min((a, b) -> a.getPrediction().getTicks()); - } - - public Optional getNextStopOf(TrainStationAlias alias) { - return this.getAllStops().stream().filter(x -> x.getStationAlias().equals(alias)).min(Comparator.comparingInt(x -> x.getPrediction().getTicks())); - } - - public List getAllStopsOf(TrainStationAlias alias) { - return this.getAllStops().stream().filter(x -> x.getStationAlias().equals(alias)).toList(); - } - - public List getAllStopsOf(String station) { - return this.getAllStops().stream().filter(x -> x.getPrediction().getStationName().equals(station)).toList(); - } - - public boolean isInDirection(TrainStationAlias start, TrainStationAlias end) { - for (TrainStop stop : getAllStops()) { - if (stop.getStationAlias().equals(start)) { - return true; - } else if (stop.getStationAlias().equals(end)) { - return false; - } - } - return false; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof SimpleTrainSchedule other) { - Set thisStops = new HashSet<>(getAllStops()); - Set otherStops = new HashSet<>(other.getAllStops()); - - if (thisStops.size() != otherStops.size()) { - return false; - } - - return thisStops.containsAll(otherStops); - } - return false; - } - - public boolean exactEquals(Object obj) { - if (obj instanceof SimpleTrainSchedule other) { - if (getAllStops().size() != other.getAllStops().size()) { - return false; - } - - TrainStop[] a = getAllStops().toArray(TrainStop[]::new); - TrainStop[] b = other.getAllStops().toArray(TrainStop[]::new); - for (int i = 0; i < a.length; i++) { - if (!a[i].equals(b[i])) - return false; - } - return true; - } - return false; - } - - @Override - public int hashCode() { - return 17 * Objects.hash(stops); - } - - @Override - public String toString() { - return Arrays.toString(stops.toArray()); - } - - public static int getTrainCycleDuration(Train train) { - return TrainListener.getInstance().getApproximatedTrainDuration(train); - } - - public SimulatedTrainSchedule simulate(Train train, int simulationTime, TrainStationAlias simulationTarget) { - final int cycleDuration = getTrainCycleDuration(train); - - int timeToTargetAfterSim = getAllStopsOf(simulationTarget).stream().mapToInt(x -> { - int v = (int)((double)(x.getPrediction().getTicks() - simulationTime) % cycleDuration); - if (v < 0) { - v += cycleDuration; - } - return v; - }).min().orElse(0); - int simToTargetTime = simulationTime + timeToTargetAfterSim; - - return new SimulatedTrainSchedule(getAllStops().parallelStream().map(x -> { - int cycle = (int)((double)(x.getPrediction().getTicks() - simToTargetTime) / cycleDuration); - int estimatedTicks = (x.getPrediction().getTicks() - simToTargetTime) % cycleDuration; - while (estimatedTicks < 0) { - estimatedTicks += cycleDuration; - cycle++; - } - cycle += x.getPrediction().getCycle(); - return new TrainStop(x.getStationAlias(), new DeparturePrediction(x.getPrediction().getTrain(), estimatedTicks, x.getPrediction().getScheduleTitle(), x.getPrediction().getStationName(), cycle, x.getPrediction().getInfo())); - }).sorted(Comparator.comparingInt(x -> x.getPrediction().getTicks())).toList(), new SimulationData(train, simulationTime, timeToTargetAfterSim)); - } - - public SimulatedTrainSchedule simulate(Train train, int simulationTime, String simulationTarget) { - final int cycleDuration = getTrainCycleDuration(train); - - int timeToTargetAfterSim = getAllStopsOf(simulationTarget).stream().mapToInt(x -> { - int v = (int)((double)(x.getPrediction().getTicks() - simulationTime) % cycleDuration); - if (v < 0) { - v += cycleDuration; - } - return v; - }).min().orElse(0); - int simToTargetTime = simulationTime + timeToTargetAfterSim; - - return new SimulatedTrainSchedule(getAllStops().parallelStream().map(x -> { - int cycle = (int)((double)(x.getPrediction().getTicks() - simToTargetTime) / cycleDuration); - int estimatedTicks = (x.getPrediction().getTicks() - simToTargetTime) % cycleDuration; - while (estimatedTicks < 0) { - estimatedTicks += cycleDuration; - cycle++; - } - cycle += x.getPrediction().getCycle(); - return new TrainStop(x.getStationAlias(), new DeparturePrediction(x.getPrediction().getTrain(), estimatedTicks, x.getPrediction().getScheduleTitle(), x.getPrediction().getStationName(), cycle, x.getPrediction().getInfo())); - }).sorted(Comparator.comparingInt(x -> x.getPrediction().getTicks())).toList(), new SimulationData(train, simulationTime, timeToTargetAfterSim)); - } - - public SimpleTrainSchedule simulate(Train train, int simulationTime) { - final int cycleDuration = getTrainCycleDuration(train); - - return new SimpleTrainSchedule(getAllStops().parallelStream().map(x -> { - int cycle = (int)((double)(x.getPrediction().getTicks() - simulationTime) / cycleDuration); - int estimatedTicks = (x.getPrediction().getTicks() - simulationTime) % cycleDuration; - while (estimatedTicks < 0) { - estimatedTicks += cycleDuration; - cycle++; - } - cycle += x.getPrediction().getCycle(); - return new TrainStop(x.getStationAlias(), new DeparturePrediction(x.getPrediction().getTrain(), estimatedTicks, x.getPrediction().getScheduleTitle(), x.getPrediction().getStationName(), cycle, x.getPrediction().getInfo())); - }).sorted(Comparator.comparingInt(x -> x.getPrediction().getTicks())).toList()); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/SimulatedTrainSchedule.java b/common/src/main/java/de/mrjulsen/crn/data/SimulatedTrainSchedule.java deleted file mode 100644 index 67b821a2..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/SimulatedTrainSchedule.java +++ /dev/null @@ -1,122 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import com.simibubi.create.content.trains.entity.Train; -import com.simibubi.create.content.trains.station.GlobalStation; - -import de.mrjulsen.crn.event.listeners.TrainListener; - -public class SimulatedTrainSchedule { - private final Collection stationOrder; - private final SimulationData data; - - public SimulatedTrainSchedule(Collection stations, SimulationData data) { - this.stationOrder = makeDiractional(stations); - this.data = data; - } - - private List makeDiractional(Collection raw) { - List newList = new ArrayList<>(); - boolean isRepeating = false; - for (TrainStop stop : raw) { - if (newList.contains(stop)) { - isRepeating = true; - continue; - } - - if (isRepeating) { - newList.add(0, stop); - } else { - newList.add(stop); - } - } - return newList; - } - - public Collection getAllStops() { - return stationOrder; - } - - public Optional getFirstStopOf(TrainStationAlias station) { - return getAllStops().stream().filter(x -> x.getStationAlias().equals(station)).findFirst(); - } - - public boolean hasStation(GlobalStation station) { - return getAllStops().stream().anyMatch(x -> x.getStationAlias().contains(station.name)); - } - - public boolean hasStationAlias(TrainStationAlias station) { - return getAllStops().stream().anyMatch(x -> x.getStationAlias().equals(station)); - } - - public SimulationData getSimulationData() { - return data; - } - - public boolean isInDirection(TrainStationAlias start, TrainStationAlias end) { - for (TrainStop stop : getAllStops()) { - if (stop.getStationAlias().equals(start)) { - return true; - } else if (stop.getStationAlias().equals(end)) { - return false; - } - } - return false; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof SimulatedTrainSchedule other) { - Set thisStops = new HashSet<>(getAllStops()); - Set otherStops = new HashSet<>(other.getAllStops()); - - if (thisStops.size() != otherStops.size()) { - return false; - } - - return thisStops.containsAll(otherStops); - } - return false; - } - - public boolean exactEquals(Object obj) { - if (obj instanceof SimulatedTrainSchedule other) { - if (getAllStops().size() != other.getAllStops().size()) { - return false; - } - - TrainStop[] a = getAllStops().toArray(TrainStop[]::new); - TrainStop[] b = other.getAllStops().toArray(TrainStop[]::new); - for (int i = 0; i < a.length; i++) { - if (!a[i].equals(b[i])) - return false; - } - return true; - } - return false; - } - - @Override - public int hashCode() { - return 17 * Objects.hash(getAllStops()); - } - - @Override - public String toString() { - return Arrays.toString(getAllStops().toArray()); - } - - public static int getTrainCycleDuration(Train train) { - return TrainListener.getInstance().getApproximatedTrainDuration(train); - } - - public static record SimulationData(Train train, int simulationTime, int simulationCorrection) {} -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/StationTag.java b/common/src/main/java/de/mrjulsen/crn/data/StationTag.java new file mode 100644 index 00000000..40afc206 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/StationTag.java @@ -0,0 +1,295 @@ +package de.mrjulsen.crn.data; + +import java.util.Date; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +public class StationTag { + + public static record ClientStationTag(String tagName, String stationName, StationInfo info, UUID tagId) { + public static final String NBT_TAG_NAME = "TagName"; + public static final String NBT_STATION_NAME = "StationName"; + public static final String NBT_STATION_INFO = "StationInfo"; + public static final String NBT_TAG_ID = "Id"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putString(NBT_TAG_NAME, tagName()); + nbt.putString(NBT_STATION_NAME, stationName()); + nbt.put(NBT_STATION_INFO, info().toNbt()); + nbt.putUUID(NBT_TAG_ID, tagId() == null ? new UUID(0, 0) : tagId()); + return nbt; + } + + public static ClientStationTag fromNbt(CompoundTag nbt) { + return new ClientStationTag( + nbt.getString(NBT_TAG_NAME), + nbt.getString(NBT_STATION_NAME), + StationInfo.fromNbt(nbt.getCompound(NBT_STATION_INFO)), + nbt.getUUID(NBT_TAG_ID) + ); + } + } + + public static final int MAX_NAME_LENGTH = 32; + + private static final String LEGACY_NBT_TAG_NAME = "AliasName"; + + private static final String NBT_ID = "Id"; + private static final String NBT_TAG_NAME = "TagName"; + private static final String NBT_STATION_LIST = "Stations"; + private static final String NBT_STATION_MAP = "StationData"; + private static final String NBT_LAST_EDITOR = "LastEditor"; + private static final String NBT_LAST_EDITED_TIME = "LastEditedTimestamp"; + + private static final String NBT_STATION_ENTRY_NAME = "Name"; + + protected UUID id; + protected TagName tagName; + protected Map stations = new HashMap<>(); + + // History + protected String lastEditorName = null; + protected long lastEditedTime = 0; + + protected StationTag(UUID id, TagName tagName, Map initialValues, String lastEditorName, long lastEditedTime) { + this(id, tagName, initialValues); + this.lastEditorName = lastEditorName; + this.lastEditedTime = lastEditedTime; + } + + public StationTag(UUID id, TagName tagName, Map initialValues) { + this(id, tagName); + stations.putAll(initialValues); + } + + public StationTag(UUID id, TagName tagName) { + this.id = id; + this.tagName = tagName; + updateLastEdited("Server"); + } + + /** + * Returns the Id of the tag. May be {@code null}, which means that this tag is temporary or not registered properly. + * @return The id of the tag or null. + */ + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public StationTag copy() { + return new StationTag(null, new TagName(getTagName().get()), new HashMap<>(getAllStations())); + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + if (tagName == null) { + return nbt; + } + + DLUtils.doIfNotNull(id, (i) -> nbt.putUUID(NBT_ID, i)); + nbt.put(NBT_TAG_NAME, getTagName().toNbt()); + if (lastEditorName != null) { + nbt.putString(NBT_LAST_EDITOR, getLastEditorName()); + } + nbt.putLong(NBT_LAST_EDITED_TIME, lastEditedTime); + + ListTag stationsList = new ListTag(); + stations.forEach((key, value) -> { + CompoundTag entry = new CompoundTag(); + entry.putString(NBT_STATION_ENTRY_NAME, key); + value.writeNbt(entry); + stationsList.add(entry); + }); + nbt.put(NBT_STATION_MAP, stationsList); + + return nbt; + } + + public static StationTag fromNbt(CompoundTag nbt, UUID overwriteId) { + UUID id = overwriteId == null ? (nbt.contains(NBT_ID) ? nbt.getUUID(NBT_ID) : null) : overwriteId; + TagName name = TagName.fromNbt(nbt.getCompound(!nbt.contains(NBT_TAG_NAME) ? LEGACY_NBT_TAG_NAME : NBT_TAG_NAME)); + String lastEditorName = nbt.contains(NBT_LAST_EDITOR) ? nbt.getString(NBT_LAST_EDITOR) : null; + long lastEditedTime = nbt.getLong(NBT_LAST_EDITED_TIME); + Map stations; + if (nbt.contains(NBT_STATION_LIST)) { + stations = new HashMap<>(nbt.getList(NBT_STATION_LIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).collect(Collectors.toMap(x -> x, x -> StationInfo.empty()))); + } else if (nbt.contains(NBT_STATION_MAP)) { + stations = new HashMap<>(nbt.getList(NBT_STATION_MAP, Tag.TAG_COMPOUND).stream().map(x -> (CompoundTag)x).collect(Collectors.toMap(x -> { + return x.getString(NBT_STATION_ENTRY_NAME); + }, x -> { + return StationInfo.fromNbt(x); + }))); + } else { + stations = new IdentityHashMap<>(); + } + + return new StationTag(id, name, stations, lastEditorName, lastEditedTime); + } + + public String getLastEditorName() { + return lastEditorName; + } + + public void updateLastEdited(String name) { + this.lastEditorName = name; + this.lastEditedTime = new Date().getTime(); + } + + public Date getLastEditedTime() { + return new Date(lastEditedTime); + } + + public String getLastEditedTimeFormatted() { + return DragonLib.DATE_FORMAT.format(getLastEditedTime()); + } + + + public TagName getTagName() { + return this.tagName; + } + + public void updateInfoForStation(String station, StationInfo info) { + if (stations.containsKey(station)) { + stations.replace(station, info); + } + } + + public void add(String station, StationInfo info) { + if (!stations.containsKey(station)) { + stations.put(station, info); + } + } + + public void addAll(Map stations) { + stations.forEach((key, value) -> { + if (!this.stations.containsKey(key)) { + this.stations.put(key, value); + } + }); + } + + /** + * @param stationName The name of the train station. + * @return {@code true} if the station is part of this tag. + */ + public boolean contains(String stationName) { + String regex = stationName.isBlank() ? stationName : "\\Q" + stationName.replace("*", "\\E.*\\Q"); + return stations.keySet().stream().anyMatch(x -> x.matches(regex)); + } + + public Set getAllStationNames() { + return Set.copyOf(stations.keySet()); + } + + public Map getAllStations() { + return Map.copyOf(stations); + } + + public StationInfo getInfoForStation(String stationName) { + return stations.containsKey(stationName) ? stations.get(stationName) : StationInfo.empty(); + } + + public void setName(TagName name) { + this.tagName = name; + } + + public void remove(String station) { + stations.remove(station); + } + + public ClientStationTag getClientTag(String station) { + return new ClientStationTag(getTagName().get(), station, getInfoForStation(station), getId()); + } + + @Override + public String toString() { + return getTagName().toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StationTag alias) { + return getTagName().equals(alias.getTagName()) && getAllStationNames().size() == alias.getAllStationNames().size() && getAllStationNames().stream().allMatch(x -> alias.contains(x)); + } + return false; + } + + @Override + public int hashCode() { + return 7 * Objects.hash(tagName); + } + + public void applyFrom(StationTag newData) { + this.tagName = newData.tagName; + this.stations.clear(); + this.stations.putAll(newData.stations); + this.lastEditedTime = newData.lastEditedTime; + this.lastEditorName = newData.lastEditorName; + } + + /** + * Information about one specific train station (not station tag!) + */ + public static record StationInfo(String platform) { + + public static final int MAX_PLATFORM_NAME_LENGTH = 8; + + private static final String NBT_PLATFORM = "Platform"; + + public static StationInfo empty() { + return new StationInfo(""); + } + + public boolean isPlatformKnown() { + return platform() != null && !platform().isBlank(); + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putString(NBT_PLATFORM, platform()); + return nbt; + } + + public void writeNbt(CompoundTag nbt) { + nbt.putString(NBT_PLATFORM, platform()); + } + + public static StationInfo fromNbt(CompoundTag nbt) { + return new StationInfo( + nbt.getString(NBT_PLATFORM) + ); + } + + @Override + public final boolean equals(Object obj) { + if (obj instanceof StationInfo other) { + return + platform().equals(other.platform()) + ; + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(platform()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/AliasName.java b/common/src/main/java/de/mrjulsen/crn/data/TagName.java similarity index 61% rename from common/src/main/java/de/mrjulsen/crn/data/AliasName.java rename to common/src/main/java/de/mrjulsen/crn/data/TagName.java index a3538a33..13ddcda4 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/AliasName.java +++ b/common/src/main/java/de/mrjulsen/crn/data/TagName.java @@ -2,12 +2,13 @@ import net.minecraft.nbt.CompoundTag; -public class AliasName { +public class TagName { + public static final TagName EMPTY = TagName.of(""); private static final String NBT_NAME = "Name"; - private String name; + private String name = ""; - public AliasName(String name) { + public TagName(String name) { this.name = name; } @@ -17,8 +18,8 @@ public CompoundTag toNbt() { return nbt; } - public static AliasName fromNbt(CompoundTag nbt) { - return new AliasName(nbt.getString(NBT_NAME)); + public static TagName fromNbt(CompoundTag nbt) { + return new TagName(nbt.getString(NBT_NAME)); } public String get() { @@ -29,8 +30,12 @@ public String set(String name) { return this.name = name; } - public static AliasName of(String name) { - return new AliasName(name); + public static TagName of(String name) { + return new TagName(name); + } + + public boolean isEmpty() { + return name == null || name.isBlank(); } @Override @@ -40,10 +45,9 @@ public String toString() { @Override public boolean equals(Object obj) { - if (obj instanceof AliasName aliasName) { + if (obj instanceof TagName aliasName) { return name.equals(aliasName.get()); } - return false; } diff --git a/common/src/main/java/de/mrjulsen/crn/data/SimpleTrainConnection.java b/common/src/main/java/de/mrjulsen/crn/data/TrainConnection.java similarity index 77% rename from common/src/main/java/de/mrjulsen/crn/data/SimpleTrainConnection.java rename to common/src/main/java/de/mrjulsen/crn/data/TrainConnection.java index 16fb15fb..66a41afd 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/SimpleTrainConnection.java +++ b/common/src/main/java/de/mrjulsen/crn/data/TrainConnection.java @@ -2,11 +2,11 @@ import java.util.UUID; -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; +import de.mrjulsen.crn.data.StationTag.StationInfo; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; -public record SimpleTrainConnection(String trainName, UUID trainId, ResourceLocation trainIconId, int ticks, String scheduleTitle, StationInfo stationDetails) { +public record TrainConnection(String trainName, UUID trainId, ResourceLocation trainIconId, int ticks, String scheduleTitle, StationInfo stationDetails) { private static final String NBT_TRAIN_NAME = "TrainName"; private static final String NBT_TRAIN_ID = "Id"; @@ -27,8 +27,8 @@ public CompoundTag toNbt() { return nbt; } - public static SimpleTrainConnection fromNbt(CompoundTag nbt) { - return new SimpleTrainConnection( + public static TrainConnection fromNbt(CompoundTag nbt) { + return new TrainConnection( nbt.getString(NBT_TRAIN_NAME), nbt.getUUID(NBT_TRAIN_ID), new ResourceLocation(nbt.getString(NBT_TRAIN_ICON)), diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainData.java b/common/src/main/java/de/mrjulsen/crn/data/TrainData.java deleted file mode 100644 index 18b5c5d3..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/TrainData.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.UUID; - -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.mcdragonlib.data.INBTSerializable; -import net.minecraft.nbt.CompoundTag; - -public class TrainData implements INBTSerializable { - - private static final String NBT_NAME = "Name"; - private static final String NBT_UUID = "UUID"; - - - private String name; - private UUID id; - - public TrainData() { - - } - - public TrainData(Train train) { - this.name = train.name.getString(); - this.id = train.id; - } - - public String getName() { - return name; - } - - public UUID getId() { - return id; - } - - @Override - public CompoundTag serializeNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putString(NBT_NAME, name); - nbt.putUUID(NBT_UUID, id); - return nbt; - } - - @Override - public void deserializeNbt(CompoundTag nbt) { - name = nbt.getString(NBT_NAME); - id = nbt.getUUID(NBT_UUID); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainExitSide.java b/common/src/main/java/de/mrjulsen/crn/data/TrainExitSide.java new file mode 100644 index 00000000..7f167b5a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/TrainExitSide.java @@ -0,0 +1,27 @@ +package de.mrjulsen.crn.data; + +import java.util.Arrays; + +public enum TrainExitSide { + UNKNOWN((byte)0), + RIGHT((byte)1), + LEFT((byte)-1); + + private byte side; + + TrainExitSide(byte side) { + this.side = side; + } + + public byte getAsByte() { + return side; + } + + public static TrainExitSide getFromByte(byte side) { + return Arrays.stream(values()).filter(x -> x.getAsByte() == side).findFirst().orElse(UNKNOWN); + } + + public TrainExitSide getOpposite() { + return getFromByte((byte)-getAsByte()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainGroup.java b/common/src/main/java/de/mrjulsen/crn/data/TrainGroup.java index dad5dc09..9a78398b 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/TrainGroup.java +++ b/common/src/main/java/de/mrjulsen/crn/data/TrainGroup.java @@ -1,98 +1,40 @@ package de.mrjulsen.crn.data; import java.util.Date; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import com.simibubi.create.content.trains.entity.Train; - import de.mrjulsen.mcdragonlib.DragonLib; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.StringTag; -import net.minecraft.nbt.Tag; public class TrainGroup { + public static int MAX_NAME_LENGTH = 32; + private static final String NBT_NAME = "Name"; - private static final String NBT_TRAIN_NAMES = "Trains"; + private static final String NBT_COLOR = "Color"; private static final String NBT_LAST_EDITOR = "LastEditor"; private static final String NBT_LAST_EDITED_TIME = "LastEditedTimestamp"; - private String name; - private Set trainNames = new HashSet<>(); + private final String name; + private int color; - protected String lastEditorName = null; - protected long lastEditedTime = 0; + protected String lastEditorName; + protected long lastEditedTime; public TrainGroup(String name) { this.name = name; - } - - public TrainGroup(String name, Set initialValues) { - this.name = name; - this.trainNames = initialValues; - } - - public void addTrain(Train train) { - addTrain(train.name.getString()); - } - - public void addTrain(String trainName) { - trainNames.add(trainName); - } - - public Set getTrainNames() { - return trainNames; - } - - public boolean contains(Train train) { - return trainNames.stream().anyMatch(x -> x.equals(train.name.getString())); - } - - public boolean contains(String trainName) { - return trainNames.stream().anyMatch(x -> x.equals(trainName)); - } - - public void setGroupName(String name) { - this.name = name; + updateLastEdited("Server"); } public String getGroupName() { return name; } - public void update(TrainGroup newData) { - this.name = newData.name; - this.trainNames = newData.trainNames; - this.lastEditedTime = newData.lastEditedTime; - this.lastEditorName = newData.lastEditorName; + public int getColor() { + return color; } - public void add(String trainName) { - trainNames.add(trainName); - } - - public void addAll(Set trainNames) { - this.trainNames.addAll(trainNames); - } - - public void add(Train train) { - add(train.name.getString()); - } - - public void addAllTrains(Set trains) { - this.trainNames.addAll(trains.stream().map(x -> x.name.getString()).toList()); - } - - public void remove(Train train) { - remove(train.name.getString()); - } - - public void remove(String trainName) { - trainNames.removeIf(x -> x.equals(trainName)); + public void setColor(int color) { + this.color = color; } public String getLastEditorName() { @@ -114,7 +56,15 @@ public String getLastEditedTimeFormatted() { @Override public int hashCode() { - return Objects.hash(name, trainNames); + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TrainGroup o) { + return name.equals(o.name); + } + return false; } public CompoundTag toNbt() { @@ -123,24 +73,25 @@ public CompoundTag toNbt() { if (lastEditorName != null) { nbt.putString(NBT_LAST_EDITOR, getLastEditorName()); } - nbt.putLong(NBT_LAST_EDITED_TIME, lastEditedTime); + if (lastEditedTime > 0) { + nbt.putLong(NBT_LAST_EDITED_TIME, lastEditedTime); + } nbt.putString(NBT_NAME, getGroupName()); - ListTag tag = new ListTag(); - tag.addAll(getTrainNames().stream().map(x -> StringTag.valueOf(x)).toList()); - nbt.put(NBT_TRAIN_NAMES, tag); + nbt.putInt(NBT_COLOR, getColor()); return nbt; } public static TrainGroup fromNbt(CompoundTag nbt) { String groupName = nbt.getString(NBT_NAME); - Set trainNames = new HashSet<>(nbt.getList(NBT_TRAIN_NAMES, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).collect(Collectors.toSet())); - String lastEditorName = nbt.contains(NBT_LAST_EDITOR) ? nbt.getString(NBT_LAST_EDITOR) : null; - long lastEditedTime = nbt.getLong(NBT_LAST_EDITED_TIME); - TrainGroup group = new TrainGroup(groupName, trainNames); - group.lastEditedTime = lastEditedTime; - group.lastEditorName = lastEditorName; - + TrainGroup group = new TrainGroup(groupName); + group.setColor(nbt.getInt(NBT_COLOR)); + if (nbt.contains(NBT_LAST_EDITOR)) { + group.lastEditorName = nbt.getString(NBT_LAST_EDITOR); + } + if (nbt.contains(NBT_LAST_EDITED_TIME)) { + group.lastEditedTime = nbt.getLong(NBT_LAST_EDITED_TIME); + } return group; } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java b/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java new file mode 100644 index 00000000..2e07e66b --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java @@ -0,0 +1,28 @@ +package de.mrjulsen.crn.data; + +import net.minecraft.nbt.CompoundTag; + +public record TrainInfo(TrainLine line, TrainGroup group) { + + private static final String NBT_TRAIN_GROUP = "Group"; + private static final String NBT_TRAIN_LINE = "Line"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + if (group != null) nbt.put(NBT_TRAIN_GROUP, group.toNbt()); + if (line != null) nbt.put(NBT_TRAIN_LINE, line.toNbt()); + + return nbt; + } + + public static TrainInfo fromNbt(CompoundTag nbt) { + return new TrainInfo( + nbt.contains(NBT_TRAIN_LINE) ? TrainLine.fromNbt(nbt.getCompound(NBT_TRAIN_LINE)) : null, + nbt.contains(NBT_TRAIN_GROUP) ? TrainGroup.fromNbt(nbt.getCompound(NBT_TRAIN_GROUP)) : null + ); + } + + public static TrainInfo empty() { + return new TrainInfo(null, null); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainLine.java b/common/src/main/java/de/mrjulsen/crn/data/TrainLine.java new file mode 100644 index 00000000..1e101c95 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/TrainLine.java @@ -0,0 +1,47 @@ +package de.mrjulsen.crn.data; + +import net.minecraft.nbt.CompoundTag; + +public class TrainLine { + + public static int MAX_NAME_LENGTH = 32; + + private static final String NBT_NAME = "Name"; + private static final String NBT_COLOR = "Color"; + + private final String name; + private int lineColor = 0; + protected String lastEditorName; + protected long lastEditedTime; + + public TrainLine(String name) { + this.name = name; + } + + public String getLineName() { + return name; + } + + public int getColor() { + return lineColor; + } + + public void setColor(int color) { + this.lineColor = color; + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putString(NBT_NAME, name); + nbt.putInt(NBT_COLOR, lineColor); + return nbt; + } + + public static TrainLine fromNbt(CompoundTag nbt) { + TrainLine line = new TrainLine( + nbt.getString(NBT_NAME) + ); + line.setColor(nbt.getInt(NBT_COLOR)); + return line; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainStationAlias.java b/common/src/main/java/de/mrjulsen/crn/data/TrainStationAlias.java deleted file mode 100644 index e6c1e424..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/TrainStationAlias.java +++ /dev/null @@ -1,217 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.Date; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import de.mrjulsen.mcdragonlib.DragonLib; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.StringTag; -import net.minecraft.nbt.Tag; - -public class TrainStationAlias { - - private static final String NBT_ALIAS_NAME = "AliasName"; - private static final String NBT_STATION_LIST = "Stations"; - private static final String NBT_STATION_MAP = "StationData"; - private static final String NBT_LAST_EDITOR = "LastEditor"; - private static final String NBT_LAST_EDITED_TIME = "LastEditedTimestamp"; - - private static final String NBT_STATION_ENTRY_NAME = "Name"; - - protected AliasName aliasName; - protected Map stations = new HashMap<>(); - // log - protected String lastEditorName = null; - protected long lastEditedTime = 0; - - protected TrainStationAlias(AliasName aliasName, Map initialValues, String lastEditorName, long lastEditedTime) { - this(aliasName, initialValues); - this.lastEditorName = lastEditorName; - this.lastEditedTime = lastEditedTime; - } - - public TrainStationAlias(AliasName aliasName, Map initialValues) { - this(aliasName); - stations.putAll(initialValues); - } - - public TrainStationAlias(AliasName aliasName) { - this.aliasName = aliasName; - } - - public TrainStationAlias copy() { - return new TrainStationAlias(new AliasName(getAliasName().get()), new HashMap<>(getAllStations())); - } - - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - if (aliasName == null) { - return nbt; - } - - nbt.put(NBT_ALIAS_NAME, getAliasName().toNbt()); - if (lastEditorName != null) { - nbt.putString(NBT_LAST_EDITOR, getLastEditorName()); - } - nbt.putLong(NBT_LAST_EDITED_TIME, lastEditedTime); - - ListTag stationsList = new ListTag(); - stations.forEach((key, value) -> { - CompoundTag entry = new CompoundTag(); - entry.putString(NBT_STATION_ENTRY_NAME, key); - value.writeNbt(entry); - stationsList.add(entry); - }); - nbt.put(NBT_STATION_MAP, stationsList); - - return nbt; - } - - public static TrainStationAlias fromNbt(CompoundTag nbt) { - if (!nbt.contains(NBT_ALIAS_NAME)) { - return new TrainStationAlias(AliasName.of("null")); - } - AliasName name = AliasName.fromNbt(nbt.getCompound(NBT_ALIAS_NAME)); - String lastEditorName = nbt.contains(NBT_LAST_EDITOR) ? nbt.getString(NBT_LAST_EDITOR) : null; - long lastEditedTime = nbt.getLong(NBT_LAST_EDITED_TIME); - Map stations; - if (nbt.contains(NBT_STATION_LIST)) { - stations = nbt.getList(NBT_STATION_LIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).collect(Collectors.toMap(x -> x, x -> StationInfo.empty())); - } else if (nbt.contains(NBT_STATION_MAP)) { - stations = nbt.getList(NBT_STATION_MAP, Tag.TAG_COMPOUND).stream().map(x -> (CompoundTag)x).collect(Collectors.toMap(x -> { - return x.getString(NBT_STATION_ENTRY_NAME); - }, x -> { - return StationInfo.fromNbt(x); - })); - } else { - stations = new IdentityHashMap<>(); - } - - return new TrainStationAlias(name, stations, lastEditorName, lastEditedTime); - } - - public String getLastEditorName() { - return lastEditorName; - } - - public void updateLastEdited(String name) { - this.lastEditorName = name; - this.lastEditedTime = new Date().getTime(); - } - - public Date getLastEditedTime() { - return new Date(lastEditedTime); - } - - public String getLastEditedTimeFormatted() { - return DragonLib.DATE_FORMAT.format(getLastEditedTime()); - } - - - public AliasName getAliasName() { - return this.aliasName; - } - - public void updateInfoForStation(String station, StationInfo info) { - if (stations.containsKey(station)) { - stations.replace(station, info); - } - } - - public void add(String station, StationInfo info) { - if (!stations.containsKey(station)) { - stations.put(station, info); - } - } - - public void addAll(Map stations) { - stations.forEach((key, value) -> { - if (!this.stations.containsKey(key)) { - this.stations.put(key, value); - } - }); - } - - public boolean contains(String station) { - String regex = station.isBlank() ? station : "\\Q" + station.replace("*", "\\E.*\\Q"); - return stations.keySet().stream().anyMatch(x -> x.matches(regex)); - } - - public Set getAllStationNames() { - return stations.keySet(); - } - - public Map getAllStations() { - return stations; - } - - public StationInfo getInfoForStation(String stationName) { - return stations.containsKey(stationName) ? stations.get(stationName) : StationInfo.empty(); - } - - public void setName(AliasName name) { - this.aliasName = name; - } - - public void remove(String station) { - stations.remove(station); - } - - @Override - public String toString() { - return String.format("%s", getAliasName()); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof TrainStationAlias alias) { - return getAliasName().equals(alias.getAliasName()) && getAllStationNames().size() == alias.getAllStationNames().size() && getAllStationNames().stream().allMatch(x -> alias.contains(x)); - } - return false; - } - - @Override - public int hashCode() { - return 7 * Objects.hash(aliasName); - } - - public void update(TrainStationAlias newData) { - this.aliasName = newData.aliasName; - this.stations = newData.stations; - this.lastEditedTime = newData.lastEditedTime; - this.lastEditorName = newData.lastEditorName; - } - - public static record StationInfo(String platform) { - private static final String NBT_PLATFORM = "Platform"; - - public static StationInfo empty() { - return new StationInfo(""); - } - - public void writeNbt(CompoundTag nbt) { - nbt.putString(NBT_PLATFORM, platform()); - } - - public static StationInfo fromNbt(CompoundTag nbt) { - return new StationInfo( - nbt.getString(NBT_PLATFORM) - ); - } - - @Override - public final boolean equals(Object obj) { - if (obj instanceof StationInfo other) { - return platform().equals(other.platform()); - } - return false; - } - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainStop.java b/common/src/main/java/de/mrjulsen/crn/data/TrainStop.java deleted file mode 100644 index a0b11bfb..00000000 --- a/common/src/main/java/de/mrjulsen/crn/data/TrainStop.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.mrjulsen.crn.data; - -import java.util.Objects; - -import com.simibubi.create.content.trains.station.GlobalStation; - -public class TrainStop { - - private TrainStationAlias station; - private DeparturePrediction prediction; - - public TrainStop(TrainStationAlias station, DeparturePrediction prediction) { - this.station = station; - this.prediction = prediction; - } - - public TrainStop copy() { - return new TrainStop(getStationAlias().copy(), getPrediction().copy()); - } - - public TrainStationAlias getStationAlias() { - return station; - } - - public DeparturePrediction getPrediction() { - return prediction; - } - - public boolean isStation(GlobalStation station) { - return getStationAlias().contains(station.name); - } - - public boolean isStationAlias(TrainStationAlias alias) { - return getStationAlias().equals(alias); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof TrainStop other) { - return getStationAlias().equals(other.getStationAlias()); - } - return false; - } - - public boolean identical(TrainStop other) { - return getStationAlias().equals(other.getStationAlias()) && prediction.getTicks() == other.getPrediction().getTicks(); - } - - @Override - public int hashCode() { - return 13 * Objects.hash(station); - } - - @Override - public String toString() { - return String.format("%s (%s)", getStationAlias(), prediction.getTicks()); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java b/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java index 15433dbe..e4a8a550 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java +++ b/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java @@ -1,57 +1,232 @@ package de.mrjulsen.crn.data; -import java.util.List; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import dev.architectury.platform.Platform; +import net.fabricmc.api.EnvType; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; +import net.minecraft.world.level.storage.LevelResource; public class UserSettings { + + private static final String FILENAME = CreateRailwaysNavigator.SHORT_MOD_ID + "_usersettings_"; + private static final int VERSION = 1; + + private static final String NBT_VERSION = "Version"; + private static final String NBT_DEPARTURE_IN = "DepartureIn"; private static final String NBT_TRANSFER_TIME = "TransferTime"; - private static final String NBT_TRAIN_GROUPS = "TrainGroupBlacklist"; + private static final String NBT_TRAIN_GROUPS = "ExcludedTrainGroups"; + private static final String NBT_SAVED_ROUTES = "SavedRoutes"; + private static final String NBT_SEARCH_DEPARTURE_TIME = "SearchDepartureIn"; + private static final String NBT_SEARCH_TRAIN_GROUPS = "SearchExcludedTrainGroups"; + + private static final Map settingsInstances = new LinkedHashMap<>(); + + private final Collection> allSettings = new ArrayList<>(); + private final UUID owner; + private final boolean readOnly; + + // Settings + public final UserSetting navigationDepartureInTicks = registerSetting(new UserSetting<>(() -> 0, NBT_DEPARTURE_IN, (nbt, val, name) -> nbt.putInt(name, val), (nbt, name) -> nbt.getInt(name), (val) -> TimeUtils.parseDurationShort(val))); + public final UserSetting navigationTransferTime = registerSetting(new UserSetting<>(() -> 1000, NBT_TRANSFER_TIME, (nbt, val, name) -> nbt.putInt(name, val), (nbt, name) -> nbt.getInt(name), (val) -> TimeUtils.parseDurationShort(val))); + public final UserSetting> navigationExcludedTrainGroups = registerSetting(new UserSetting<>(() -> new HashSet<>(), NBT_TRAIN_GROUPS, + (nbt, val, name) -> { + ListTag list = new ListTag(); + list.addAll(val.stream().map(x -> StringTag.valueOf(x)).toList()); + nbt.put(name, list); + }, (nbt, name) -> { + return nbt.getList(name, Tag.TAG_STRING).stream().filter(x -> GlobalSettings.hasInstance() ? GlobalSettings.getInstance().trainGroupExists(x.getAsString()) : true).map(x -> x.getAsString()).collect(Collectors.toSet()); + },(val) -> val.isEmpty() ? TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups.all").getString() : TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups.excluded", val.size()).getString())); + + public final UserSetting> savedRoutes = registerSetting(new UserSetting<>(() -> new HashSet<>(), NBT_SAVED_ROUTES, (nbt, val, name) -> { + ListTag list = new ListTag(); + list.addAll(val); + nbt.put(name, list); + }, (nbt, name) -> { + return nbt.getList(name, Tag.TAG_COMPOUND).stream().map(x -> (CompoundTag)x).collect(Collectors.toSet()); + },(val) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.saved", val.size()).getString())); + + public final UserSetting searchDepartureInTicks = registerSetting(new UserSetting<>(() -> 0, NBT_SEARCH_DEPARTURE_TIME, (nbt, val, name) -> nbt.putInt(name, val), (nbt, name) -> nbt.getInt(name), (val) -> TimeUtils.parseDurationShort(val))); + public final UserSetting> searchExcludedTrainGroups = registerSetting(new UserSetting<>(() -> new HashSet<>(), NBT_SEARCH_TRAIN_GROUPS, + (nbt, val, name) -> { + ListTag list = new ListTag(); + list.addAll(val.stream().map(x -> StringTag.valueOf(x)).toList()); + nbt.put(name, list); + }, (nbt, name) -> { + return nbt.getList(name, Tag.TAG_STRING).stream().filter(x -> GlobalSettings.hasInstance() ? GlobalSettings.getInstance().trainGroupExists(x.getAsString()) : true).map(x -> x.getAsString()).collect(Collectors.toSet()); + },(val) -> val.isEmpty() ? TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups.all").getString() : TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".search_options.train_groups.excluded", val.size()).getString())); + + public UserSettings(UUID playerId, boolean readOnly) { + this.owner = playerId; + this.readOnly = readOnly; + } + + public UUID getOwnerId() { + return owner; + } + + public boolean isReadOnly() { + return readOnly; + } + + private void checkReadOnly() { + if (isReadOnly()) { + throw new IllegalAccessError("This instance of the user settings is read-only!"); + } + } - private final int transferTime; - private final List trainGroupBlacklist; + private static void update(UserSettings settings) { + if (settingsInstances.containsKey(settings.getOwnerId())) { + settingsInstances.get(settings.getOwnerId()).checkReadOnly(); + } + settingsInstances.put(settings.getOwnerId(), settings); + } - public UserSettings() { - this(ModClientConfig.TRANSFER_TIME.get(), ModClientConfig.TRAIN_GROUP_FILTER_BLACKLIST.get()); + protected > T registerSetting(T setting) { + allSettings.add(setting); + return setting; } - private UserSettings(int transferTime, List trainGroupBlacklist) { - this.transferTime = transferTime; - this.trainGroupBlacklist = trainGroupBlacklist; + + public static UserSettings getSettingsFor(UUID playerId, boolean readOnly) { + return settingsInstances.computeIfAbsent(playerId, x -> UserSettings.load(x, readOnly)); } - public CompoundTag toNbt() { + /** Client-side only! */ + public final void clientSave(Runnable andThen) throws RuntimeSideException { + if (Platform.getEnv() == EnvType.SERVER) { + throw new RuntimeSideException(true); + } + checkReadOnly(); + DataAccessor.getFromServer(this, ModAccessorTypes.SAVE_USER_SETTINGS, $ -> DLUtils.doIfNotNull(andThen, x -> x.run())); + } + + /** Server-side only! */ + public final synchronized void save() throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + checkReadOnly(); + UserSettings.update(this); + CompoundTag nbt = this.toNbt(); + try { + NbtIo.writeCompressed(nbt, new File(ModCommonEvents.getCurrentServer().get().getWorldPath(new LevelResource("data/" + FILENAME + getOwnerId() + ".nbt")).toString())); + CreateRailwaysNavigator.LOGGER.info("Saved user settings."); + } catch (IOException e) { + CreateRailwaysNavigator.LOGGER.error("Unable to save user settings.", e); + } + } + + /** Server-side only! */ + public static UserSettings load(UUID playerId, boolean readOnly) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + + File settingsFile = new File(ModCommonEvents.getCurrentServer().get().getWorldPath(new LevelResource("data/" + FILENAME + playerId + ".nbt")).toString()); + + if (settingsFile.exists()) { + try { + return UserSettings.fromNbt(NbtIo.readCompressed(settingsFile), playerId, readOnly); + } catch (IOException e) { + CreateRailwaysNavigator.LOGGER.error("Cannot load user settings for player: " + playerId, e); + } + } + return new UserSettings(playerId, readOnly); + } + + public final CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); - nbt.putInt(NBT_TRANSFER_TIME, getTransferTime()); - ListTag list = new ListTag(); - list.addAll(getTrainGroupBlacklist().stream().map(x -> StringTag.valueOf(x)).toList()); - nbt.put(NBT_TRAIN_GROUPS, list); + allSettings.forEach(x -> x.serialize(nbt)); + nbt.putInt(NBT_VERSION, VERSION); return nbt; } - public static UserSettings fromNbt(CompoundTag nbt) { - return new UserSettings( - nbt.getInt(NBT_TRANSFER_TIME), - nbt.getList(NBT_TRAIN_GROUPS, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList() - ); + public final static UserSettings fromNbt(CompoundTag nbt, UUID playerId, boolean readOnly) { + UserSettings settings = new UserSettings(playerId, readOnly); + @SuppressWarnings("unused") final int version = nbt.getInt(NBT_VERSION); + settings.allSettings.forEach(x -> x.deserialize(nbt)); + return settings; } - public int getTransferTime() { - return transferTime; - } + public static class UserSetting { + private T value; + private final String serializationName; + private final Supplier defaultValue; + private final ISerializationContext serializer; + private final BiFunction deserializer; + private final Function stringRepresentation; + + public UserSetting(Supplier defaultValue, String serializationName, ISerializationContext serializer, BiFunction deserializer, Function stringRepresentation) { + this.defaultValue = defaultValue; + this.serializer = serializer; + this.serializationName = serializationName; + this.deserializer = deserializer; + this.stringRepresentation = stringRepresentation; + this.value = defaultValue.get(); + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + public void setToDefault() { + this.value = getDefault(); + } + + public T getDefault() { + return defaultValue.get(); + } + + private void serialize(CompoundTag nbt) { + serializer.execute(nbt, value, getSerializationName()); + } + + private T deserialize(CompoundTag nbt) { + return value = deserializer.apply(nbt, getSerializationName()); + } + + private String getSerializationName() { + return serializationName; + } - public List getTrainGroupBlacklist() { - return trainGroupBlacklist; + @Override + public String toString() { + return stringRepresentation.apply(value); + } } - public boolean isTrainExcluded(Train train, GlobalSettings settingsInstance) { - boolean b = settingsInstance.getTrainGroupsList().stream().filter(x -> getTrainGroupBlacklist().contains(x.getGroupName())).anyMatch(x -> x.contains(train)); - return b; + @FunctionalInterface + private static interface ISerializationContext { + void execute(CompoundTag nbt, T value, String serializationName); } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java new file mode 100644 index 00000000..4feba82a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java @@ -0,0 +1,673 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; + +import java.lang.StringBuilder; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.SavedRoutesManager; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.data.train.RoutePartProgressState; +import de.mrjulsen.crn.data.train.RouteProgressState; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.events.DefaultTrainDataRefreshEvent; +import de.mrjulsen.crn.util.IListenable; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; + +public class ClientRoute extends Route implements AutoCloseable, IListenable { + + public static record ListenerNotificationData(ClientRoute route, ClientRoutePart part, ClientTrainStop trainStop, TransferConnection connection) {} + public static record QueuedAnnouncementEvent(Runnable callback, ClientRoutePart part, ClientTrainStop trainStop) {} + + /** Called every time the real-time data updates. */ + public static final String EVENT_UPDATE = "update"; + + /** Called when announcing the start of the journey. */ + public static final String EVENT_ANNOUNCE_START = "announce_start"; + /** Called when arriving at the first station of the journey. */ + public static final String EVENT_ARRIVAL_AT_START = "arrival_at_start"; + /** Called when departing from the first station of the journey. */ + public static final String EVENT_DEPARTURE_FROM_START = "departure_from_start"; + + /** Called while travelling between two stations. */ + public static final String EVENT_WHILE_TRANSIT = "while_transit"; + + /** Called when announcing a stopover station with no special role on the route. */ + public static final String EVENT_ANNOUNCE_STOPOVER = "announce_stopover"; + /** Called when arriving at a stopover station with no special role on the route. */ + public static final String EVENT_ARRIVAL_AT_STOPOVER = "arrival_at_stopover"; + /** Called when departing from a stopover station with no special role on the route. */ + public static final String EVENT_DEPARTURE_AT_STOPOVER = "departure_from_stopover"; + + /** Called when announcing the arrival at a transfer station. */ + public static final String EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION = "announce_transfer_arrival_station"; + /** Called when arraving at a transfer station. */ + public static final String EVENT_ARRIVAL_AT_TRANSFER_ARRIVAL_STATION = "arrival_at_transfer_arrival_station"; + /** Called when the train that brought you to the transfer station departs. */ + public static final String EVENT_DEPARTURE_FROM_TRANSFER_ARRIVAL_STATION = "departure_from_transfer_arrival_station"; + /** Called while waiting for the connecting train. */ + public static final String EVENT_WHILE_TRANSFER = "while_transfer"; + /** Called when the connecting train announces arrival at a transfer station. */ + public static final String EVENT_ANNOUNCE_TRANSFER_DEPARTURE_STATION = "announce_transfer_departure_station"; + /** Called when the connecting train arrives at a transfer station. */ + public static final String EVENT_ARRIVAL_AT_TRANSFER_DEPARTURE_STATION = "arrival_at_transfer_departure_station"; + /** Called when the connecting train departs at a transfer station. */ + public static final String EVENT_DEPARTURE_FROM_TRANSFER_DEPARTURE_STATION = "departure_from_transfer_departure_station"; + + /** Called when announcing the end of the journey. */ + public static final String EVENT_ANNOUNCE_LAST_STOP = "announce_last_stop"; + /** Called when arriving at the last station of the journey. */ + public static final String EVENT_ARRIVAL_AT_LAST_STOP = "arrival_at_last_stop"; + /** Called when departing from the last station of the journey. */ + public static final String EVENT_DEPARTURE_FROM_LAST_STOP = "departure_from_last_stop"; + + /** Called when the first stop changes. */ + public static final String EVENT_FIRST_STOP_STATION_CHANGED = "first_stop_station_changed"; + /** Called when the first stop is delayed. */ + public static final String EVENT_FIRST_STOP_DELAYED = "first_stop_delayed"; + + /** Called when the transfer arrival station changes. */ + public static final String EVENT_TRANSFER_ARRIVAL_STATION_CHANGED = "transfer_arrival_station_changed"; + /** Called when the transfer arrival station is delayed. */ + public static final String EVENT_TRANSFER_ARRIVAL_DELAYED = "transfer_arrival_delayed"; + /** Called when the transfer departure station changes. */ + public static final String EVENT_TRANSFER_DEPARTURE_STATION_CHANGED = "transfer_departure_station_changed"; + /** Called when the transfer departure station is delayed. */ + public static final String EVENT_TRANSFER_DEPARTURE_DELAYED = "transfer_departure_delayed"; + + /** Called when the last stop changes. */ + public static final String EVENT_LAST_STOP_STATION_CHANGED = "last_stop_station_changed"; + /** Called when the last stop is delayed. */ + public static final String EVENT_LAST_STOP_DELAYED = "last_stop_delayed"; + + /** Called when any stop is announced. */ + public static final String EVENT_ANY_STOP_ANNOUNCED = "any_stop_announced"; + /** Called when arriving at any stop. */ + public static final String EVENT_ARRIVAL_AT_ANY_STOP = "arrival_at_any_stop"; + /** Called when departing from any stop. */ + public static final String EVENT_DEPARTURE_FROM_ANY_STOP = "departure_from_any_stop"; + /** Called when announcing any important station (start, transfers and end) */ + public static final String EVENT_ANNOUNCE_ANY_IMPORTANT_STATION = "announce_any_important_station"; + /** Called when arriving at any important station (start, transfers and end) */ + public static final String EVENT_ARRIVAL_AT_ANY_IMPORTANT_STATION = "arrival_at_any_important_station"; + /** Called when departing from any important station (start, transfers and end) */ + public static final String EVENT_DEPARTURE_FROM_ANY_IMPORTANT_STATION = "departure_from_any_important_station"; + /** Called when any important station (start, transfers and end) reports delays. */ + public static final String EVENT_ANY_STATION_DELAYED = "any_station_delayed"; + /** Called when any important station (start, transfers and end) reports changes. */ + public static final String EVENT_ANY_STATION_CHANGED = "any_station_changed"; + + /** Called when a transfer connection is endangered. */ + public static final String EVENT_ANY_TRANSFER_ENDANGERED = "any_transfer_endangered"; + /** Called when a transfer connection is missed. */ + public static final String EVENT_ANY_TRANSFER_MISSED = "any_transfer_missed"; + /** Called when the route part changes. */ + public static final String EVENT_PART_CHANGED = "part_changed"; + public static final String EVENT_SCHEDULE_CHANGED = "schedule_changed"; + public static final String EVENT_ANY_TRAIN_CANCELLED = "train_cancelled"; + + + // Texts + private static final String keyNotificationJourneyBeginsTitle = "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title"; + private static final String keyNotificationJourneyBegins = "gui.createrailwaysnavigator.route_overview.notification.journey_begins"; + private static final String keyNotificationJourneyBeginsWithPlatform = "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform"; + private static final String keyNotificationPlatformChangedTitle = "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title"; + private static final String keyNotificationPlatformChanged = "gui.createrailwaysnavigator.route_overview.notification.platform_changed"; + private static final String keyNotificationTrainDelayedTitle = "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title"; + private static final String keyNotificationTrainDelayed = "gui.createrailwaysnavigator.route_overview.notification.train_delayed"; + private static final String keyNotificationTransferTitle = "gui.createrailwaysnavigator.route_overview.notification.transfer.title"; + private static final String keyNotificationTransfer = "gui.createrailwaysnavigator.route_overview.notification.transfer"; + private static final String keyNotificationTransferWithPlatform = "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform"; + private static final String keyNotificationConnectionEndangeredTitle = "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title"; + private static final String keyNotificationConnectionEndangered = "gui.createrailwaysnavigator.route_overview.notification.connection_endangered"; + private static final String keyNotificationConnectionMissedTitle = "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title"; + private static final String keyNotificationConnectionMissed = "gui.createrailwaysnavigator.route_overview.notification.connection_missed"; + private static final String keyNotificationJourneyCompletedTitle = "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title"; + private static final String keyNotificationJourneyCompleted = "gui.createrailwaysnavigator.route_overview.notification.journey_completed"; + private static final String keyNotificationConnectionCanceledTitle = "gui.createrailwaysnavigator.route_overview.connection_cancelled"; + private static final String keyNotificationConnectionCanceled = "gui.createrailwaysnavigator.route_overview.journey_interrupted"; + + private final Map>> listeners = new HashMap<>(); + private final Map> queuedNotifications = new HashMap<>(); + + private final long id = System.nanoTime(); + private final Map listenerIds = new HashMap<>(); + private int listenersCount = 0; + + private boolean isClosed; + + // State + private RouteProgressState progressState = RouteProgressState.BEFORE; + private final Queue queuedAnnouncements = new ConcurrentLinkedQueue<>(); + private ClientRoutePart currentPart; + private int currentPartIndex; + private final Cache> clientParts = new Cache<>(() -> getParts().stream().filter(x -> x instanceof ClientRoutePart).map(x -> (ClientRoutePart)x).toList()); + + // User settings + private boolean savedRouteRemoved = false; + private boolean showNotifications = false; + + // spam blocker + private boolean stationChangedSent = false; + private boolean scheduleChangedSent = false; + private boolean stationDelayedSent = false; + private boolean connectionWarningSent = false; + private boolean cancelledSent = false; + + private void resetSpamBlockers() { + stationChangedSent = false; + scheduleChangedSent = false; + connectionWarningSent = false; + stationDelayedSent = false; + cancelledSent = false; + } + + public ClientRoute(List parts, boolean realTimeTracker) { + super(parts, realTimeTracker); + this.currentPart = getFirstClientPart(); + + if (!realTimeTracker) return; + getClientParts().stream().forEach(x -> listenerIds.put(ClientTrainListener.register(x.getSessionId(), x.getTrainId(), x::update), x)); + CRNEventsManager.getEvent(DefaultTrainDataRefreshEvent.class).register(CreateRailwaysNavigator.MOD_ID + "_" + id, this::update); + addListener(); + + createEvent(EVENT_UPDATE); + createEvent(EVENT_ANNOUNCE_START); + createEvent(EVENT_ARRIVAL_AT_START); + createEvent(EVENT_DEPARTURE_FROM_START); + createEvent(EVENT_WHILE_TRANSIT); + createEvent(EVENT_ANNOUNCE_STOPOVER); + createEvent(EVENT_ARRIVAL_AT_STOPOVER); + createEvent(EVENT_DEPARTURE_AT_STOPOVER); + createEvent(EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION); + createEvent(EVENT_ARRIVAL_AT_TRANSFER_ARRIVAL_STATION); + createEvent(EVENT_DEPARTURE_FROM_TRANSFER_ARRIVAL_STATION); + createEvent(EVENT_WHILE_TRANSFER); + createEvent(EVENT_ANNOUNCE_TRANSFER_DEPARTURE_STATION); + createEvent(EVENT_ARRIVAL_AT_TRANSFER_DEPARTURE_STATION); + createEvent(EVENT_DEPARTURE_FROM_TRANSFER_DEPARTURE_STATION); + createEvent(EVENT_ANNOUNCE_LAST_STOP); + createEvent(EVENT_ARRIVAL_AT_LAST_STOP); + createEvent(EVENT_DEPARTURE_FROM_LAST_STOP); + createEvent(EVENT_FIRST_STOP_STATION_CHANGED); + createEvent(EVENT_FIRST_STOP_DELAYED); + createEvent(EVENT_TRANSFER_ARRIVAL_STATION_CHANGED); + createEvent(EVENT_TRANSFER_ARRIVAL_DELAYED); + createEvent(EVENT_TRANSFER_DEPARTURE_STATION_CHANGED); + createEvent(EVENT_TRANSFER_DEPARTURE_DELAYED); + createEvent(EVENT_LAST_STOP_STATION_CHANGED); + createEvent(EVENT_LAST_STOP_DELAYED); + createEvent(EVENT_ANY_STOP_ANNOUNCED); + createEvent(EVENT_ARRIVAL_AT_ANY_STOP); + createEvent(EVENT_DEPARTURE_FROM_ANY_STOP); + createEvent(EVENT_ANNOUNCE_ANY_IMPORTANT_STATION); + createEvent(EVENT_ARRIVAL_AT_ANY_IMPORTANT_STATION); + createEvent(EVENT_DEPARTURE_FROM_ANY_IMPORTANT_STATION); + createEvent(EVENT_ANY_STATION_DELAYED); + createEvent(EVENT_ANY_STATION_CHANGED); + createEvent(EVENT_ANY_TRANSFER_ENDANGERED); + createEvent(EVENT_ANY_TRANSFER_MISSED); + createEvent(EVENT_PART_CHANGED); + createEvent(EVENT_SCHEDULE_CHANGED); + createEvent(EVENT_ANY_TRAIN_CANCELLED); + + + getFirstClientPart().listen(ClientRoutePart.EVENT_ANNOUNCE_START, this, x -> { + if (currentPartIndex > 0) return; + + sendNotification( + ELanguage.translate(keyNotificationJourneyBeginsTitle, getEnd().getClientTag().tagName()), + getStart().getRealTimeStationTag().info().isPlatformKnown() ? + ELanguage.translate(keyNotificationJourneyBeginsWithPlatform, getStart().getTrainDisplayName(), getStart().getDisplayTitle(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false), getStart().getRealTimeStationTag().info().platform()) : + ELanguage.translate(keyNotificationJourneyBegins, getStart().getTrainDisplayName(), getStart().getDisplayTitle(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false)) + ); + + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_START, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + + }, x.part(), x.trainStop())); + }); + getFirstClientPart().listen(ClientRoutePart.EVENT_ARRIVAL_AT_START, this, x -> { + if (currentPartIndex != 0) return; + this.progressState = RouteProgressState.AT_START; + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_START, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + getFirstClientPart().listen(ClientRoutePart.EVENT_DEPARTURE_FROM_START, this, x -> { + if (currentPartIndex != 0) return; + this.progressState = RouteProgressState.TRAVELING; + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_START, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + getFirstClientPart().listen(ClientRoutePart.EVENT_FIRST_STOP_STATION_CHANGED, this, x -> { + if (currentPartIndex > 0) return; + notifyListeners(EVENT_ANY_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_FIRST_STOP_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + getFirstClientPart().listen(ClientRoutePart.EVENT_FIRST_STOP_DELAYED, this, x -> { + if (currentPartIndex > 0) return; + notifyListeners(EVENT_ANY_STATION_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_FIRST_STOP_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + + for (int i = 0; i < getParts().size(); i++) { + ClientRoutePart part = getClientParts().get(i); + final int k = i; + + if (i > 0) { + part.listen(ClientRoutePart.EVENT_ANNOUNCE_START, this, x -> { + if (currentPartIndex > k) return; + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_TRANSFER_DEPARTURE_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }, x.part(), x.trainStop())); + }); + part.listen(ClientRoutePart.EVENT_ARRIVAL_AT_START, this, x -> { + if (currentPartIndex != k) return; + this.progressState = RouteProgressState.BEFORE_CONTINUATION; + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_TRANSFER_DEPARTURE_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + part.listen(ClientRoutePart.EVENT_DEPARTURE_FROM_START, this, x -> { + if (currentPartIndex != k) return; + this.progressState = RouteProgressState.TRAVELING; + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_TRANSFER_DEPARTURE_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + part.listen(ClientRoutePart.EVENT_FIRST_STOP_STATION_CHANGED, this, x -> { + if (currentPartIndex > k) return; + notifyListeners(EVENT_ANY_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_TRANSFER_DEPARTURE_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + part.listen(ClientRoutePart.EVENT_FIRST_STOP_DELAYED, this, x -> { + if (currentPartIndex > k) return; + notifyListeners(EVENT_ANY_STATION_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_TRANSFER_DEPARTURE_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + } + + part.listen(ClientRoutePart.EVENT_NEXT_STOP_ANNOUNCED, this, x -> { + if (currentPartIndex > k) return; + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + this.progressState = RouteProgressState.NEXT_STOP_ANNOUNCED; + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_STOPOVER, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }, x.part(), x.trainStop())); + }); + part.listen(ClientRoutePart.EVENT_ARRIVAL_AT_STOPOVER, this, x -> { + if (currentPartIndex != k) return; + this.progressState = RouteProgressState.AT_STOPOVER; + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_STOPOVER, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + part.listen(ClientRoutePart.EVENT_DEPARTURE_FROM_STOPOVER, this, x -> { + if (currentPartIndex != k) return; + this.progressState = RouteProgressState.TRAVELING; + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_AT_STOPOVER, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + + part.listen(ClientRoutePart.EVENT_SCHEDULE_CHANGED, this, x -> { + if (scheduleChangedSent) return; + sendNotification(ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.notification.schedule_changed.title"), ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.notification.schedule_changed")); + notifyListeners(EVENT_SCHEDULE_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + scheduleChangedSent = true; + }); + + part.listen(ClientRoutePart.EVENT_TRAIN_CANCELLED, this, x -> { + if (cancelledSent) return; + sendNotification(ELanguage.translate(keyNotificationConnectionCanceledTitle), ELanguage.translate(keyNotificationConnectionCanceled, x.part().getFirstStop().getTrainDisplayName())); + notifyListeners(EVENT_ANY_TRAIN_CANCELLED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + cancelledSent = true; + }); + + if (i < parts.size() - 1) { + part.listen(ClientRoutePart.EVENT_LAST_STOP_ANNOUNCED, this, x -> { + if (currentPartIndex > k) return; + + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + this.progressState = RouteProgressState.TRANSFER_ANNOUNCED; + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }, x.part(), x.trainStop())); + }); + part.listen(ClientRoutePart.EVENT_ARRIVAL_AT_LAST_STOP, this, x -> { + if (currentPartIndex != k) return; + this.progressState = RouteProgressState.AT_TRANSFER; + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_TRANSFER_ARRIVAL_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + part.listen(ClientRoutePart.EVENT_DEPARTURE_FROM_LAST_STOP, this, x -> { + if (currentPartIndex != k) return; + this.progressState = RouteProgressState.WHILE_TRANSFER; + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_TRANSFER_ARRIVAL_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + part.listen(ClientRoutePart.EVENT_LAST_STOP_STATION_CHANGED, this, x -> { + if (currentPartIndex > k) return; + notifyListeners(EVENT_ANY_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_TRANSFER_ARRIVAL_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + part.listen(ClientRoutePart.EVENT_LAST_STOP_DELAYED, this, x -> { + if (currentPartIndex > k) return; + notifyListeners(EVENT_ANY_STATION_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_TRANSFER_ARRIVAL_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), getConnectionWith(x.trainStop()).orElse(null))); + }); + } + } + + getLastClientPart().listen(ClientRoutePart.EVENT_LAST_STOP_ANNOUNCED, this, x -> { + if (currentPartIndex > parts.size() - 1) return; + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + this.progressState = RouteProgressState.END_ANNOUNCED; + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ANNOUNCE_LAST_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }, x.part(), x.trainStop())); + }); + getLastClientPart().listen(ClientRoutePart.EVENT_ARRIVAL_AT_LAST_STOP, this, x -> { + if (currentPartIndex != parts.size() - 1) return; + this.progressState = RouteProgressState.AT_END; + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_ARRIVAL_AT_LAST_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + getLastClientPart().listen(ClientRoutePart.EVENT_DEPARTURE_FROM_LAST_STOP, this, x -> { + if (currentPartIndex < parts.size() - 1) return; + this.progressState = RouteProgressState.AFTER; + sendNotification(ELanguage.translate(keyNotificationJourneyCompletedTitle), ELanguage.translate(keyNotificationJourneyCompleted)); + if (!savedRouteRemoved) { + savedRouteRemoved = true; + SavedRoutesManager.removeRoute(this); + SavedRoutesManager.push(true, null); + } + queuedAnnouncements.clear(); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_IMPORTANT_STATION, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_DEPARTURE_FROM_LAST_STOP, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + getLastClientPart().listen(ClientRoutePart.EVENT_LAST_STOP_STATION_CHANGED, this, x -> { + if (currentPartIndex > parts.size() - 1) return; + notifyListeners(EVENT_ANY_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_LAST_STOP_STATION_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + getLastClientPart().listen(ClientRoutePart.EVENT_LAST_STOP_DELAYED, this, x -> { + if (currentPartIndex > parts.size() - 1) return; + notifyListeners(EVENT_ANY_STATION_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + notifyListeners(EVENT_LAST_STOP_DELAYED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); + }); + + for (TransferConnection connection : getConnections()) { + connection.listen(TransferConnection.EVENT_CONNECTION_ENDANGERED, this, x -> { + if (connectionWarningSent) return; + queueConnectionEndangeredNotification(x); + notifyListeners(EVENT_ANY_TRANSFER_ENDANGERED, new ListenerNotificationData(this, null, null, x)); + connectionWarningSent = true; + }); + connection.listen(TransferConnection.EVENT_CONNECTION_MISSED, this, x -> { + if (connectionWarningSent) return; + queueConnectionMissedNotification(x); + notifyListeners(EVENT_ANY_TRANSFER_MISSED, new ListenerNotificationData(this, null, null, x)); + closeAll(); + connectionWarningSent = true; + }); + } + + listen(EVENT_DEPARTURE_FROM_TRANSFER_ARRIVAL_STATION, this, (p) -> { + int idx = parts.indexOf(p.part()); + if (idx >= 0) { + if (idx < parts.size() - 1) { + currentPart = getClientParts().get(idx + 1); + currentPartIndex = idx + 1; + ClientRoutePart part = getClientParts().get(currentPartIndex); + notifyListeners(EVENT_PART_CHANGED, new ListenerNotificationData(this, part, part.getFirstClientStop(), p.connection())); + + if (part.getProgressState() != RoutePartProgressState.BEFORE && part.getProgressState() != RoutePartProgressState.AT_START) { + queueConnectionMissedNotification(p.connection()); + notifyListeners(EVENT_ANY_TRANSFER_MISSED, new ListenerNotificationData(this, null, null, p.connection())); + closeAll(); + } + return; + } + } + currentPart = p.part(); + currentPartIndex = idx; + }); + + listen(EVENT_ANY_STATION_CHANGED, this, (p) -> { + if (stationChangedSent) return; + sendNotification(ELanguage.translate(keyNotificationPlatformChangedTitle), ELanguage.translate(keyNotificationPlatformChanged, + p.trainStop().getTrainDisplayName(), + p.trainStop().getRealTimeStationTag().info().platform() + )); + stationChangedSent = true; + }); + + listen(EVENT_ANY_STATION_DELAYED, this, (p) -> { + if (stationDelayedSent) return; + queueDelayNotification(p.trainStop(), p.part().getFirstStop() == p.trainStop()); + stationDelayedSent = true; + }); + + listen(EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION, this, (p) -> { + sendNotification(ELanguage.translate(keyNotificationTransferTitle), getStart().getRealTimeStationTag().info().isPlatformKnown() ? ELanguage.translate(keyNotificationTransferWithPlatform, + p.connection().getDepartureStation().getTrainDisplayName(), + p.connection().getDepartureStation().getDisplayTitle(), + p.connection().getDepartureStation().getRealTimeStationTag().info().platform() + ) : ELanguage.translate(keyNotificationTransfer, + p.connection().getDepartureStation().getTrainDisplayName(), + p.connection().getDepartureStation().getDisplayTitle() + ) + ); + }); + } + + private void sendNotification(Component title, Component description) { + if (shouldShowNotifications()) { + ClientWrapper.sendCRNNotification(title, description); + } + } + + private void queueDelayNotification(ClientTrainStop stop, boolean start) { + if (shouldShowNotifications()) { + ClientWrapper.sendCRNNotification( + ELanguage.translate(keyNotificationTrainDelayedTitle, stop.getTrainDisplayName(), TimeUtils.parseDurationShort((int)(start ? stop.getDepartureTimeDeviation() : stop.getArrivalTimeDeviation()))), + ELanguage.translate(keyNotificationTrainDelayed, + ModUtils.formatTime(start ? stop.getRoundedRealTimeDepartureTime() : stop.getRoundedRealTimeArrivalTime(), false), + ModUtils.formatTime(start ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime(), false), + stop.getClientTag().tagName() + )); + } + } + + private void queueConnectionEndangeredNotification(TransferConnection connection) { + if (shouldShowNotifications()) { + ClientWrapper.sendCRNNotification(ELanguage.translate(keyNotificationConnectionEndangeredTitle), ELanguage.translate(keyNotificationConnectionEndangered, connection.getDepartureStation().getTrainDisplayName(), connection.getDepartureStation().getDisplayTitle())); + } + } + + private void queueConnectionMissedNotification(TransferConnection connection) { + if (shouldShowNotifications()) { + ClientWrapper.sendCRNNotification(ELanguage.translate(keyNotificationConnectionMissedTitle), ELanguage.translate(keyNotificationConnectionMissed, connection.getDepartureStation().getTrainDisplayName(), connection.getDepartureStation().getDisplayTitle())); + } + } + + public static ClientRoute empty(boolean realTimeTracker) { + return new ClientRoute(List.of(), realTimeTracker); + } + + public RouteProgressState getState() { + return progressState; + } + + @Override + public Map>> getListeners() { + return listeners; + } + + public boolean shouldShowNotifications() { + return showNotifications; + } + + public void setShowNotifications(boolean b) { + this.showNotifications = b; + } + + public List getClientParts() { + return clientParts.get(); + } + + public ClientRoutePart getFirstClientPart() { + return getClientParts().get(0); + } + + public ClientRoutePart getLastClientPart() { + return getClientParts().get(getClientParts().size() - 1); + } +/* + public TrainStop getStart() { + return getFirstPart().getFirstStop(); + } + + public TrainStop getEnd() { + return getLastPart().getLastStop(); + } + + public ImmutableList getParts() { + return ImmutableList.copyOf(parts); + } + */ + + public ClientRoutePart getCurrentPart() { + return currentPart; + } + + public int getCurrentPartIndex() { + return parts.indexOf(currentPart); + } + + public void update() { + if (isClosed) { + return; + } + + if (isAnyCancelled()) { + closeAll(); + return; + } + + notifyListeners(EVENT_UPDATE, new ListenerNotificationData(this, getCurrentPart(), getCurrentPart().getNextStop(), null)); + if (getState() == RouteProgressState.TRAVELING) { + notifyListeners(EVENT_WHILE_TRANSIT, new ListenerNotificationData(this, getCurrentPart(), getCurrentPart().getNextStop(), null)); + } else if (getState() == RouteProgressState.WHILE_TRANSFER) { + notifyListeners(EVENT_WHILE_TRANSFER, new ListenerNotificationData(this, getCurrentPart(), getCurrentPart().getNextStop(), null)); + } + + if (getState() == RouteProgressState.TRAVELING || getState() == RouteProgressState.WHILE_TRANSFER) { + while (!queuedAnnouncements.isEmpty()) { + QueuedAnnouncementEvent event = queuedAnnouncements.poll(); + if (getCurrentPart().getNextStop() != event.trainStop() || getCurrentPart() != event.part()) { + continue; + } + event.callback().run(); + break; + } + } + + getConnections().stream().forEach(x -> x.update()); + + // process notifications + queuedNotifications.entrySet().forEach(x -> { + if (x.getValue().isEmpty()) { + return; + } + }); + queuedNotifications.clear(); + isCancelled.clear(); + resetSpamBlockers(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ROUTE[" + getStart().getClientTag().tagName() + " -> " + getEnd().getClientTag().tagName() + "]"); + return builder.toString(); + } + + public void addListener() { + listenersCount++; + } + + @Override + public void close() { + listenersCount--; + if (listenersCount <= 0) { + closeAll(); + } + } + + public void closeAll() { + listenersCount = 0; + listenerIds.entrySet().stream().forEach(x -> ClientTrainListener.unregister(x.getValue().getTrainId(), x.getKey())); + getClientParts().stream().forEach(x -> { + x.stopListeningAll(this); + x.close(); + }); + getConnections().stream().forEach(x -> { + x.stopListeningAll(this); + x.close(); + }); + stopListeningAll(this); + CRNEventsManager.getEvent(DefaultTrainDataRefreshEvent.class).unregister(CreateRailwaysNavigator.MOD_ID + "_" + id); + clearEvents(); + isClosed = true; + CreateRailwaysNavigator.LOGGER.info("Route listener closed."); + } + + public static ClientRoute fromNbt(CompoundTag nbt, boolean realTimeTracker) { + return new ClientRoute( + nbt.getList(NBT_PARTS, Tag.TAG_COMPOUND).stream().map(x -> ClientRoutePart.fromNbt((CompoundTag)x)).toList(), + realTimeTracker + ); + } + + @Override + public long timeOrderValue() { + return getStart().getScheduledDepartureTime(); + } + + private static enum NotificationType { + DELAY, + CONNECTION_ENDANGERED, + CONNECTION_MISSED + } + + public boolean isClosed() { + return isClosed; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java new file mode 100644 index 00000000..ec2d8296 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java @@ -0,0 +1,323 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.Map; +import java.util.Queue; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.HashMap; + +import com.google.common.collect.ImmutableList; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.data.train.RoutePartProgressState; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.ClientTrainStop.TrainStopRealTimeData; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.util.IListenable; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +public class ClientRoutePart extends RoutePart implements ITrainListenerClient, IListenable { + + public static record ListenerNotificationData(ClientRoutePart part, ClientTrainStop trainStop) {} + public static record QueuedAnnouncementEvent(Runnable callback, ClientTrainStop trainStop) {} + + public static final String EVENT_UPDATE = "update"; + public static final String EVENT_ANNOUNCE_START = "announce_start"; + public static final String EVENT_ARRIVAL_AT_START = "arrival_at_start"; + public static final String EVENT_DEPARTURE_FROM_START = "departure_at_start"; + public static final String EVENT_WHILE_TRANSIT = "while_transit"; + public static final String EVENT_NEXT_STOP_ANNOUNCED = "next_stop_announced"; + public static final String EVENT_ARRIVAL_AT_STOPOVER = "arrival_at_stopover"; + public static final String EVENT_DEPARTURE_FROM_STOPOVER = "departure_at_stopover"; + public static final String EVENT_LAST_STOP_ANNOUNCED = "last_stop_announced"; + public static final String EVENT_ARRIVAL_AT_LAST_STOP = "arrival_at_last_stop"; + public static final String EVENT_DEPARTURE_FROM_LAST_STOP = "departure_at_last_stop"; + public static final String EVENT_FIRST_STOP_STATION_CHANGED = "first_stop_station_changed"; + public static final String EVENT_FIRST_STOP_DELAYED = "first_stop_delayed"; + public static final String EVENT_LAST_STOP_STATION_CHANGED = "last_stop_station_changed"; + public static final String EVENT_LAST_STOP_DELAYED = "last_stop_delayed"; + public static final String EVENT_ANY_STOP_ANNOUNCED = "any_stop_announced"; + public static final String EVENT_ARRIVAL_AT_ANY_STOP = "arrival_at_any_stop"; + public static final String EVENT_DEPARTURE_FROM_ANY_STOP = "departure_at_any_stop"; + public static final String EVENT_SCHEDULE_CHANGED = "schedule_changed"; + public static final String EVENT_TRAIN_CANCELLED = "train_cancelled"; + + private final Map>> listeners = new HashMap<>(); + + // state + private RoutePartProgressState progressState = RoutePartProgressState.BEFORE; + private ClientTrainStop nextStop; + private final Queue queuedAnnouncements = new ConcurrentLinkedQueue<>(); + + private final Cache> clientTrainStops = new Cache<>(() -> getAllStops().stream().filter(x -> x instanceof ClientTrainStop).map(x -> (ClientTrainStop)x).toList()); + private final Cache> clientJourneyStops = new Cache<>(() -> getAllJourneyStops().stream().filter(x -> x instanceof ClientTrainStop).map(x -> (ClientTrainStop)x).toList()); + + + public ClientRoutePart(UUID sessionId, UUID trainId, List routeStops, List allStops) { + super(sessionId, trainId, routeStops, allStops); + this.nextStop = getFirstClientStop(); + + createEvent(EVENT_UPDATE); + createEvent(EVENT_ANNOUNCE_START); + createEvent(EVENT_ARRIVAL_AT_START); + createEvent(EVENT_DEPARTURE_FROM_START); + createEvent(EVENT_WHILE_TRANSIT); + createEvent(EVENT_NEXT_STOP_ANNOUNCED); + createEvent(EVENT_ARRIVAL_AT_STOPOVER); + createEvent(EVENT_DEPARTURE_FROM_STOPOVER); + createEvent(EVENT_LAST_STOP_ANNOUNCED); + createEvent(EVENT_ARRIVAL_AT_LAST_STOP); + createEvent(EVENT_DEPARTURE_FROM_LAST_STOP); + createEvent(EVENT_FIRST_STOP_STATION_CHANGED); + createEvent(EVENT_FIRST_STOP_DELAYED); + createEvent(EVENT_LAST_STOP_STATION_CHANGED); + createEvent(EVENT_LAST_STOP_DELAYED); + createEvent(EVENT_ANY_STOP_ANNOUNCED); + createEvent(EVENT_ARRIVAL_AT_ANY_STOP); + createEvent(EVENT_DEPARTURE_FROM_ANY_STOP); + createEvent(EVENT_SCHEDULE_CHANGED); + createEvent(EVENT_TRAIN_CANCELLED); + + getFirstClientStop().listen(ClientTrainStop.EVENT_ANNOUNCE_NEXT_STOP, this, x -> { + notifyListeners(EVENT_ANNOUNCE_START, new ListenerNotificationData(this, x)); + }); + getFirstClientStop().listen(ClientTrainStop.EVENT_STATION_REACHED, this, x -> { + progressState = RoutePartProgressState.AT_START; + notifyListeners(EVENT_ARRIVAL_AT_START, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x)); + }); + getFirstClientStop().listen(ClientTrainStop.EVENT_STATION_LEFT, this, x -> { + progressState = RoutePartProgressState.TRAVELING; + notifyListeners(EVENT_DEPARTURE_FROM_START, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x)); + }); + getFirstClientStop().listen(ClientTrainStop.EVENT_DELAY, this, x -> { + notifyListeners(EVENT_FIRST_STOP_DELAYED, new ListenerNotificationData(this, x)); + }); + getFirstClientStop().listen(ClientTrainStop.EVENT_STATION_CHANGED, this, x -> { + notifyListeners(EVENT_FIRST_STOP_STATION_CHANGED, new ListenerNotificationData(this, x)); + }); + + for (ClientTrainStop stop : getClientStopovers()) { + stop.listen(ClientTrainStop.EVENT_ANNOUNCE_NEXT_STOP, this, x -> { + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + progressState = RoutePartProgressState.NEXT_STOP_ANNOUNCED; + notifyListeners(EVENT_NEXT_STOP_ANNOUNCED, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x)); + }, x)); + }); + stop.listen(ClientTrainStop.EVENT_STATION_REACHED, this, x -> { + progressState = RoutePartProgressState.AT_STOPOVER; + notifyListeners(EVENT_ARRIVAL_AT_STOPOVER, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x)); + }); + stop.listen(ClientTrainStop.EVENT_STATION_LEFT, this, x -> { + progressState = RoutePartProgressState.TRAVELING; + notifyListeners(EVENT_DEPARTURE_FROM_STOPOVER, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x)); + }); + } + + getLastClientStop().listen(ClientTrainStop.EVENT_ANNOUNCE_NEXT_STOP, this, x -> { + queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { + progressState = RoutePartProgressState.END_ANNOUNCED; + notifyListeners(EVENT_LAST_STOP_ANNOUNCED, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_ANY_STOP_ANNOUNCED, new ListenerNotificationData(this, x)); + }, x)); + }); + getLastClientStop().listen(ClientTrainStop.EVENT_STATION_REACHED, this, x -> { + progressState = RoutePartProgressState.AT_END; + queuedAnnouncements.clear(); + notifyListeners(EVENT_ARRIVAL_AT_LAST_STOP, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_ARRIVAL_AT_ANY_STOP, new ListenerNotificationData(this, x)); + }); + getLastClientStop().listen(ClientTrainStop.EVENT_STATION_LEFT, this, x -> { + progressState = RoutePartProgressState.AFTER; + queuedAnnouncements.clear(); + notifyListeners(EVENT_DEPARTURE_FROM_LAST_STOP, new ListenerNotificationData(this, x)); + notifyListeners(EVENT_DEPARTURE_FROM_ANY_STOP, new ListenerNotificationData(this, x)); + close(); + }); + getLastClientStop().listen(ClientTrainStop.EVENT_DELAY, this, x -> { + notifyListeners(EVENT_LAST_STOP_DELAYED, new ListenerNotificationData(this, x)); + }); + getLastClientStop().listen(ClientTrainStop.EVENT_STATION_CHANGED, this, x -> { + notifyListeners(EVENT_LAST_STOP_STATION_CHANGED, new ListenerNotificationData(this, x)); + }); + + getAllClientStops().stream().forEach(x -> { + x.listen(ClientTrainStop.EVENT_SCHEDULE_CHANGED, this, a -> { + notifyListeners(EVENT_SCHEDULE_CHANGED, new ListenerNotificationData(this, a)); + }); + }); + + + // TEST + listen(EVENT_DEPARTURE_FROM_ANY_STOP, this, (p) -> { + int idx = routeStops.indexOf(p.trainStop()); + if (idx < 0 || idx >= routeStops.size() - 1) { + nextStop = p.trainStop(); + } else { + nextStop = (ClientTrainStop)routeStops.get(idx + 1); + } + }); + } + + @Override + public Map>> getListeners() { + return listeners; + } + + public List getAllJourneyClientStops() { + return clientJourneyStops.get(); + } + + public List getAllClientStops() { + return clientTrainStops.get(); + } + + public List getClientStopovers() { + return getAllClientStops().size() <= 2 ? List.of() : ImmutableList.copyOf(getAllClientStops().subList(1, routeStops.size() - 1)); + } + + public ClientTrainStop getFirstClientStop() { + return getAllClientStops().get(0); + } + + public ClientTrainStop getLastClientStop() { + return getAllClientStops().get(getAllClientStops().size() - 1); + } + + public ClientTrainStop getNextStop() { + return nextStop; + } + + public int getNextStopIndex() { + return routeStops.indexOf(nextStop); + } + + public RoutePartProgressState getProgressState() { + return progressState; + } + + @Override + public void update(TrainRealTimeData data) { + if (isCancelled()) { + return; + } + + status.clear(); + if (!data.sessionId().equals(getSessionId())) { + cancelled = true; + } + + MutableSingle shouldRenderStatus = new MutableSingle<>(false); + + getAllClientStops().stream().forEach(x -> { + if (data.stationData().containsKey(x.getScheduleIndex())) { + x.update(data.stationData().get(x.getScheduleIndex())); + if (x.shouldRenderRealTime()) { + shouldRenderStatus.setFirst(true); + } + } + }); + getAllJourneyClientStops().stream().forEach(x -> { + if (data.stationData().containsKey(x.getScheduleIndex())) { + x.update(data.stationData().get(x.getScheduleIndex())); + } + }); + + if (shouldRenderStatus.getFirst() || data.cancelled()) { + status.addAll(data.statusInfo()); + } + + this.cancelled = this.cancelled || data.cancelled(); + + notifyListeners(EVENT_UPDATE, new ListenerNotificationData(this, nextStop)); + if (getProgressState() == RoutePartProgressState.TRAVELING) { + notifyListeners(EVENT_WHILE_TRANSIT, new ListenerNotificationData(this, nextStop)); + + while (!queuedAnnouncements.isEmpty()) { + QueuedAnnouncementEvent event = queuedAnnouncements.peek(); + if (routeStops.indexOf(event.trainStop()) > getNextStopIndex()) { + break; + } + event = queuedAnnouncements.poll(); + if (getNextStop() != event.trainStop()) { + continue; + } + event.callback().run(); + break; + } + } + + if (isCancelled()) { + CreateRailwaysNavigator.LOGGER.info("Train got cancelled. Closing route..."); + notifyListeners(EVENT_TRAIN_CANCELLED, new ListenerNotificationData(this, nextStop)); + close(); + } + } + + public static RoutePart fromNbt(CompoundTag nbt) { + return new ClientRoutePart( + nbt.contains(NBT_SESSION_ID) ? nbt.getUUID(NBT_SESSION_ID) : new UUID(0, 0), + nbt.getUUID(NBT_TRAIN_ID), + nbt.getList(NBT_STOPS, Tag.TAG_COMPOUND).stream().map(x -> ClientTrainStop.fromNbt((CompoundTag)x)).toList(), + nbt.getList(NBT_JOURNEY, Tag.TAG_COMPOUND).stream().map(x -> ClientTrainStop.fromNbt((CompoundTag)x)).toList() + ); + } + + @Override + public void close() { + getAllClientStops().stream().forEach(x -> { + x.stopListeningAll(this); + x.close(); + }); + getAllJourneyClientStops().stream().forEach(x -> { + x.stopListeningAll(this); + x.close(); + }); + stopListeningAll(this); + } + + + public static record TrainRealTimeData(UUID sessionId, Map stationData, Set statusInfo, boolean cancelled) { + + private static final String NBT_SESSION_ID = "SessionId"; + private static final String NBT_STATUS_INFOS = "Status"; + private static final String NBT_CANCELLED = "Cancelled"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putUUID(NBT_SESSION_ID, sessionId); + ListTag status = new ListTag(); + status.addAll(statusInfo().stream().map(x -> x.toNbt()).toList()); + nbt.put(NBT_STATUS_INFOS, status); + nbt.putBoolean(NBT_CANCELLED, cancelled); + + for (Map.Entry e : stationData.entrySet()) { + nbt.put("" + e.getKey(), e.getValue().toNbt()); + } + return nbt; + } + + public static TrainRealTimeData fromNbt(CompoundTag nbt) { + return new TrainRealTimeData( + nbt.getUUID(NBT_SESSION_ID), + nbt.getAllKeys().stream().filter(x -> { try { Integer.parseInt(x); return true; } catch (Exception e) { return false; } }).collect(Collectors.toMap(x -> Integer.parseInt(x), x -> TrainStopRealTimeData.fromNbt(nbt.getCompound(x)))), + nbt.getList(NBT_STATUS_INFOS, Tag.TAG_COMPOUND).stream().map(x -> CompiledTrainStatus.fromNbt((CompoundTag)x)).collect(Collectors.toSet()), + nbt.getBoolean(NBT_CANCELLED) + ); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java new file mode 100644 index 00000000..7f291924 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java @@ -0,0 +1,67 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.ArrayList; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.data.Pair; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; + +public final class ClientTrainListener { + + private static final ConcurrentHashMap>>> callbacks = new ConcurrentHashMap<>(); + + public static int debug_registeredListenersCount() { + return callbacks.values().stream().mapToInt(x -> x.size()).sum(); + } + + public static UUID register(UUID sessionId, UUID trainId, Consumer callback) { + Map>> trainCallbacks = callbacks.computeIfAbsent(trainId, x -> new ConcurrentHashMap<>()); + + UUID id = null; + do { + id = UUID.randomUUID(); + } while (trainCallbacks.containsKey(id)); + + trainCallbacks.put(id, Pair.of(sessionId, callback)); + return id; + } + + public static void unregister(UUID trainId, UUID callbackId) { + if (callbacks.containsKey(trainId)) { + Map>> trainCallbacks = callbacks.get(trainId); + if (trainCallbacks.containsKey(callbackId)) { + trainCallbacks.remove(callbackId); + } + + if (trainCallbacks.isEmpty()) { + callbacks.remove(trainId); + } + } + } + + public static void tick(Runnable andThen) { + callbacks.entrySet().stream().forEach(x -> { + if (x.getValue().isEmpty()) { + callbacks.remove(x.getKey()); + return; + } + + final Map>> listeners = x.getValue(); + DataAccessor.getFromServer(x.getKey(), ModAccessorTypes.UPDATE_REALTIME, res -> { + if (res != null) { + new ArrayList<>(listeners.values()).stream().forEach(a -> a.getSecond().accept(res)); + } + DLUtils.doIfNotNull(andThen, a -> a.run()); + }); + }); + } + + public static void clear() { + callbacks.clear(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/EdgeData.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/EdgeData.java new file mode 100644 index 00000000..2a6b09e5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/EdgeData.java @@ -0,0 +1,63 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.UUID; + +import de.mrjulsen.crn.data.train.TrainPrediction; + +public class EdgeData implements Comparable { + + private final TrainPrediction prediction; + private final Node node1; + private final Node node2; + + // Calc + private long cost = -1; + + protected EdgeData(Node node1, Node node2, TrainPrediction prediction, long cost) { + this.prediction = prediction; + this.node1 = node1; + this.node2 = node2; + this.cost = cost; + } + + public EdgeData(Node node1, Node node2, TrainPrediction prediction) { + this(node1, node2, prediction, prediction.getLastTransitTime()); + } + + public Node getFirstNode() { + return node1; + } + + public Node getSecondNode() { + return node2; + } + + public UUID getTrainId() { + return prediction.getData().getTrainId(); + } + + public boolean connected(EdgeData other) { + return getTrainId().equals(other.getTrainId()) && (getSectionIndex() == other.getSectionIndex()); + } + + public int getSectionIndex() { + return prediction.getSection().getScheduleIndex(); + } + + public TrainPrediction getPrediction() { + return prediction; + } + + public long getCost() { + return cost; + } + + public EdgeData invert() { + return new EdgeData(node2, node1, prediction); + } + + @Override + public int compareTo(EdgeData o) { + return Long.compare(cost, o.cost); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ITrainListenerClient.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ITrainListenerClient.java new file mode 100644 index 00000000..ea5dc4d9 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ITrainListenerClient.java @@ -0,0 +1,5 @@ +package de.mrjulsen.crn.data.navigation; + +public interface ITrainListenerClient extends AutoCloseable { + void update(T data); +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java new file mode 100644 index 00000000..9320b6bf --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java @@ -0,0 +1,430 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainPrediction; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.data.train.TrainUtils; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.crn.data.navigation.Node.EdgeConnection; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; + +/* ####################################################### + * + * § 1: Das Navigationssystem ist unantastbar! + * + * ####################################################### + */ + +public class NavigatableGraph { + + protected final UserSettings userSettings; + + protected final Map nodesByTag = new HashMap<>(); + protected final Map> nodesByTrain = new HashMap<>(); + protected final Map /* connection */>> edgesByTag = new HashMap<>(); + + //#region GRAPH GENERATION + + /** Server-side only! */ + public NavigatableGraph(UserSettings userSettings) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + this.userSettings = userSettings; + long startTime = System.currentTimeMillis(); + Set trains = TrainListener.getAllTrains().stream().filter(x -> + !globalSettings().isTrainBlacklisted(x) && + !globalSettings().isTrainExcludedByUser(x, userSettings) && + TrainUtils.isTrainUsable(x) + ).collect(Collectors.toSet()); + for (Train train : trains) { + addTrain(train, TrainListener.data.get(train.id)); + } + + CreateRailwaysNavigator.LOGGER.info(String.format("Graph generated. Took %sms. Contains %s nodes and %s edges. %s train processed.", + System.currentTimeMillis() - startTime, + nodesByTag.size(), + edgesByTag.values().stream().flatMap(x -> x.values().stream().flatMap(y -> y.stream())).count(), + trains.size() + )); + } + + protected GlobalSettings globalSettings() { + return GlobalSettings.getInstance(); + } + + protected void addTrain(Train train, TrainData data) { + Deque predictions = new ConcurrentLinkedDeque<>(data.getPredictions()); + boolean singleSection = data.isSingleSection(); + if (predictions.isEmpty()) { + return; + } + + if (CreateRailwaysNavigator.isDebug()) CreateRailwaysNavigator.LOGGER.info("\nEDGES FOR TRAIN: " + train.name.getString() + ", Single Section: " + singleSection + ", Sections Count: " + data.getSections().size()); + StringBuilder sb = new StringBuilder(); + + TrainPrediction lastPrediction = null; // The last prediction in the list + boolean nothingFound = true; + boolean stationsRemoved = false; + while (!predictions.isEmpty()) { + TrainPrediction prediction = predictions.peekLast(); + TrainTravelSection section = prediction.getSection(); + if ((globalSettings().isStationBlacklisted(prediction.getStationName())) || + (!section.isUsable() && (!section.isFirstStop(prediction) || !section.previousSection().isUsable() || !section.previousSection().shouldIncludeNextStationOfNextSection())) + ) { + predictions.removeLast(); + stationsRemoved = true; + continue; + } + + nothingFound = false; + lastPrediction = prediction; + break; + } + if (nothingFound || lastPrediction == null) { + return; + } + + Node lastNode = addNode(lastPrediction); + TrainPrediction lastTrainPrediction = lastPrediction; + if (CreateRailwaysNavigator.isDebug()) sb.append(lastTrainPrediction.getStationTag().getTagName().get()); + boolean noEdgePossible = false; + while (!predictions.isEmpty()) { + TrainPrediction prediction = predictions.poll(); + if (!isPredictionAllowed(prediction)) { + noEdgePossible = true; + continue; + } + Node node = addNode(prediction); + if ((!noEdgePossible && !stationsRemoved && lastTrainPrediction.getSection().getScheduleIndex() == prediction.getSection().getScheduleIndex()) || lastTrainPrediction.getSection().shouldIncludeNextStationOfNextSection()) { + addEdge(lastNode, node, prediction); + if (CreateRailwaysNavigator.isDebug()) sb.append(" ----- "); + } else { + if (CreateRailwaysNavigator.isDebug()) sb.append(" > < "); + } + lastNode = node; + lastTrainPrediction = prediction; + noEdgePossible = false; + stationsRemoved = false; + if (CreateRailwaysNavigator.isDebug()) sb.append(prediction.getStationTag().getTagName().get()); + } + if (CreateRailwaysNavigator.isDebug()) CreateRailwaysNavigator.LOGGER.info(sb.toString()); + } + + protected boolean isPredictionAllowed(TrainPrediction prediction) { + TrainTravelSection section = prediction.getSection(); + boolean usable = section.isUsable() || (section.isFirstStop(prediction) && section.previousSection().isUsable() && section.previousSection().shouldIncludeNextStationOfNextSection()); + return !globalSettings().isStationBlacklisted(prediction.getStationName()) && (prediction.getSection().getTrainGroup() == null || !userSettings.navigationExcludedTrainGroups.getValue().contains(prediction.getSection().getTrainGroup().getGroupName())) && usable; + } + + protected Node addNode(TrainPrediction prediction) { + StationTag tag = prediction.getStationTag(); + Node node = nodesByTag.computeIfAbsent(tag, x -> new Node(prediction)); + nodesByTrain.computeIfAbsent(prediction.getData().getTrainId(), x -> new HashSet<>()).add(node); + node.addTrain(prediction); + return node; + } + + protected void addEdge(Node first, Node second, TrainPrediction prediction) { + if (first == second) { + return; + } + edgesByTag.computeIfAbsent(first, x -> new HashMap<>()).computeIfAbsent(second, x -> new HashSet<>()).add(new EdgeData(first, second, prediction)); + } + +//#endregion +//#region GRAPH ACCESSORS + + protected Map dijkstra(StationTag start, boolean avoidTransfers) { + nodesByTag.values().forEach(x -> x.init()); + Node startNode = nodesByTag.get(start); + startNode.setConnection(startNode, null, 0); + startNode.setTransferPoint(true); + + PriorityQueue queue = new PriorityQueue<>(); + Map excludedNodes = new HashMap<>(); + queue.add(startNode); + + while (!queue.isEmpty()) { + Node currentNode = queue.poll(); + Map /* via */> reachableNodes = edgesByTag.get(currentNode); + + if (reachableNodes != null) { + reachableNodes.entrySet().stream().filter(x -> !excludedNodes.containsKey(x.getKey().getStationTag())).forEach(y -> { + final Node targetNode = y.getKey(); + y.getValue().forEach(x -> { + final EdgeData viaEdge = x; + + EdgeConnection connection = currentNode.selectBestConnectionFor(targetNode, viaEdge); + boolean isTransfer = connection != null && !connection.edge().connected(viaEdge); + + // TODO: Evtl die kürzeste Umstiegszeit (also Kosten für den Umstieg) berechnen lassen anhand der aktuellen Verkehrslage + + long newCost = currentNode.getCost() + viaEdge.getCost() + (isTransfer && avoidTransfers ? ModCommonConfig.TRANSFER_COST.get() : 0); + targetNode.setConnection(currentNode, viaEdge, newCost); + }); + queue.add(targetNode); + }); + } + + excludedNodes.put(currentNode.getStationTag(), currentNode); + } + + return excludedNodes; + } + + protected Node dijkstraProcessor(Map nodes, StationTag start, StationTag end) { + if (nodes.size() <= 1 || !nodes.containsKey(end) || !nodes.containsKey(start)) { + return null; + } + + Node currentNode = nodes.get(end); + while (!currentNode.getStationTag().equals(start)) { + Node prevNode = currentNode.getPreviousNode(); + if (prevNode == null || currentNode == prevNode) break; + prevNode.setNextNode(currentNode); + currentNode = prevNode; + } + return currentNode; + } + + public List searchRoute(StationTag start, StationTag end, boolean avoidTransfers) { + if (!nodesByTag.containsKey(start) || !nodesByTag.containsKey(end)) { + return List.of(); + } + + Node startNode = dijkstraProcessor(dijkstra(start, avoidTransfers), start, end); + + if (CreateRailwaysNavigator.isDebug()) { + StringBuilder sb = new StringBuilder("Dijkstra Nodes: "); + Node node = startNode; + while (node != null) { + sb.append(node.getStationTag().getTagName().get() + " > "); + node = node.getNextNode(); + } + CreateRailwaysNavigator.LOGGER.info(sb.toString()); + } + + if (startNode == null) { + return List.of(); + } + startNode.setTransferPoint(true); + + List route = new ArrayList<>(); + Node currentNode = startNode; + while (!currentNode.getStationTag().equals(end)) { + route.add(currentNode); + MutableSingle nextNode = new MutableSingle(currentNode.getNextNode()); + List connections = new ArrayList<>(currentNode.getNextConnections().stream().filter(x -> x.edge().getSecondNode().equals(nextNode.getFirst())).toList()); + while (nextNode.getFirst() != null && connections != null && !connections.isEmpty()) { + Node pNode = nextNode.getFirst().getNextNode(); + if (pNode == null) { + break; + } + List pConnections = nextNode.getFirst().getNextConnections().stream().filter(x -> x.edge().getSecondNode().equals(pNode)).toList(); + connections.removeIf(x -> pConnections.stream().noneMatch(y -> x.edge().connected(y.edge()))); + if (connections.isEmpty()) break; + nextNode.setFirst(pNode); + } + + currentNode = nextNode.getFirst(); + currentNode.setTransferPoint(true); + } + currentNode.setTransferPoint(true); + route.add(currentNode); + + return route; + } + + public ImmutableMap createTrainSchedules() { + return ImmutableMap.copyOf(TrainUtils.getTrains(true).stream() + .filter(x -> { + return TrainListener.data.containsKey(x.id) && + TrainUtils.isTrainUsable(x) && + !globalSettings().isTrainBlacklisted(x) && + !globalSettings().isTrainExcludedByUser(x, userSettings) + ; + }) + .map(x -> new TrainSchedule(TrainListener.data.get(x.id).getSessionId(), x)) + .collect(Collectors.toMap(x -> x.getTrain().id, x -> x))); + } + + public List searchTrainsForRoute(List nodes) { + ImmutableMap schedules = createTrainSchedules(); + + StringBuilder sb = new StringBuilder("Transfer points: "); + Queue transferNodes = new ConcurrentLinkedQueue<>(nodes.stream().filter(x -> x.isTransferPoint()).peek(x -> { + if (CreateRailwaysNavigator.isDebug()) sb.append(x.getStationTag().getTagName().get() + " > "); + }).toList()); + if (CreateRailwaysNavigator.isDebug()) CreateRailwaysNavigator.LOGGER.info(sb.toString()); + + if (transferNodes.size() <= 1) { + return List.of(); + } + + Node start = transferNodes.poll(); + Node end = transferNodes.poll(); + + Set excludedTrainIds = new HashSet<>(); + List departingTrains = TrainUtils.getDepartingTrainsAt(start.getStationTag()).stream().filter(train -> + TrainListener.data.containsKey(train.id) && + TrainUtils.isTrainUsable(train) && + !excludedTrainIds.contains(train.id) && + !globalSettings().isTrainBlacklisted(train) && + schedules.containsKey(train.id) && + schedules.get(train.id).stopsAt(start.getStationTag()) && + schedules.get(train.id).stopsAt(end.getStationTag()) + ).toList(); + + List routes = new ArrayList<>(); + + for (Train train : departingTrains) { + TrainData trainData = TrainListener.data.get(train.id); + int simulationTime = userSettings.navigationDepartureInTicks.getValue(); + Queue tempTransferNodes = new ConcurrentLinkedQueue<>(transferNodes); + Node tempEnd = end; + + RoutePart part = RoutePart.get(schedules.get(train.id).getSessionId(), schedules.get(train.id).simulate(simulationTime), start.getStationTag(), end.getStationTag(), userSettings); + if (!RoutePart.validate(part, trainData)) { + continue; + } + + while (!tempTransferNodes.isEmpty()) { + RoutePart tempPart = RoutePart.get(schedules.get(train.id).getSessionId(), schedules.get(train.id).simulate(simulationTime), start.getStationTag(), tempTransferNodes.peek().getStationTag(), userSettings); + if (!RoutePart.validate(tempPart, trainData)) { + break; + } + part = tempPart; + tempEnd = tempTransferNodes.poll(); + } + + + if (ModCommonConfig.EXCLUDE_TRAINS.get()) excludedTrainIds.add(train.id); + + // Step 2 + List parts = new ArrayList<>(); + parts.add(part); + if (!tempTransferNodes.isEmpty()) { + Set exclTrns = new HashSet<>(excludedTrainIds); + if (ModCommonConfig.EXCLUDE_TRAINS.get()) exclTrns.add(part.getTrainId()); + List res = searchForTrainsInternal(tempEnd, schedules, new ConcurrentLinkedQueue<>(tempTransferNodes), exclTrns, part); + if (res == null) continue; + parts.addAll(res); + } + routes.add(new Route(parts, false)); + } + + return routes; + } + + public List searchForTrainsInternal(Node start, ImmutableMap schedules, Queue transferNodes, Set excludedTrainIds, RoutePart previousPart) { + Node end = transferNodes.poll(); + + List departingTrains = TrainUtils.getDepartingTrainsAt(start.getStationTag()).stream().filter(train -> + TrainListener.data.containsKey(train.id) && + TrainUtils.isTrainUsable(train) && + !excludedTrainIds.contains(train.id) && + !globalSettings().isTrainBlacklisted(train) && + schedules.containsKey(train.id) && + schedules.get(train.id).stopsAt(start.getStationTag()) && + schedules.get(train.id).stopsAt(end.getStationTag()) + ).toList(); + + RoutePart bestPart = null; + Node bestPartEnd = null; + Queue bestPartRemainingTransfers = null; + + for (Train train : departingTrains) { + TrainData trainData = TrainListener.data.get(train.id); + long simulationTime = previousPart.timeUntilEnd() - 1 + userSettings.navigationTransferTime.getValue(); + Queue tempTransferNodes = new ConcurrentLinkedQueue<>(transferNodes); + Node tempEnd = end; + + RoutePart part = RoutePart.get(schedules.get(train.id).getSessionId(), schedules.get(train.id).simulate(simulationTime), start.getStationTag(), tempEnd.getStationTag(), userSettings); + if (!RoutePart.validate(part, trainData)) { + continue; + } + + while (!tempTransferNodes.isEmpty()) { + RoutePart tempPart = RoutePart.get(schedules.get(train.id).getSessionId(), schedules.get(train.id).simulate(simulationTime), start.getStationTag(), tempTransferNodes.peek().getStationTag(), userSettings); + if (!RoutePart.validate(tempPart, trainData)) { + break; + } + part = tempPart; + tempEnd = tempTransferNodes.poll(); + } + + if (bestPart == null || part.compareTo(bestPart) <= 0) { + bestPart = part; + bestPartEnd = tempEnd; + bestPartRemainingTransfers = tempTransferNodes; + } + } + + if (bestPart == null || bestPart.isEmpty()) { + return null; + } + + if (ModCommonConfig.EXCLUDE_TRAINS.get()) excludedTrainIds.add(bestPart.getTrainId()); + List parts = new ArrayList<>(); + parts.add(bestPart); + if (bestPartRemainingTransfers != null && !bestPartRemainingTransfers.isEmpty()) { + List res = searchForTrainsInternal(bestPartEnd, schedules, bestPartRemainingTransfers, excludedTrainIds, bestPart); + if (res == null) return null; + parts.addAll(res); + } + return parts; + } + + /** + * Generates possible routes from the start station to the destination station. + * @param start The start station. + * @param destination The destination station. + * @param playerId The UUID of the player to use its user settings, or {@code null} to use the default settings. + * @param avoidTransfers Tries to avoid transfers as much as possible. + * @return The possible routes. + */ + public static List searchRoutes(StationTag start, StationTag destination, UUID playerId, boolean avoidTransfers) { + long startTime = System.currentTimeMillis(); + UserSettings userSettings = UserSettings.getSettingsFor(playerId, true); + NavigatableGraph graph = new NavigatableGraph(userSettings); + + List nodes = graph.searchRoute(start, destination, avoidTransfers); + List routes = graph.searchTrainsForRoute(nodes); + + int minNumber = routes.stream().mapToInt(x -> x.getTransferCount()).min().orElse(0); + routes = routes.stream().filter(x -> x.getTransferCount() == minNumber).toList(); + + CreateRailwaysNavigator.LOGGER.info(String.format("%s route(s) calculated. Took %sms.", + routes.size(), + System.currentTimeMillis() - startTime + )); + return routes.stream().sorted((a, b) -> Long.compare(a.getStart().getScheduledDepartureTime(), b.getStart().getScheduledDepartureTime())).toList(); + } + + //#endregion +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java new file mode 100644 index 00000000..759ad40c --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java @@ -0,0 +1,185 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.base.Objects; + +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.train.TrainPrediction; + +public class Node implements Comparable { + private final StationTag tag; + private final Map departingTrains = new HashMap<>(); + + // Dijkstra + private long cost = Long.MAX_VALUE; + private Node previousNode = null; + private Node nextNode = null; + //private EdgeData previousEdge = null; + private final Set connections = new HashSet<>(); + private final Set nextConnections = new HashSet<>(); + private final Map preferredConnectionForNode = new HashMap<>(); + + private boolean isTransferPoint = false; + + + + public Node(TrainPrediction prediction) { + this.tag = prediction.getStationTag(); + } + + public void addTrain(TrainPrediction prediction) { + this.departingTrains.put(prediction.getData().getTrainId(), prediction); + } + + public Map getIdsOfDepartingTrains() { + return departingTrains; + } + + public StationTag getStationTag() { + return tag; + } + + public long getCost() { + return cost; + } + + public boolean setCost(long cost) { + boolean b = this.cost != cost; + if (b) { + connections.clear(); + preferredConnectionForNode.clear(); + } + this.cost = cost; + return b; + } + + public Node getPreviousNode() { + return previousNode; + } + + public void setConnection(Node previousNode, EdgeData viaEdge, long cost) { + if (cost > this.cost) { + return; + } + setCost(cost); + if (viaEdge != null) { + connections.add(new EdgeConnection(previousNode, viaEdge)); + } + setPreviousNode(previousNode); + } + + public void setPreviousNode(Node previousNode) { + this.previousNode = previousNode; + } + + public void setNextNode(Node nextNode) { + this.nextNode = nextNode; + nextNode.getConnections().stream().filter(x -> x.target() == this).forEach(x -> { + nextConnections.add(new EdgeConnection(nextNode, x.edge().invert())); + }); + } + + public Node getNextNode() { + return nextNode; + } + + public boolean isTransferPoint() { + return isTransferPoint; + } + + public void setTransferPoint(boolean isTransferPoint) { + this.isTransferPoint = isTransferPoint; + } + + public boolean hasConnections() { + return !this.connections.isEmpty(); + } + + @Override + public int compareTo(Node o) { + return Long.compare(cost, o.cost); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Node o && + tag.equals(o.tag) + ; + } + + @Override + public int hashCode() { + return Objects.hashCode(tag); + } + + public void init() { + this.cost = Long.MAX_VALUE; + this.previousNode = null; + this.isTransferPoint = false; + this.connections.clear(); + this.preferredConnectionForNode.clear(); + } + + public EdgeConnection selectBestConnectionFor(Node nextNode, EdgeData nextEdge) { // Sucht die beste Edge zu diesem Knoten unter Beachtung des nächsten Knotens + if (!hasConnections()) { // Keine Alternativen + return null; + } + + EdgeConnection possibleConnection = getPreviousNode() == null ? null : (getPreviousNode().getPreferredConnectionFor(this)); + + for (EdgeConnection connection : connections) { + if (connection.edge() == null) continue; + + if (connection.edge().connected(nextEdge)) { // Wenn es zuvor eine Edge gab, die mit Zug X erreichbar ist, und Zug X auch auf der neuen Edge benötigt wird, wähle diese. + possibleConnection = connection; + break; + } + } + + if (possibleConnection == null) { + return null; + } + + preferredConnectionForNode.put(nextNode, possibleConnection); // Speichere diese Präferenz. Wenn man mit der späteren Node dann hier sucht, bekommt man die Conenction zurück. + return possibleConnection; + } + + public EdgeConnection getPreferredConnectionFor(Node node) { + return preferredConnectionForNode.getOrDefault(node, connections.stream().findFirst().orElse(null)); + } + + + public Set getConnections() { + return connections; + } + + public Set getNextConnections() { + return nextConnections; + } + + + public static class EdgeConnection { + + private final Node target; + private final EdgeData edge; + + public EdgeConnection(Node target, EdgeData edge) { + this.target = target; + this.edge = edge; + } + + public Node target() { + return target; + } + + public EdgeData edge() { + return edge; + } + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java new file mode 100644 index 00000000..678e4d62 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java @@ -0,0 +1,146 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import com.google.common.collect.ImmutableList; +import java.lang.StringBuilder; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.data.ISaveableNavigatorData; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +public class Route implements ISaveableNavigatorData { + + protected static final String NBT_PARTS = "Parts"; + + protected final List parts; + protected final List connections; + protected final Cache isCancelled = new Cache<>(() -> getParts().stream().anyMatch(x -> x.isCancelled())); + + public Route(List parts, boolean realTimeTracker) { + this.parts = parts; + this.connections = TransferConnection.getConnections(parts); + } + + public static Route empty(boolean realTimeTracker) { + return new Route(List.of(), realTimeTracker); + } + + public List getConnections() { + return connections; + } + + public Optional getConnectionWith(TrainStop stop) { + return getConnections().stream().filter(x -> x.getArrivalStation() == stop || x.getDepartureStation() == stop).findFirst(); + } + + public RoutePart getFirstPart() { + return parts.get(0); + } + + public RoutePart getLastPart() { + return parts.get(parts.size() - 1); + } + + public TrainStop getStart() { + return getFirstPart().getFirstStop(); + } + + public TrainStop getEnd() { + return getLastPart().getLastStop(); + } + + public ImmutableList getParts() { + return ImmutableList.copyOf(parts); + } + + public int getTransferCount() { + return parts.size() - 1; + } + + public long departureIn() { + return getFirstPart().departureIn(); + } + + public long arrivalAtDestinationIn() { + return getLastPart().timeUntilEnd(); + } + + public long travelTime() { + return arrivalAtDestinationIn() - departureIn(); + } + + public boolean isAnyCancelled() { + return isCancelled.get(); + } + + /** Checks whether the connection to this part of the route is still reachable or not. The first part is always reachable because there is no transfer there. A part is marked as reachable if the train has not yet departed there. */ + public boolean isPartReachable(RoutePart part) { + int idx = parts.indexOf(part); + if (idx <= 0) { + return true; + } + for (int i = 0; i < connections.size(); i++) { + if (connections.get(i).isConnectionMissed()) { + return !(i < idx); + } + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ROUTE[" + getStart().getClientTag().tagName() + " -> " + getEnd().getClientTag().tagName() + "]"); + return builder.toString(); + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + ListTag list = new ListTag(); + list.addAll(parts.stream().map(x -> x.toNbt()).toList()); + nbt.put(NBT_PARTS, list); + return nbt; + } + + public static Route fromNbt(CompoundTag nbt, boolean realTimeTracker) { + return new Route( + nbt.getList(NBT_PARTS, Tag.TAG_COMPOUND).stream().map(x -> RoutePart.fromNbt((CompoundTag)x)).toList(), + realTimeTracker + ); + } + + @Override + public List getOverviewData() { + List lines = new ArrayList<>(); + lines.add(new SaveableNavigatorDataLine(TextUtils.text(ModUtils.formatTime(getStart().getScheduledDepartureTime(), false) + " " + getStart().getClientTag().tagName()), ModGuiIcons.ROUTE_START.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); + lines.add(new SaveableNavigatorDataLine(TextUtils.text(ModUtils.formatTime(getEnd().getScheduledArrivalTime(), false) + " " + getEnd().getClientTag().tagName()), ModGuiIcons.ROUTE_END.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); + lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.date", (getStart().getScheduledDepartureTime() + DragonLib.DAYTIME_SHIFT) / DragonLib.TICKS_PER_DAY, ModUtils.formatTime(getStart().getScheduledDepartureTime(), false)).append(" | ").append(TimeUtils.parseDurationShort(departureIn())), ModGuiIcons.CALENDAR.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); + lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.transfers", getTransferCount()).append(TextUtils.text(" | " + TimeUtils.parseDurationShort(travelTime()))), ModGuiIcons.INFO.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); + if (isAnyCancelled()) { + lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.cancelled").withStyle(ChatFormatting.RED), ModGuiIcons.IMPORTANT.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); + } + return lines; + } + + @Override + public SaveableNavigatorDataLine getTitle() { + return new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.saved_route"), ModGuiIcons.BOOKMARK.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE)); + } + + @Override + public long timeOrderValue() { + return getStart().getScheduledDepartureTime(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java new file mode 100644 index 00000000..9e95601f --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java @@ -0,0 +1,217 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.Set; +import java.util.UUID; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.data.Pair; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +public class RoutePart implements Comparable { + + protected static final String NBT_SESSION_ID = "SessionId"; + protected static final String NBT_TRAIN_ID = "TrainId"; + protected static final String NBT_STOPS = "Stops"; + protected static final String NBT_JOURNEY = "EntireJourney"; + + protected final UUID sessionId; + protected final UUID trainId; + protected final List routeStops; + protected final List allStops; + protected boolean cancelled; + protected final Set status = new HashSet<>(); + + public static RoutePart get(UUID sessionId, TrainSchedule schedule, StationTag from, StationTag to, UserSettings settings) { + List stops = getBetween(schedule, from, to, settings).stream().filter(x -> !x.isEmpty() && x.stream().limit(x.size() - 1).allMatch(y -> y.getSectionIndex() == x.get(0).getSectionIndex())).findFirst().orElse(List.of()); + List entireJourney = List.of(); + if (stops.isEmpty()) { + return null; + } else { + TrainStop firstStop = stops.get(0); + entireJourney = TrainSchedule.ofSectionForIndex(sessionId, schedule.getTrain(), firstStop.getScheduleIndex(), firstStop.getScheduleIndex(), firstStop.getSimulationTime()).getAllStops(); + } + return new RoutePart(sessionId, schedule.getTrain().id, stops, entireJourney); + } + + public static boolean validate(RoutePart part, TrainData trainData) { + if (part == null || part.isEmpty()) { + return false; + } + int startSectionIndex = part.getFirstStop().getSectionIndex(); + int endSectionIndex = part.getLastStop().getSectionIndex(); + TrainTravelSection startSection = trainData.getSectionByIndex(startSectionIndex); + TrainTravelSection endSection = trainData.getSectionByIndex(endSectionIndex); + if (startSectionIndex != endSectionIndex && !(endSection.isFirstStop(part.getLastStop().getScheduleIndex()) && endSection.previousSection() == startSection && startSection.shouldIncludeNextStationOfNextSection() && startSection.isUsable())) { + return false; + } + return true; + } + + public RoutePart(UUID sessionId, TrainSchedule schedule) { + this( + sessionId, + schedule.getTrain().id, + schedule.getAllStops(), + TrainSchedule.ofSectionForIndex( + sessionId, + schedule.getTrain(), + schedule.getAllStops().isEmpty() ? 0 : schedule.getAllStops().get(schedule.getAllStops().size() - 1).getScheduleIndex(), + schedule.getAllStops().isEmpty() ? 0 : schedule.getAllStops().get(0).getScheduleIndex(), + schedule.getAllStops().isEmpty() ? 0 : schedule.getAllStops().get(0).getSimulationTime() + ).getAllStops() + ); + } + + public RoutePart(UUID sessionId, UUID trainId, List routeStops, List allStops) { + this.sessionId = sessionId; + this.routeStops = routeStops; + this.trainId = trainId; + this.allStops = allStops; + } + + public boolean isEmpty() { + return routeStops.isEmpty(); + } + + public TrainStop getFirstStop() { + return routeStops.get(0); + } + + public TrainStop getLastStop() { + return routeStops.get(routeStops.size() - 1); + } + + public UUID getSessionId() { + return sessionId; + } + + public UUID getTrainId() { + return trainId; + } + + public boolean isCancelled() { + return cancelled; + } + + public List getStopovers() { + return routeStops.size() <= 2 ? List.of() : ImmutableList.copyOf(routeStops.subList(1, routeStops.size() - 1)); + } + + public ImmutableList getAllStops() { + return ImmutableList.copyOf(routeStops); + } + + public ImmutableList getAllJourneyStops() { + return ImmutableList.copyOf(allStops); + } + + public long departureIn() { + return getFirstStop().getScheduledDepartureTime() - DragonLib.getCurrentWorldTime(); + } + + public long arrivalIn() { + return getFirstStop().getScheduledArrivalTime() - DragonLib.getCurrentWorldTime(); + } + + public long travelTime() { + return getLastStop().getScheduledArrivalTime() - getFirstStop().getScheduledDepartureTime(); + } + + public long timeUntilEnd() { + return departureIn() + travelTime(); + } + + public Set getStatus() { + return ImmutableSet.copyOf(status); + } + + private static Set> getBetween(TrainSchedule schedule, StationTag start, StationTag end, UserSettings settings) { + List stops = schedule.getAllStops().stream().sorted((a, b) -> Long.compare(a.getScheduledDepartureTime(), b.getScheduledDepartureTime())).toList(); + + if (stops.stream().noneMatch(x -> x.getTag().equals(start)) || stops.stream().noneMatch(x -> x.getTag().equals(end))) { + return Set.of(); + } + + // Step 1: Select ALL possible pairs of start and end indices + Set> sections = new HashSet<>(); + int firstStartIndex = -1; + int startIndex = -1; + + for (int i = 0, index = 0; i < stops.size() + (firstStartIndex < 0 ? stops.size() : firstStartIndex); i++, index = i % stops.size()) { + TrainStop stop = stops.get(index); + if (stop.getTag().equals(start)) { + startIndex = index; + if (firstStartIndex < 0) firstStartIndex = index; + } + if (startIndex >= 0 && stop.getTag().equals(end)) { + sections.add(new Pair<>(startIndex, index)); + startIndex = -1; + } + } + + if (sections.isEmpty()) { + return Set.of(); + } + + // Step 2: Gather all stations between those indices. + Set> routeParts = new HashSet<>(); + for (Pair startStopPair : sections) { + if (GlobalSettings.getInstance().isTrainStationExcludedByUser(schedule.getTrain(), stops.get(startStopPair.getFirst()), settings) || GlobalSettings.getInstance().isTrainStationExcludedByUser(schedule.getTrain(), stops.get(startStopPair.getSecond()), settings)) { + continue; + } + + List stopovers = new ArrayList<>(); + + int index = startStopPair.getFirst() - 1; + do { + index++; + TrainStop stop = stops.get(index % stops.size()); + stop.simulateCycles(index / stops.size()); + stopovers.add(stop); + } while (index % stops.size() != startStopPair.getSecond()); + + routeParts.add(stopovers); + } + return routeParts; + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putUUID(NBT_SESSION_ID, sessionId); + nbt.putUUID(NBT_TRAIN_ID, trainId); + ListTag stopsList = new ListTag(); + stopsList.addAll(routeStops.stream().map(x -> x.toNbt(true)).toList()); + nbt.put(NBT_STOPS, stopsList); + ListTag journeyList = new ListTag(); + journeyList.addAll(allStops.stream().map(x -> x.toNbt(true)).toList()); + nbt.put(NBT_JOURNEY, journeyList); + return nbt; + } + + public static RoutePart fromNbt(CompoundTag nbt) { + return new RoutePart( + nbt.contains(NBT_SESSION_ID) ? nbt.getUUID(NBT_SESSION_ID) : new UUID(0, 0), + nbt.getUUID(NBT_TRAIN_ID), + nbt.getList(NBT_STOPS, Tag.TAG_COMPOUND).stream().map(x -> TrainStop.fromNbt((CompoundTag)x)).toList(), + nbt.getList(NBT_JOURNEY, Tag.TAG_COMPOUND).stream().map(x -> TrainStop.fromNbt((CompoundTag)x)).toList() + ); + } + + @Override + public int compareTo(RoutePart o) { + return Long.compare(departureIn(), o.departureIn()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java new file mode 100644 index 00000000..43b8c06a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java @@ -0,0 +1,89 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.HashSet; + +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.mcdragonlib.data.Cache; + +public class TrainSchedule { + private final UUID sessionId; + private final Train train; + private final List stops; + private final Cache> stopsChronologically = new Cache<>(() -> getAllStops().stream().sorted((a, b) -> Long.compare(a.getScheduledArrivalTime(), b.getScheduledArrivalTime())).toList()); + + private boolean simulated; + private long simulationTime; + + private TrainSchedule(UUID sessionId, Train train, List stops) { + this.sessionId = sessionId; + this.train = train; + this.stops = stops; + } + + public TrainSchedule(UUID sessionId, Train train) { + this(TrainListener.data.get(train.id).getSessionId(), train, TrainListener.data.get(train.id).getPredictions().stream().map(x -> new TrainStop(x)).toList()); + } + + public static TrainSchedule empty() { + return new TrainSchedule(new UUID(0, 0), null, List.of()); + } + + public static TrainSchedule ofSectionForIndex(UUID sessionId, Train train, int stationSectionIndex, int targetStationIndex, long simulationTime) { + return new TrainSchedule(sessionId, train, TrainListener.data.get(train.id).getSectionForIndex(stationSectionIndex).getAllStops(simulationTime, targetStationIndex)); + } + + public List getAllStops() { + return stops; + } + + public List getAllStopsChronologically() { + return stopsChronologically.get(); + } + + public UUID getSessionId() { + return sessionId; + } + + public Train getTrain() { + return train; + } + + public boolean stopsAt(StationTag tag) { + return stops.stream().anyMatch(x -> x.getTag().equals(tag)); + } + + public TrainSchedule simulate(long ticks) { + simulated = true; + simulationTime += ticks; + return new TrainSchedule(sessionId, train, stops.stream().map(x -> x.copy()).peek(x -> x.simulateTicks(ticks)).toList()); + } + + public boolean isSimulated() { + return simulated; + } + + public long getSimulationTime() { + return simulationTime; + } + + public boolean isEqual(TrainSchedule other) { + if (other == null) { + return false; + } + if (getAllStops().size() != other.getAllStops().size()) { + return false; + } + Set tagsA = new HashSet<>(); + Set tagsB = new HashSet<>(); + getAllStops().stream().forEach(x -> tagsA.add(x.getTag().getTagName().get())); + other.getAllStops().stream().forEach(x -> tagsB.add(x.getTag().getTagName().get())); + return tagsA.size() == tagsB.size() && tagsA.stream().allMatch(x -> tagsB.contains(x)); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/TransferConnection.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/TransferConnection.java new file mode 100644 index 00000000..732410f3 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/TransferConnection.java @@ -0,0 +1,110 @@ +package de.mrjulsen.crn.data.navigation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableList; + +import de.mrjulsen.crn.data.train.TrainState; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.util.IListenable; + +public class TransferConnection implements AutoCloseable, IListenable { + + public static final String EVENT_CONNECTION_ENDANGERED = "connection_endangered"; + public static final String EVENT_CONNECTION_MISSED = "connection_missed"; + private final Map>> listeners = new HashMap<>(); + + private final TrainStop arrival; + private final TrainStop departure; + + private boolean isPossible = true; + + private boolean wasConnectionEndangered = false; + private boolean wasConnectionMissed = false; + + public TransferConnection(TrainStop arrival, TrainStop departure) { + this.arrival = arrival; + this.departure = departure; + + createEvent(EVENT_CONNECTION_ENDANGERED); + createEvent(EVENT_CONNECTION_MISSED); + } + + public static ImmutableList getConnections(List parts) { + List connections = new ArrayList<>(); + for (int i = 0; i < parts.size() - 1; i++) { + connections.add(new TransferConnection(parts.get(i).getLastStop(), parts.get(i + 1).getFirstStop())); + } + return ImmutableList.copyOf(connections); + } + + public TrainStop getArrivalStation() { + return arrival; + } + + public TrainStop getDepartureStation() { + return departure; + } + + public long getScheduledTransferTime() { + return departure.getScheduledDepartureTime() - arrival.getScheduledArrivalTime(); + } + + public long getRealTimeTransferTime() { + return (departure.shouldRenderRealTime() ? departure.getRealTimeDepartureTime() : departure.getScheduledDepartureTime()) - (arrival.shouldRenderRealTime() ? arrival.getRealTimeArrivalTime() : arrival.getScheduledArrivalTime()); + } + + public boolean isPossible() { + return isPossible; + } + + public boolean isConnectionEndangered() { + return !isPossible() || getRealTimeTransferTime() <= 0; + } + + public boolean isConnectionMissed() { + if (!isPossible) { + return true; + } + + boolean possible = !(arrival.getState() == TrainState.BEFORE && departure.getState() == TrainState.AFTER); + if (!possible) { + isPossible = false; + } + return !possible; + } + + @Override + public Map>> getListeners() { + return listeners; + } + + public void update() { + if (!isPossible()) { + return; + } + + boolean endangered = isConnectionEndangered(); + boolean missed = isConnectionMissed(); + + if (endangered != wasConnectionEndangered) { + notifyListeners(EVENT_CONNECTION_ENDANGERED, this); + } + if (missed != wasConnectionMissed) { + notifyListeners(EVENT_CONNECTION_MISSED, this); + } + + wasConnectionEndangered = endangered; + wasConnectionMissed = missed; + } + + @Override + public void close() { + stopListeningAll(this); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java new file mode 100644 index 00000000..b8889efc --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java @@ -0,0 +1,147 @@ +package de.mrjulsen.crn.data.schedule.condition; + +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.condition.ScheduledDelay; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import com.simibubi.create.foundation.utility.Components; +import com.simibubi.create.foundation.utility.Lang; +import com.simibubi.create.foundation.utility.Pair; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.ResizableButton; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainPrediction; +import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +public class DynamicDelayCondition extends ScheduledDelay { + + private static final String NBT_MIN = "Min"; + + public DynamicDelayCondition() { + super(); + data.putInt(NBT_MIN, 5); + } + + @Override + public Pair getSummary() { + return Pair.of(new ItemStack(Items.COMPARATOR), TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.condition." + getId().getPath() + ".title", formatCustomTime(getMinValue(), true), formatTime(true))); + } + + protected Component formatCustomTime(int time, boolean compact) { + if (compact) + return Components.literal(time + getUnit().suffix); + return Components.literal(time + " ").append(Lang.translateDirect(getUnit().key)); + } + + @Override + public List getTitleAs(String type) { + return ImmutableList.of( + TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath()), + Lang.translateDirect("schedule.condition.for_x_time", formatTime(false)).withStyle(ChatFormatting.DARK_AQUA), + TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".at_least", formatCustomTime(getMinValue(), false)).withStyle(ChatFormatting.DARK_AQUA) + ); + } + + @Override + public boolean tickCompletion(Level level, Train train, CompoundTag context) { + int time = context.getInt("Time"); + + long currentDelay = 0; + long scheduledDepartureTime = 0; + boolean initialized = false; + + if (TrainListener.data.containsKey(train.id)) { + TrainData data = TrainListener.data.get(train.id); + Optional pred = data.getNextStopPrediction(); + if (pred.isPresent()) { + currentDelay = pred.get().getArrivalTimeDeviation(); + initialized = data.isInitialized() && !data.isPreparing(); + scheduledDepartureTime = pred.get().getScheduledDepartureTime(); + } + } + + if (time >= (initialized ? Math.max(totalWaitTicks() - currentDelay, minWaitTicks()) : totalWaitTicks()) && (!initialized || DragonLib.getCurrentWorldTime() >= scheduledDepartureTime)) + return true; + + context.putInt("Time", time + 1); + requestDisplayIfNecessary(context, time); + return false; + } + + @Override + public ResourceLocation getId() { + return new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "dynamic_delay"); + } + + public int getMinValue() { + return intData(NBT_MIN); + } + + public int minWaitTicks() { + return getMinValue() * getUnit().ticksPer; + } + + @Override + @Environment(EnvType.CLIENT) + public void initConfigurationWidgets(ModularGuiLineBuilder builder) { + builder.addScrollInput(0, 26, (i, l) -> { + i.titled(Lang.translateDirect("generic.duration")) + .withShiftStep(15) + .withRange(0, 121); + i.lockedTooltipX = -15; + i.lockedTooltipY = 35; + }, "Value"); + + builder.addScrollInput(26, 26, (i, l) -> { + i.titled(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.condition." + getId().getPath() + ".min_duration")) + .withShiftStep(15) + .withRange(0, 121); + i.lockedTooltipX = -15; + i.lockedTooltipY = 35; + }, NBT_MIN); + + builder.addSelectionScrollInput(52, 58, (i, l) -> { + i.forOptions(TimeUnit.translatedOptions()) + .titled(Lang.translateDirect("generic.timeUnit")); + }, "TimeUnit"); + + + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + ResizableButton btn = new ResizableButton(accessor.crn$getX() + 110, accessor.crn$getY() - 4, 16, 16, TextUtils.empty(), + (b) -> { + Util.getPlatform().openUri(Constants.HELP_PAGE_DYNAMIC_DELAYS); + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + ModGuiIcons.HELP.render(graphics, x, y); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ICustomSuggestionsInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ICustomSuggestionsInstruction.java new file mode 100644 index 00000000..406f3579 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ICustomSuggestionsInstruction.java @@ -0,0 +1,10 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; + +import de.mrjulsen.crn.data.train.TrainData; + +public interface ICustomSuggestionsInstruction { + void run(ScheduleRuntime runtime, TrainData data, Train train, int currentScheduleIndex); +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IPredictableInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IPredictableInstruction.java new file mode 100644 index 00000000..b52a99e6 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IPredictableInstruction.java @@ -0,0 +1,11 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; + +import de.mrjulsen.crn.data.train.TrainData; + +@FunctionalInterface +public interface IPredictableInstruction { + void predict(TrainData data, ScheduleRuntime runtime, int indexInSchedule, Train train); +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationPredictableInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationPredictableInstruction.java new file mode 100644 index 00000000..52508254 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationPredictableInstruction.java @@ -0,0 +1,12 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; + +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainPrediction; + +@FunctionalInterface +public interface IStationPredictableInstruction { + void predictForStation(TrainData data, TrainPrediction prediction, ScheduleRuntime runtime, int indexInSchedule, Train train); +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationTagInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationTagInstruction.java new file mode 100644 index 00000000..3086ddd5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/IStationTagInstruction.java @@ -0,0 +1,3 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +public interface IStationTagInstruction extends ICustomSuggestionsInstruction {} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ITrainNameInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ITrainNameInstruction.java new file mode 100644 index 00000000..86cc72bb --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ITrainNameInstruction.java @@ -0,0 +1,3 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +public interface ITrainNameInstruction extends ICustomSuggestionsInstruction {} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java new file mode 100644 index 00000000..ca894572 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java @@ -0,0 +1,88 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +import java.util.List; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; +import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import com.simibubi.create.foundation.utility.Pair; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.ResizableButton; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Blocks; + +public class ResetTimingsInstruction extends ScheduleInstruction implements IStationTagInstruction, IPredictableInstruction { + + + public ResetTimingsInstruction() { + } + + @Override + public Pair getSummary() { + return Pair.of(new ItemStack(Blocks.BARRIER), TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction." + getId().getPath()).withStyle(ChatFormatting.AQUA)); + } + + @Override + public ResourceLocation getId() { + return new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "reset_timings"); + } + + @Override + public boolean supportsConditions() { + return false; + } + + @Override + public List getTitleAs(String type) { + return List.of(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath()).withStyle(ChatFormatting.GOLD)); + } + + @Override + @Environment(EnvType.CLIENT) + public void initConfigurationWidgets(ModularGuiLineBuilder builder) { + + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 16, 16, TextUtils.empty(), + (b) -> { + Util.getPlatform().openUri(Constants.HELP_PAGE_SCHEDULED_TIMES_AND_REAL_TIME); + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + ModGuiIcons.HELP.render(graphics, x, y); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); + } + + @Override + public void run(ScheduleRuntime runtime, TrainData data, Train train, int index) { + DLUtils.doIfNotNull(data, x -> { + data.resetPredictions(); + }); + } + + @Override + public void predict(TrainData data, ScheduleRuntime runtime, int indexInSchedule, Train train) { + } +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java new file mode 100644 index 00000000..86fd2467 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java @@ -0,0 +1,140 @@ +package de.mrjulsen.crn.data.schedule.instruction; + +import java.util.ArrayList; +import java.util.List; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; +import com.simibubi.create.content.trains.schedule.ScheduleScreen; +import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import com.simibubi.create.foundation.utility.Pair; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.screen.TrainSectionSettingsScreen; +import de.mrjulsen.crn.client.gui.widgets.ResizableButton; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; +import de.mrjulsen.crn.mixin.ScheduleScreenAccessor; +import de.mrjulsen.crn.registry.ModBlocks; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +public class TravelSectionInstruction extends ScheduleInstruction implements IStationTagInstruction, IPredictableInstruction { + + public static final String NBT_TRAIN_GROUP = "TrainGroup"; + public static final String NBT_TRAIN_LINE = "TrainLine"; + public static final String NBT_INCLUDE_PREVIOUS_STATION = "IncludePreviousStation"; + public static final String NBT_USABLE = "Usable"; + + public TravelSectionInstruction() { + } + + @Override + protected void readAdditional(CompoundTag tag) { + super.readAdditional(tag); + if (!tag.contains(NBT_TRAIN_GROUP)) tag.putString(NBT_TRAIN_GROUP, ""); + if (!tag.contains(NBT_TRAIN_LINE)) tag.putString(NBT_TRAIN_LINE, ""); + if (!tag.contains(NBT_INCLUDE_PREVIOUS_STATION)) tag.putBoolean(NBT_INCLUDE_PREVIOUS_STATION, false); + if (!tag.contains(NBT_USABLE)) tag.putBoolean(NBT_USABLE, true); + } + + @Override + public Pair getSummary() { + return Pair.of(new ItemStack(ModBlocks.ADVANCED_DISPLAY.get()), TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction." + getId().getPath()).withStyle(ChatFormatting.AQUA)); + } + + @Override + public ResourceLocation getId() { + return new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "travel_section"); + } + + @Override + public boolean supportsConditions() { + return false; + } + + @Override + public List getTitleAs(String type) { + String noneText = TextUtils.translate("gui.createrailwaysnavigator.section_settings.none").getString(); + List lines = new ArrayList<>(); + lines.add(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath()).withStyle(ChatFormatting.GOLD)); + lines.add(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".description").withStyle(ChatFormatting.GRAY)); + if (data.contains(NBT_TRAIN_GROUP)) lines.add(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".train_group").withStyle(ChatFormatting.DARK_AQUA).append(TextUtils.text(data.getString(NBT_TRAIN_GROUP).isBlank() ? noneText : data.getString(NBT_TRAIN_GROUP).toString()).withStyle(ChatFormatting.WHITE))); + if (data.contains(NBT_TRAIN_LINE)) lines.add(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".train_line").withStyle(ChatFormatting.DARK_AQUA).append(TextUtils.text(data.getString(NBT_TRAIN_LINE).isBlank() ? noneText : data.getString(NBT_TRAIN_LINE).toString()).withStyle(ChatFormatting.WHITE))); + if (data.contains(NBT_INCLUDE_PREVIOUS_STATION)) lines.add(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".include_previous_station").withStyle(ChatFormatting.DARK_AQUA).append((data.getBoolean(NBT_INCLUDE_PREVIOUS_STATION) ? CommonComponents.GUI_YES : CommonComponents.GUI_NO))); + if (data.contains(NBT_USABLE)) lines.add(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".usable").withStyle(ChatFormatting.DARK_AQUA).append((data.getBoolean(NBT_USABLE) ? CommonComponents.GUI_YES : CommonComponents.GUI_NO))); + return lines; + } + + /** HERE BE DRAGONS! This code is very illegal, but it works... */ + @Override + @Environment(EnvType.CLIENT) + @SuppressWarnings("resource") + public void initConfigurationWidgets(ModularGuiLineBuilder builder) { + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + + ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 121, 16, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction." + getId().getPath() + ".configure"), + (b) -> { + if (Minecraft.getInstance().screen instanceof ScheduleScreen scheduleScreen) { + ((ScheduleScreenAccessor)scheduleScreen).crn$getOnEditorClose().accept(true); + builder.customArea(0, 0).speechBubble(); + Minecraft.getInstance().setScreen(new TrainSectionSettingsScreen(scheduleScreen, data)); + } + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + int j = isActive() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x + width / 2, y + (height - 8) / 2, this.getMessage(), j, EAlignment.CENTER, true); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "config_btn")); + } + + @Override + public void run(ScheduleRuntime runtime, TrainData data, Train train, int index) { + DLUtils.doIfNotNull(data, x -> { + x.addTravelSection(getSectionData(x, index)); + x.changeCurrentSection(index); + }); + } + + private TrainTravelSection getSectionData(TrainData data, int index) { + return new TrainTravelSection( + data, + index, + GlobalSettings.getInstance().getTrainGroup(this.data.getString(NBT_TRAIN_GROUP)).orElse(null), + GlobalSettings.getInstance().getTrainLine(this.data.getString(NBT_TRAIN_LINE)).orElse(null), + this.data.getBoolean(NBT_INCLUDE_PREVIOUS_STATION), + this.data.getBoolean(NBT_USABLE) + ); + } + + @Override + public void predict(TrainData data, ScheduleRuntime runtime, int indexInSchedule, Train train) { + DLUtils.doIfNotNull(data, x -> { + x.addTravelSection(getSectionData(x, indexInSchedule)); + }); + } +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java b/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java new file mode 100644 index 00000000..91113968 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java @@ -0,0 +1,496 @@ +package de.mrjulsen.crn.data.storage; + +import java.io.File; +import java.io.IOException; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.ArrayList; + +import com.google.common.collect.ImmutableList; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.station.GlobalStation; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Map; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TagName; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainPrediction; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.mcdragonlib.data.INBTSerializable; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +public class GlobalSettings implements INBTSerializable { + + /** @deprecated For data migration only. Use {@code FILENAME} instead. */ + @Deprecated + public static final String LEGACY_FILENAME = "createrailwaysnavigator_global_settings.dat"; + + public static final String FILENAME = CreateRailwaysNavigator.MOD_ID + "_global_settings.nbt"; + + public static final int DATA_VERSION = 1; + + private static final String NBT_VERSION = "Version"; + private static final String NBT_STATION_TAGS = "StationTags"; + private static final String NBT_TRAIN_GROUPS = "TrainGroups"; + private static final String NBT_STATION_BLACKLIST = "StationBlacklist"; + private static final String NBT_TRAIN_BLACKLIST = "TrainBlacklist"; + private static final String NBT_TRAIN_LINES = "TrainLines"; + + private final MinecraftServer server; + + private final Map stationTags = new HashMap<>(); + private final Map trainGroups = new HashMap<>(); + private final Set stationBlacklist = new HashSet<>(); + private final Set trainBlacklist = new HashSet<>(); + private final Map trainLines = new HashMap<>(); + + private static GlobalSettings instance; + + + private GlobalSettings(MinecraftServer server) { + this.server = server; + } + + public synchronized static GlobalSettings getInstance() { + if (instance == null) { + try { + instance = GlobalSettings.open(ModCommonEvents.getCurrentServer().get()); + } catch (IOException e) { + CreateRailwaysNavigator.LOGGER.error("Unable to open settings file.", e); + instance = new GlobalSettings(ModCommonEvents.getCurrentServer().get()); + } + } + return instance; + } + + public static boolean hasInstance() { + return instance != null; + } + + public static void clearInstance() { + if (instance != null) { + instance.close(); + } + instance = null; + } + + public synchronized void save() { + CompoundTag nbt = this.serializeNbt(); + + try { + NbtIo.writeCompressed(nbt, new File(server.getWorldPath(new LevelResource("data/" + FILENAME)).toString())); + CreateRailwaysNavigator.LOGGER.info("Saved global settings."); + } catch (IOException e) { + CreateRailwaysNavigator.LOGGER.error("Unable to save global settings.", e); + } + } + + public synchronized static GlobalSettings open(MinecraftServer server) throws IOException { + File legacyFile = new File(server.getWorldPath(new LevelResource("data/" + LEGACY_FILENAME)).toString()); + File settingsFile = new File(server.getWorldPath(new LevelResource("data/" + FILENAME)).toString()); + + GlobalSettings file = new GlobalSettings(server); + + if (legacyFile.exists()) { + CreateRailwaysNavigator.LOGGER.warn("A legacy global settings file was found. Try to load it."); + file.deserializeNbtLegacy(NbtIo.readCompressed(legacyFile).getCompound("data")); + legacyFile.delete(); + } else if (settingsFile.exists()) { + file.deserializeNbt(NbtIo.readCompressed(settingsFile)); + } + return file; + } + + public CompoundTag serializeNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putInt(NBT_VERSION, DATA_VERSION); + + CompoundTag stationsComp = new CompoundTag(); + this.stationTags.entrySet().forEach(x -> stationsComp.put(x.getKey().toString(), x.getValue().toNbt())); + nbt.put(NBT_STATION_TAGS, stationsComp); + + CompoundTag trainGroupComp = new CompoundTag(); + this.trainGroups.entrySet().forEach(x -> trainGroupComp.put(x.getKey().toString(), x.getValue().toNbt())); + nbt.put(NBT_TRAIN_GROUPS, trainGroupComp); + + ListTag stationsBlacklist = new ListTag(); + this.stationBlacklist.stream().forEach(x -> stationsBlacklist.add(StringTag.valueOf(x))); + nbt.put(NBT_STATION_BLACKLIST, stationsBlacklist); + + ListTag trainsBlacklist = new ListTag(); + this.trainBlacklist.stream().forEach(x -> trainsBlacklist.add(StringTag.valueOf(x))); + nbt.put(NBT_TRAIN_BLACKLIST, trainsBlacklist); + + CompoundTag trainLinesComp = new CompoundTag(); + this.trainLines.entrySet().forEach(x -> trainLinesComp.put(x.getKey().toString(), x.getValue().toNbt())); + nbt.put(NBT_TRAIN_LINES, trainLinesComp); + + return nbt; + } + + public void deserializeNbt(CompoundTag nbt) { + @SuppressWarnings("unused") int version = nbt.getInt(NBT_VERSION); + + CompoundTag stationsComp = nbt.getCompound(NBT_STATION_TAGS); + this.stationTags.putAll(stationsComp.getAllKeys().stream().map(x -> StationTag.fromNbt(stationsComp.getCompound(x), UUID.fromString(x))).collect(Collectors.toMap(x -> x.getId(), x -> x))); + CompoundTag trainGroupsComp = nbt.getCompound(NBT_TRAIN_GROUPS); + this.trainGroups.putAll(trainGroupsComp.getAllKeys().stream().map(x -> TrainGroup.fromNbt(trainGroupsComp.getCompound(x))).collect(Collectors.toMap(x -> x.getGroupName(), x -> x))); + this.stationBlacklist.addAll(nbt.getList(NBT_STATION_BLACKLIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList()); + this.trainBlacklist.addAll(nbt.getList(NBT_TRAIN_BLACKLIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList()); + CompoundTag trainLinesComp = nbt.getCompound(NBT_TRAIN_LINES); + this.trainLines.putAll(trainLinesComp.getAllKeys().stream().map(x -> TrainLine.fromNbt(trainLinesComp.getCompound(x))).collect(Collectors.toMap(x -> x.getLineName(), x -> x))); + + } + + /** + * @deprecated For data migration only. Use {@code deserializeNbt} instead. + */ + @Deprecated + public void deserializeNbtLegacy(CompoundTag nbt) { + final String NBT_ALIAS_REGISTRY = "RegisteredAliasData"; + final String NBT_BLACKLIST = "StationBlacklist"; + final String NBT_TRAIN_BLACKLIST = "TrainBlacklist"; + final String NBT_TRAIN_GROUP_REGISTRY = "RegisteredTrainGroups"; + + Collection aliasData = new ArrayList<>(); + Collection trainGroupData = new ArrayList<>(); + Collection blacklistData = new ArrayList<>(); + Collection trainBlacklistData = new ArrayList<>(); + + if (nbt.contains(NBT_ALIAS_REGISTRY)) { + aliasData = nbt.getList(NBT_ALIAS_REGISTRY, Tag.TAG_COMPOUND).stream().map(x -> (CompoundTag)x).toList(); + } + + if (nbt.contains(NBT_TRAIN_GROUP_REGISTRY)) { + trainGroupData = nbt.getList(NBT_TRAIN_GROUP_REGISTRY, Tag.TAG_COMPOUND).stream().map(x -> (CompoundTag)x).toList(); + } + + if (nbt.contains(NBT_BLACKLIST)) { + blacklistData = nbt.getList(NBT_BLACKLIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList(); + } + + if (nbt.contains(NBT_TRAIN_BLACKLIST)) { + trainBlacklistData = nbt.getList(NBT_TRAIN_BLACKLIST, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList(); + } + + Set usedIds = new LinkedHashSet<>(); + stationTags.putAll(aliasData.stream().map(x -> { + UUID id; + do { + id = UUID.randomUUID(); + } while (usedIds.contains(id)); + usedIds.add(id); + return StationTag.fromNbt(x, id); + }).collect(Collectors.toMap(x -> x.getId(), x -> x))); + usedIds.clear(); + trainGroups.putAll(trainGroupData.stream().map(x -> TrainGroup.fromNbt(x)).collect(Collectors.toMap(x -> x.getGroupName(), x -> x))); + stationBlacklist.addAll(blacklistData); + trainBlacklist.addAll(trainBlacklistData); + + save(); + } + + public void close() { + this.save(); + } + +//#region +++ STATION TAGS +++ + + public boolean hasStationTag(GlobalStation station) { + return hasStationTag(station.name); + } + + public boolean hasStationTag(String stationName) { + return stationTags.values().stream().anyMatch(x -> x.contains(stationName)); + } + + public boolean stationTagExists(String tagName) { + return stationTagExists(TagName.of(tagName)); + } + + public boolean stationTagExists(TagName tagName) { + return stationTags.values().stream().anyMatch(x -> x.getTagName().equals(tagName)); + } + + public boolean stationTagExists(UUID id) { + return stationTags.containsKey(id); + } + + public StationTag getOrCreateStationTagFor(GlobalStation station) { + return getOrCreateStationTagFor(station.name); + } + + public StationTag getOrCreateStationTagFor(TagName tagName) { + return getTagByName(tagName).orElse(getOrCreateStationTagFor(tagName.get())); + } + + /** + * @param stationName The name of the train station. + * @return Returns the station tag for the given train station. + */ + public StationTag getOrCreateStationTagFor(String stationName) { + if (stationName.contains("*")) { + return getOrCreateTagForWildcard(stationName); + } + + Optional a = stationTags.values().stream().filter(x -> x.contains(stationName)).findFirst(); + if (a.isPresent()) { + return a.get(); + } + + return new StationTag(null, TagName.of(stationName), Map.of(stationName, StationInfo.empty())); + } + + private StationTag getOrCreateTagForWildcard(String stationName) { + String regex = stationName.isBlank() ? stationName : "\\Q" + stationName.replace("*", "\\E.*\\Q") + "\\E"; + Optional a = stationTags.values().stream().filter(x -> x.getAllStationNames().stream().anyMatch(y -> y.matches(regex))).findFirst(); + if (a.isPresent()) { + return a.get(); + } + + return new StationTag(null, TagName.of(stationName), Map.of(stationName, StationInfo.empty())); + } + + /** + * Get the station tag with the given name or create and register a new one, if no tag exists. + * @param name The name of the station tag. + * @return The station tag for the name. + */ + public StationTag createOrGetStationTag(String name) { + return createOrGetStationTag(TagName.of(name)); + } + + /** + * Get the station tag with the given name or create and register a new one, if no tag exists. + * @param name The name of the station tag. + * @return The station tag for the name. + */ + public StationTag createOrGetStationTag(TagName name) { + Optional tag = getTagByName(name); + if (tag.isPresent()) { + return tag.get(); + } + UUID newId; + do { + newId = UUID.randomUUID(); + } while (stationTags.containsKey(newId)); + StationTag newTag = new StationTag(newId, name); + stationTags.put(newId, newTag); + return newTag; + } + + public StationTag registerStationTag(StationTag tag) { + UUID newId; + do { + newId = UUID.randomUUID(); + } while (stationTags.containsKey(newId)); + tag.setId(newId); + stationTags.put(newId, tag); + return tag; + } + + public Optional getTagByName(TagName name) { + return stationTags.values().stream().filter(x -> x.getTagName().equals(name)).findFirst(); + } + + public Optional getStationTag(UUID id) { + return Optional.ofNullable(stationTagExists(id) ? stationTags.get(id) : null); + } + + public boolean removeStationTag(String name) { + return removeStationTag(TagName.of(name)); + } + + public boolean removeStationTag(TagName name) { + return stationTags.values().removeIf(x -> x.getTagName().equals(name)); + } + + public StationTag removeStationTag(UUID id) { + return stationTags.remove(id); + } + + public ImmutableList getAllStationTags() { + return ImmutableList.copyOf(stationTags.values()); + } + +//#endregion +//#region +++ TRAIN GROUPS +++ + + public boolean trainGroupExists(String name) { + return trainGroups.containsKey(name); + } + + /** + * Get the train group with the given name or create and register a new one, if no group exists. + * @param name The name of the train group. + * @return The train group for the name. + */ + public TrainGroup createOrGetTrainGroup(String name) { + Optional tag = getTrainGroup(name); + if (tag.isPresent()) { + return tag.get(); + } + TrainGroup newGroup = new TrainGroup(name); + trainGroups.put(name, newGroup); + return newGroup; + } + + public Optional getTrainGroup(String name) { + return Optional.ofNullable(trainGroupExists(name) ? trainGroups.get(name) : null); + } + + public TrainGroup removeTrainGroup(String name) { + return trainGroups.remove(name); + } + + public ImmutableList getAllTrainGroups() { + return ImmutableList.copyOf(trainGroups.values()); + } + + public boolean isTrainExcludedByUser(Train train, UserSettings settings) { + return !TrainListener.data.get(train.id).getSections().isEmpty() && TrainListener.data.get(train.id).getSections().stream().allMatch(x -> !x.isUsable() || (x.getTrainGroup() != null && settings.navigationExcludedTrainGroups.getValue().contains(x.getTrainGroup().getGroupName()))); + //List groupsOfTrain = getTrainGroupsOfTrain(train); + //Set excludedGroups = settings.navigationExcludedTrainGroups.getValue(); + //return !groupsOfTrain.isEmpty() && !excludedGroups.isEmpty() && groupsOfTrain.stream().allMatch(a -> excludedGroups.stream().anyMatch(b -> a.getId().equals(b.getId()))); + } + + public boolean isTrainStationExcludedByUser(Train train, TrainPrediction at, UserSettings settings) { + return at.getSection().getTrainGroup() != null && (!at.getSection().isUsable() || (at.getSection().getTrainGroup() != null && settings.navigationExcludedTrainGroups.getValue().contains(at.getSection().getTrainGroup().getGroupName()))); + } + + public boolean isTrainStationExcludedByUser(Train train, TrainStop at, UserSettings settings) { + TrainTravelSection section = TrainListener.data.get(train.id).getSectionByIndex(at.getSectionIndex()); + return section.getTrainGroup() != null && (!section.isUsable() || (section.getTrainGroup() != null && settings.navigationExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))); + } + +//#endregion +//#region +++ STATION BLACKLIST +++ + + public boolean isStationBlacklisted(GlobalStation station) { + return isStationBlacklisted(station.name); + } + + public boolean isStationBlacklisted(String name) { + return stationBlacklist.contains(name); + } + + public void blacklistStation(GlobalStation station) { + blacklistStation(station.name); + } + + public void blacklistStation(String stationName) { + stationBlacklist.add(stationName); + } + + public boolean removeStationFromBlacklist(GlobalStation station) { + return removeStationFromBlacklist(station.name); + } + + public boolean removeStationFromBlacklist(String stationName) { + return stationBlacklist.removeIf(x -> x.equals(stationName)); + } + + public boolean isEntireStationTagBlacklisted(StationTag tag) { + if (tag == null) { + return true; + } + return tag.getAllStationNames().stream().allMatch(x -> isStationBlacklisted(x)); + } + + public ImmutableList getAllBlacklistedStations() { + return ImmutableList.copyOf(stationBlacklist); + } + +//#endregion +//#region +++ TRAIN BLACKLIST +++ + + public boolean isTrainBlacklisted(Train train) { + return isTrainBlacklisted(train.name.getString()); + } + + public boolean isTrainBlacklisted(String trainName) { + return trainBlacklist.contains(trainName); + } + + public void blacklistTrain(Train train) { + blacklistTrain(train.name.getString()); + } + + public void blacklistTrain(String trainName) { + trainBlacklist.add(trainName); + } + + public boolean removeTrainFromBlacklist(Train train) { + return removeTrainFromBlacklist(train.name.getString()); + } + + public boolean removeTrainFromBlacklist(String trainName) { + return trainBlacklist.removeIf(x -> x.equals(trainName)); + } + + /* TODO + public boolean isEntireTrainGroupBlacklisted(TrainGroup tag) { + if (tag == null) { + return true; + } + return tag.getTrainNames().stream().allMatch(x -> isTrainBlacklisted(x)); + } + */ + + public ImmutableList getAllBlacklistedTrains() { + return ImmutableList.copyOf(trainBlacklist); + } + +//#endregion + +//#region +++ TRAIN LINES +++ + + public boolean trainLineExists(String name) { + return trainLines.containsKey(name); + } + + public TrainLine createOrGetTrainLine(String name) { + Optional tag = getTrainLine(name); + if (tag.isPresent()) { + return tag.get(); + } + TrainLine newGroup = new TrainLine(name); + trainLines.put(newGroup.getLineName(), newGroup); + return newGroup; + } + + public Optional getTrainLine(String name) { + return Optional.ofNullable(trainLineExists(name) ? trainLines.get(name) : null); + } + + public TrainLine removeTrainLine(String name) { + return trainLines.remove(name); + } + + public ImmutableList getAllTrainLines() { + return ImmutableList.copyOf(trainLines.values()); + } + +//#endregion + +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettingsClient.java b/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettingsClient.java new file mode 100644 index 00000000..c354d600 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettingsClient.java @@ -0,0 +1,124 @@ +package de.mrjulsen.crn.data.storage; + +import java.util.function.Consumer; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.Optional; + +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; + +/** + * Client access for global settings, which are only present on the server side. + */ +public class GlobalSettingsClient { + + public static void getStationTags(Consumer> result) { + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_STATION_TAGS, result); + } + + public static void getStationTag(String name, Consumer result) { + DataAccessor.getFromServer(name, ModAccessorTypes.GET_STATION_TAG, result); + } + + public static void createStationTag(String name, Consumer result) { + DataAccessor.getFromServer(name, ModAccessorTypes.CREATE_STATION_TAG, result); + } + + public static void registerNewStationTag(StationTag tag, Runnable callback) { + DataAccessor.getFromServer(tag, ModAccessorTypes.REGISTER_STATION_TAG, x -> callback.run()); + } + + public static void deleteStationTag(UUID tagId, Runnable callback) { + DataAccessor.getFromServer(tagId, ModAccessorTypes.DELETE_STATION_TAG, x -> callback.run()); + } + + public static record UpdateStationTagNameData(UUID tagId, String name) {} + public static void updateStationTagNameData(UUID tagId, String name, Runnable callback) { + DataAccessor.getFromServer(new UpdateStationTagNameData(tagId, name), ModAccessorTypes.UPDATE_STATION_TAG_NAME, x -> callback.run()); + } + + public static record AddStationTagEntryData(UUID tagId, String station, StationInfo info) {} + public static void addStationTagEntry(UUID tagId, String station, StationInfo info, Consumer> callback) { + DataAccessor.getFromServer(new AddStationTagEntryData(tagId, station, info), ModAccessorTypes.ADD_STATION_TAG_ENTRY, callback); + } + + public static void updateStationTagEntry(UUID tagId, String station, StationInfo info, Consumer> callback) { + DataAccessor.getFromServer(new AddStationTagEntryData(tagId, station, info), ModAccessorTypes.UPDATE_STATION_TAG_ENTRY, callback); + } + + public static record RemoveStationTagEntryData(UUID tagId, String station) {} + public static void removeStationTagEntry(UUID tagId, String station, Consumer> callback) { + DataAccessor.getFromServer(new RemoveStationTagEntryData(tagId, station), ModAccessorTypes.REMOVE_STATION_TAG_ENTRY, callback); + } + + + + public static void getTrainGroups(Consumer> result) { + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_TRAIN_GROUPS, result); + } + + public static void deleteTrainGroup(String name, Runnable callback) { + DataAccessor.getFromServer(name, ModAccessorTypes.DELETE_TRAIN_GROUP, x -> callback.run()); + } + + public static record UpdateTrainGroupColorData(String name, int color) {} + public static void updateTrainGroupColor(String name, int color, Runnable callback) { + DataAccessor.getFromServer(new UpdateTrainGroupColorData(name, color), ModAccessorTypes.UPDATE_TRAIN_GROUP_COLOR, x -> callback.run()); + } + + public static void createTrainGroup(String name, Consumer result) { + DataAccessor.getFromServer(name, ModAccessorTypes.CREATE_TRAIN_GROUP, result); + } + + + + public static void getBlacklistedStations(Consumer> result) { + DataAccessor.getFromServer(null, ModAccessorTypes.GET_BLACKLISTED_STATIONS, result); + } + + public static void addStationToBlacklist(String name, Consumer> result) { + DataAccessor.getFromServer(name, ModAccessorTypes.ADD_STATION_TO_BLACKLIST, result); + } + + public static void removeStationFromBlacklist(String name, Consumer> result) { + DataAccessor.getFromServer(name, ModAccessorTypes.REMOVE_STATION_FROM_BLACKLIST, result); + } + + + + public static void getBlacklistedTrains(Consumer> result) { + DataAccessor.getFromServer(null, ModAccessorTypes.GET_BLACKLISTED_TRAINS, result); + } + + public static void addTrainToBlacklist(String name, Consumer> result) { + DataAccessor.getFromServer(name, ModAccessorTypes.ADD_TRAIN_TO_BLACKLIST, result); + } + + public static void removeTrainFromBlacklist(String name, Consumer> result) { + DataAccessor.getFromServer(name, ModAccessorTypes.REMOVE_TRAIN_FROM_BLACKLIST, result); + } + + + + public static void getTrainLines(Consumer> result) { + DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_TRAIN_LINES, result); + } + + public static void deleteTrainLine(String lineId, Runnable callback) { + DataAccessor.getFromServer(lineId, ModAccessorTypes.DELETE_TRAIN_LINE, x -> callback.run()); + } + public static record UpdateTrainLineColorData(String name, int color) {} + public static void updateTrainLineColor(String name, int color, Runnable callback) { + DataAccessor.getFromServer(new UpdateTrainLineColorData(name, color), ModAccessorTypes.UPDATE_TRAIN_LINE_COLOR, x -> callback.run()); + } + + public static void createTrainLine(String name, Consumer result) { + DataAccessor.getFromServer(name, ModAccessorTypes.CREATE_TRAIN_LINE, result); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/ClientTrainStop.java b/common/src/main/java/de/mrjulsen/crn/data/train/ClientTrainStop.java new file mode 100644 index 00000000..6b367a38 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/ClientTrainStop.java @@ -0,0 +1,215 @@ +package de.mrjulsen.crn.data.train; + +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import com.simibubi.create.content.trains.entity.TrainIconType; + +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.StationTag.ClientStationTag; +import de.mrjulsen.crn.data.TrainInfo; +import de.mrjulsen.crn.data.navigation.ITrainListenerClient; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.util.IListenable; +import dev.architectury.platform.Platform; +import dev.architectury.utils.Env; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +/** + * A small variant of the {@code NewTrainPrediction} class, representing a stop of a train on its route with some important information. + */ +public class ClientTrainStop extends TrainStop implements ITrainListenerClient, IListenable { + + public static final String EVENT_UPDATE = "update"; + public static final String EVENT_DELAY = "delayed"; + public static final String EVENT_SCHEDULE_CHANGED = "schedule_changed"; + public static final String EVENT_STATION_CHANGED = "station_changed"; + public static final String EVENT_STATION_REACHED = "station_reached"; + public static final String EVENT_STATION_LEFT = "station_left"; + public static final String EVENT_ANNOUNCE_NEXT_STOP = "announce_next_stop"; + private final Map>> listeners = new HashMap<>(); + + private boolean isClosed = false; + + public ClientTrainStop(int scheduleIndex, int sectionIndex, UUID trainId, String trainName, TrainIconType trainIcon, TrainInfo trainInfo, + String scheduleTitle, boolean isCustomTitle, String terminusText, int stayDuration, boolean simulated, + long scheduledDepartureTime, long scheduledArrivalTime, int cycle, ClientStationTag tag, long realTimeArrivalTime, + long realTimeDepartureTime, int realTimeCycle, ClientStationTag realTimeTag, long arrivalTimeDeviation, + long departureTimeDeviation, int realTimeTicksUntilArrival, TrainState trainPosition) + { + super(scheduleIndex, sectionIndex, trainId, trainName, trainIcon, trainInfo, scheduleTitle, isCustomTitle, + terminusText, stayDuration, simulated, scheduledDepartureTime, scheduledArrivalTime, cycle, tag, + realTimeArrivalTime, realTimeDepartureTime, realTimeCycle, realTimeTag, arrivalTimeDeviation, + departureTimeDeviation, realTimeTicksUntilArrival, trainPosition); + initEvents(); + } + + private void initEvents() { + this.createEvent(EVENT_UPDATE); + this.createEvent(EVENT_DELAY); + this.createEvent(EVENT_SCHEDULE_CHANGED); + this.createEvent(EVENT_STATION_CHANGED); + this.createEvent(EVENT_STATION_REACHED); + this.createEvent(EVENT_STATION_LEFT); + this.createEvent(EVENT_ANNOUNCE_NEXT_STOP); + } + + @Override + public Map>> getListeners() { + return listeners; + } + + /** Client-side only! */ + public long getRoundedRealTimeArrivalTime() throws RuntimeSideException { + if (Platform.getEnvironment() != Env.CLIENT) { + throw new RuntimeSideException(true); + } + return (getScheduledArrivalTime() + getArrivalTimeDeviation()) / ModClientConfig.REALTIME_PRECISION_THRESHOLD.get() * ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(); + } + + /** Client-side only! */ + public long getRoundedRealTimeDepartureTime() throws RuntimeSideException { + if (Platform.getEnvironment() != Env.CLIENT) { + throw new RuntimeSideException(true); + } + return (getScheduledDepartureTime() + getDepartureTimeDeviation()) / ModClientConfig.REALTIME_PRECISION_THRESHOLD.get() * ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(); + } + + @Override + public void update(TrainStopRealTimeData data) { + if (isClosed) { + return; + } + + if (data.cycle() != getScheduledCycle()) { + if (data.cycle() > getScheduledCycle() && trainState != TrainState.AFTER) { + trainState = TrainState.AFTER; + notifyListeners(EVENT_STATION_LEFT, this); + close(); + } + return; + } + + boolean wasDelayed = isDepartureDelayed(); + String oldRealTimeStation = getRealTimeStationTag().stationName(); + int oldTimeUntilArrival = getTicksUntilArrival(); + + if (scheduledArrivalTime != data.scheduledArrivalTime() || scheduledDepartureTime != data.scheduledDepartureTime()) { + notifyListeners(EVENT_SCHEDULE_CHANGED, this); + } + + this.scheduledArrivalTime = data.scheduledArrivalTime(); + this.scheduledDepartureTime = data.scheduledDepartureTime(); + this.realTimeArrivalTime = data.realTimeArrivalTime(); + this.realTimeDepartureTime = data.realTimeDepartureTime(); + this.arrivalTimeDeviation = data.deltaArrivalTime(); + this.departureTimeDeviation = data.deltaDepartureTime(); + this.realTimeCycle = data.cycle(); + this.realTimeTag = data.station(); + this.realTimeTicksUntilArrival = data.ticksUntilArrival(); + + if (!wasDelayed && isAnyDelayed()) { + notifyListeners(EVENT_DELAY, this); + } + if (!oldRealTimeStation.equals(getRealTimeStationTag().stationName())) { + notifyListeners(EVENT_STATION_CHANGED, this); + } + if (trainState == TrainState.BEFORE && oldTimeUntilArrival > getTicksUntilArrival() && getTicksUntilArrival() <= ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) { + trainState = TrainState.ANNOUNCED; + notifyListeners(EVENT_ANNOUNCE_NEXT_STOP, this); + } + + if (trainState.getPositionMultiplier() < 0 && getTicksUntilArrival() <= 0) { + trainState = TrainState.STAYING; + notifyListeners(EVENT_STATION_REACHED, this); + } + + notifyListeners(EVENT_UPDATE, this); + } + + public static TrainStop fromNbt(CompoundTag nbt) { + return new ClientTrainStop( + nbt.getInt(NBT_SCHEDULE_INDEX), + nbt.getInt(NBT_SECTION_INDEX), + nbt.getUUID(NBT_TRAIN_ID), + nbt.getString(NBT_TRAIN_NAME), + TrainIconType.byId(new ResourceLocation(nbt.getString(NBT_TRAIN_ICON))), + TrainInfo.fromNbt(nbt.getCompound(NBT_TRAIN_INFO)), + nbt.getString(NBT_SCHEDULE_TITLE), + nbt.getBoolean(NBT_IS_CUSTOM_TITLE), + nbt.getString(NBT_TERMINUS_TEXT), + nbt.getInt(NBT_STAY_DURATION), + nbt.getLong(NBT_SIMULATED_TIME) != 0, + nbt.getLong(NBT_SCHEDULED_DEPARTURE_TIME), + nbt.getLong(NBT_SCHEDULED_ARRIVAL_TIME), + nbt.getInt(NBT_CYCLE), + ClientStationTag.fromNbt(nbt.getCompound(NBT_TAG)), + nbt.getLong(NBT_REAL_TIME_ARRIVAL_TIME), + nbt.getLong(NBT_REAL_TIME_DEPARTURE_TIME), + nbt.getInt(NBT_REAL_CYCLE), + ClientStationTag.fromNbt(nbt.getCompound(NBT_REAL_TIME_TAG)), + nbt.contains(NBT_REAL_TIME_ARRIVAL_TIME) ? nbt.getLong(NBT_REAL_TIME_ARRIVAL_TIME) - nbt.getLong(NBT_SCHEDULED_ARRIVAL_TIME) : 0, + nbt.contains(NBT_REAL_TIME_DEPARTURE_TIME) ? nbt.getLong(NBT_REAL_TIME_DEPARTURE_TIME) - nbt.getLong(NBT_SCHEDULED_DEPARTURE_TIME) : 0, + 0, + TrainState.BEFORE + ); + } + + @Override + public void close() { + clearEvents(); + isClosed = true; + } + + public static record TrainStopRealTimeData(ClientStationTag station, int entryIndex, long scheduledArrivalTime, long scheduledDepartureTime, long realTimeArrivalTime, long realTimeDepartureTime, long deltaArrivalTime, long deltaDepartureTime, int ticksUntilArrival, int cycle) { + private static final String NBT_INDEX = "Index"; + private static final String NBT_STATION = "Station"; + private static final String NBT_SCHEDULED_ARRIVAL = "Arrival"; + private static final String NBT_SCHEDULED_DEPARTURE = "Departure"; + private static final String NBT_REAL_TIME_ARRIVAL = "RealArrival"; + private static final String NBT_REAL_TIME_DEPARTURE = "RealDeparture"; + private static final String NBT_DELTA_ARRIVAL = "DeltaArrival"; + private static final String NBT_DELTA_DEPARTURE = "DeltaDeparture"; + private static final String NBT_CYCLE = "Cycle"; + private static final String NBT_TICKS_UNTIL_ARRIVAL = "TUA"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.put(NBT_STATION, station.toNbt()); + nbt.putInt(NBT_INDEX, entryIndex); + nbt.putLong(NBT_SCHEDULED_ARRIVAL, scheduledArrivalTime); + nbt.putLong(NBT_SCHEDULED_DEPARTURE, scheduledDepartureTime); + nbt.putLong(NBT_REAL_TIME_ARRIVAL, realTimeArrivalTime); + nbt.putLong(NBT_REAL_TIME_DEPARTURE, realTimeDepartureTime); + nbt.putLong(NBT_DELTA_ARRIVAL, deltaArrivalTime); + nbt.putLong(NBT_DELTA_DEPARTURE, deltaDepartureTime); + nbt.putInt(NBT_CYCLE, cycle); + nbt.putInt(NBT_TICKS_UNTIL_ARRIVAL, ticksUntilArrival); + + return nbt; + } + + public static TrainStopRealTimeData fromNbt(CompoundTag nbt) { + return new TrainStopRealTimeData( + ClientStationTag.fromNbt(nbt.getCompound(NBT_STATION)), + nbt.getInt(NBT_INDEX), + nbt.getLong(NBT_SCHEDULED_ARRIVAL), + nbt.getLong(NBT_SCHEDULED_DEPARTURE), + nbt.getLong(NBT_REAL_TIME_ARRIVAL), + nbt.getLong(NBT_REAL_TIME_DEPARTURE), + nbt.getLong(NBT_DELTA_ARRIVAL), + nbt.getLong(NBT_DELTA_DEPARTURE), + nbt.getInt(NBT_TICKS_UNTIL_ARRIVAL), + nbt.getInt(NBT_CYCLE) + ); + } + } + + public boolean isClosed() { + return isClosed; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/RoutePartProgressState.java b/common/src/main/java/de/mrjulsen/crn/data/train/RoutePartProgressState.java new file mode 100644 index 00000000..8d8466de --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/RoutePartProgressState.java @@ -0,0 +1,40 @@ +package de.mrjulsen.crn.data.train; + +import java.util.Arrays; + +public enum RoutePartProgressState { + BEFORE(0), + AT_START(1), + TRAVELING(2), + NEXT_STOP_ANNOUNCED(3), + AT_STOPOVER(4), + END_ANNOUNCED(4), + AT_END(5), + AFTER(6); + + private int index; + + private RoutePartProgressState(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static RoutePartProgressState getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(BEFORE); + } + + public boolean isAnyStopAnnounced() { + return this == NEXT_STOP_ANNOUNCED || this == END_ANNOUNCED; + } + + public boolean isAtAnyStop() { + return this == AT_START || this == AT_STOPOVER || this == AT_END; + } + + public boolean isOutOfBounds() { + return this == BEFORE || this == AFTER; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/RouteProgressState.java b/common/src/main/java/de/mrjulsen/crn/data/train/RouteProgressState.java new file mode 100644 index 00000000..072713bb --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/RouteProgressState.java @@ -0,0 +1,52 @@ +package de.mrjulsen.crn.data.train; + +import java.util.Arrays; + +public enum RouteProgressState { + BEFORE(0), + AT_START(1), + TRAVELING(2), + NEXT_STOP_ANNOUNCED(3), + AT_STOPOVER(4), + TRANSFER_ANNOUNCED(5), + AT_TRANSFER(6), + WHILE_TRANSFER(7), + BEFORE_CONTINUATION(8), + END_ANNOUNCED(9), + AT_END(10), + AFTER(11); + + private int index; + + private RouteProgressState(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static RouteProgressState getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(BEFORE); + } + + public boolean isAnyStopAnnounced() { + return this == NEXT_STOP_ANNOUNCED || this == END_ANNOUNCED || this == TRANSFER_ANNOUNCED; + } + + public boolean isAtAnyStop() { + return this == AT_START || this == AT_STOPOVER || this == AT_END || this == AT_TRANSFER || this == BEFORE_CONTINUATION; + } + + public boolean isOutOfBounds() { + return this == BEFORE || this == AFTER; + } + + public boolean isTransferring() { + return this == AT_TRANSFER || this == WHILE_TRANSFER || this == BEFORE_CONTINUATION; + } + + public boolean isWaiting() { + return isOutOfBounds() || this == WHILE_TRANSFER; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java new file mode 100644 index 00000000..48ff4576 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java @@ -0,0 +1,672 @@ +package de.mrjulsen.crn.data.train; + +import java.util.List; +import java.util.Optional; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.Map.Entry; +import java.util.Map; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; + +import com.google.common.collect.ImmutableMap; +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.events.TotalDurationTimeChangedEvent; +import de.mrjulsen.crn.data.TrainInfo; +import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.data.train.TrainStatus.TrainStatusType; +import de.mrjulsen.crn.util.IListenable; +import de.mrjulsen.crn.util.LockedList; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.data.Cache; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +/** Contains general data about a specific train (but not about the individual stations) */ +public class TrainData implements IListenable { + + private transient static final int VERSION = 1; + + public transient static final String EVENT_TOTAL_DURATION_CHANGED = "total_duration_changed"; + public transient static final String EVENT_SECTION_CHANGED = "section_changed"; + public transient static final String EVENT_DESTINATION_CHANGED = "destination_changed"; + public transient static final String EVENT_STATION_REACHED = "station_reached"; + + private transient static final String NBT_VERSION = "Version"; + private transient static final String NBT_ID = "SessionId"; + private transient static final String NBT_TRAIN_ID = "TrainId"; + private transient static final String NBT_PREDICTIONS = "Predictions"; + private transient static final String NBT_CURRENT_SCHEDULE_INDEX = "CurrentScheduleIndex"; + private transient static final String NBT_LINE_ID = "LineId"; + private transient static final String NBT_LAST_DELAY_OFFSET = "LastDelay"; + private transient static final String NBT_CANCELLED = "Cancelled"; + private transient static final String NBT_TRANSIT_TIMES = "TransitTimes"; + + private transient static final int INVALID = -1; + + + private transient final Map>> listeners = new HashMap<>(); // Events + + private transient final Train train; + private UUID sessionId; + private final ConcurrentHashMap predictionsByIndex = new ConcurrentHashMap<>(); + private transient final ConcurrentHashMap sectionsByIndex = new ConcurrentHashMap<>(); + private transient final Cache defaultSection = new Cache<>(() -> TrainTravelSection.def(this)); + private transient final List predictionsChronologically = new LockedList<>(); + private transient final Set validPredictionEntries = new HashSet<>(); + private transient final Cache isDynamic = new Cache<>(() -> getTrain().runtime.getSchedule().entries.stream().anyMatch(x -> x.conditions.stream().flatMap(y -> y.stream()).anyMatch(y -> y instanceof DynamicDelayCondition c && c.minWaitTicks() < c.totalWaitTicks()))); + + private int currentScheduleIndex = INVALID; + private transient int currentTravelSectionIndex = INVALID; + private transient int lastScheduleIndex = INVALID; + private String lineId; + + private transient int totalDuration = INVALID; + private transient long destinationReachTime; + private transient boolean isAtStation = false; + + private transient boolean wasWaitingForSignal = false; + public transient UUID waitingForSignalId; + public transient final Set occupyingTrains = new HashSet<>(); + public transient int waitingForSignalTicks; + public transient boolean isManualControlled; + + /** Last measured ransit time (mem) */ + public transient int transitTime = 0; + /** Contains the last (single!) measured transit time that can be used for the calculation. */ + private transient final Map measuredTransitTimes = new HashMap<>(); + /** Contains the x last measured transit times that can be used for the calculation. */ + public final Map /* last x transit times */> transitTimeHistory = new HashMap<>(); + /** The current valid and used transit time. */ + public final Map currentTransitTime = new HashMap<>(); + + // Delays + private long lastSectionDelayOffset; + private boolean cancelled = false; + private transient final Map delaysBySignal = new HashMap<>(); + private final Set currentStatusInfos = new HashSet<>(); // Reasons for delays, etc. + + private int refreshTimingsCounter = 0; + + // Queued Tasks + /** whether all predictions should be deleted */ + private transient boolean hardResetPredictions = false; + /** whether the initialization should now be completed */ + private transient boolean initializationFinishTask = false; + /** whether the initialization has already been completed */ + private transient boolean initializationCompleted = false; + private boolean hasStarted = false; + + // temp mem + private boolean sectionChanged; + private boolean destinationChanged; + + /* PLEASE NOTE! + * Chronologically update order (once every ~5 seconds): + * refreshPre() (once) + * setPredictionData() (x times) + * refreshPost() (once) + * + * tick() (every tick) + */ + + private TrainData(Train train, UUID sessionId) { + this.train = train; + this.sessionId = sessionId; + this.totalDuration = INVALID; + + createEvent(EVENT_TOTAL_DURATION_CHANGED); + createEvent(EVENT_DESTINATION_CHANGED); + createEvent(EVENT_SECTION_CHANGED); + createEvent(EVENT_STATION_REACHED); + } + + public static Optional of(UUID trainId) { + Optional train = TrainUtils.getTrain(trainId); + if (train.isPresent()) { + return Optional.of(new TrainData(train.get(), UUID.randomUUID())); + } + return Optional.empty(); + } + + public static TrainData of(Train train) { + return new TrainData(train, UUID.randomUUID()); + } + + public UUID getSessionId() { + return sessionId; + } + + public UUID getTrainId() { + return getTrain().id; + } + + public Train getTrain() { + return train; + } + + public TrainInfo getTrainInfo(int scheduleIndex) { + return new TrainInfo(getSectionForIndex(scheduleIndex).getTrainLine(), getSectionForIndex(scheduleIndex).getTrainGroup()); + } + + /** + * Checks if this train uses dynamic wait times to catch up for delays. + * If there are no dynamic waiting times, a train cannot catch up for delays, resulting in permanent delays. + * Even if everything goes according to plan, a train tends to be delayed due to calculation inaccuracies. + */ + public boolean isDynamic() { + return isDynamic.get(); + } + + private int getHistoryBufferSize() { + return ModCommonConfig.TOTAL_DURATION_BUFFER_SIZE.get() * 2 + 1; + } + + /** {@code true} when the train is currently waiting at a station. */ + public boolean isAtStation() { + return train.navigation.destination == null; + } + + /** The time in ticks the train is waiting at the current station. */ + public long waitingAtStationTicks() { + return isAtStation() ? DragonLib.getCurrentWorldTime() - destinationReachTime : 0; + } + + public boolean isCancelled() { + return cancelled; + } + + public int getTotalDuration() { + return totalDuration; + } + + public int getTransitTicks() { + return transitTime; + } + + public int getTransitTimeOf(int scheduleIndex) { + return currentTransitTime.containsKey(scheduleIndex) ? currentTransitTime.get(scheduleIndex) : INVALID; + } + + public boolean isWaitingAtStation() { + return isAtStation; + } + + public TrainTravelSection getCurrentTravelSection() { + return currentTravelSectionIndex < 0 || !sectionsByIndex.containsKey(currentTravelSectionIndex) ? defaultSection.get() : sectionsByIndex.get(currentTravelSectionIndex); + } + + public TrainTravelSection getSectionByIndex(int scheduleIndex) { + return sectionsByIndex.isEmpty() ? defaultSection.get() : sectionsByIndex.get(scheduleIndex); + } + + public void addTravelSection(TrainTravelSection section) { + this.sectionsByIndex.put(section.getScheduleIndex(), section); + } + + public String getCurrentTitle() { + return this.predictionsByIndex.containsKey(currentScheduleIndex) ? this.predictionsByIndex.get(currentScheduleIndex).getTitle() : ""; + } + + public String getTrainName() { + return train.name.getString(); + } + + public int getCurrentScheduleIndex() { + return currentScheduleIndex; + } + + public boolean hasCustomTravelSections() { + return !sectionsByIndex.isEmpty(); + } + + public boolean isSingleSection() { + return sectionsByIndex.size() <= 1; + } + + public List getSections() { + return sectionsByIndex.isEmpty() ? List.of(defaultSection.get()) : sectionsByIndex.values().stream().sorted((a, b) -> Integer.compare(a.getScheduleIndex(), b.getScheduleIndex())).toList(); + } + + public TrainTravelSection getSectionForIndex(int anyIndex) { + if (isSingleSection()) { + return getSections().get(0); + } + TrainTravelSection selectedSection = getSections().get(getSections().size() - 1); + for (TrainTravelSection section : getSections()) { + if (section.getScheduleIndex() > anyIndex) { + break; + } + selectedSection = section; + } + return selectedSection; + } + + public synchronized List getPredictions() { + return new ArrayList<>(predictionsByIndex.values()); + } + + public synchronized Map getPredictionsRaw() { + return new HashMap<>(predictionsByIndex); + } + + public synchronized List getPredictionsChronologically() { + return new ArrayList<>(predictionsChronologically); + } + + public synchronized Optional getNextStopPrediction() { + return predictionsChronologically.isEmpty() ? Optional.empty() : Optional.ofNullable(predictionsChronologically.get(0)); + } + + public void resetPredictions() { + predictionsByIndex.values().stream().forEach(x -> x.reset()); + lastSectionDelayOffset = 0; + refreshTimingsCounter = 0; + resetStatus(true); + isDynamic.clear(); + if (CreateRailwaysNavigator.isDebug()) CreateRailwaysNavigator.LOGGER.info(getTrainName() + " has reset their scheduled times."); + } + + public void hardResetPredictions() { + hardResetPredictions = true; + } + + public synchronized boolean isDelayed() { + return predictionsChronologically.stream().anyMatch(TrainPrediction::isAnyDelayed); + } + + public boolean isCurrentSectionDelayed() { + return isDelayed() && getHighestDeviation() - lastSectionDelayOffset > ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get(); + } + + public long getHighestDeviation() { + return predictionsByIndex.values().stream().mapToLong(x -> Math.max(x.getArrivalTimeDeviation(), x.getDepartureTimeDeviation())).max().orElse(0); + } + + public long getDeviationDelayOffset() { + return lastSectionDelayOffset; + } + + public TrainTravelSection getCurrentSection() { + return currentTravelSectionIndex < 0 || !hasCustomTravelSections() || !sectionsByIndex.containsKey(currentTravelSectionIndex) ? TrainTravelSection.def(this) : sectionsByIndex.get(currentTravelSectionIndex); + } + + public Map getWaitingForSignalsTime() { + return ImmutableMap.copyOf(delaysBySignal); + } + + public Set getStatus() { + return currentStatusInfos.stream().map(x -> TrainStatus.Registry.getRegisteredStatus().get(x).compile(this)).collect(Collectors.toSet()); + } + + public int debug_statusInfoCount() { + return currentStatusInfos.size(); + } + + private void resetStatus(boolean keepPreviousDelays) { + currentStatusInfos.clear(); + if (keepPreviousDelays && isDelayed()) { + currentStatusInfos.add(TrainStatus.DELAY_FROM_PREVIOUS_JOURNEY.getLocation()); + } + } + + public void applyStatus() { + if (isCancelled()) { + currentStatusInfos.clear(); + currentStatusInfos.add(TrainStatus.CANCELLED.getLocation()); + return; + } + + for (Entry x : TrainStatus.Registry.getRegisteredStatus().entrySet()) { + if (x.getValue().isTriggerd(this)) { + currentStatusInfos.add(x.getKey()); + } + } + + + if (currentStatusInfos.stream().noneMatch(x -> TrainStatus.Registry.getRegisteredStatus().get(x).getImportance() == TrainStatusType.DELAY && !x.equals(TrainStatus.DEFAULT_DELAY.getLocation())) && isCurrentSectionDelayed()) { + currentStatusInfos.add(TrainStatus.DEFAULT_DELAY.getLocation()); + } else { + currentStatusInfos.remove(TrainStatus.DEFAULT_DELAY.getLocation()); + } + } + + public boolean hasSectionChanged() { + return sectionChanged; + } + + /** + * Indicates whether there is enough data about this train and whether it has already been initialized. + * Trains that have not yet been initialized do not yet contain any reliable data to make any predictions. + */ + public boolean isInitialized() { + return !currentTransitTime.isEmpty() && + currentTransitTime.values().stream().noneMatch(x -> x < 0) + ; + } + + public int debug_initializedStationsCount() { + return (int)currentTransitTime.values().stream().filter(x -> x > 0).count(); + } + + /** + * Indicates whether any preparations need to be made before the initialization phase can begin. + * This is especially the case after starting the world, when the train was still in the middle of its journey. + */ + public boolean isPreparing() { + return !hasStarted; + } + + public synchronized TrainPrediction setPredictionData(int entryIndex, int currentIndex, int maxEntries, int stayDuration, int minStayDuration, int transitTime, TrainDeparturePrediction predictionData) { + // keep track of current schedule index + this.destinationChanged = destinationChanged || this.currentScheduleIndex != currentIndex; + this.currentScheduleIndex = currentIndex; + + // Update CRN predictions with data from Create + TrainPrediction pred = predictionsByIndex.computeIfAbsent(entryIndex, i -> new TrainPrediction(this, entryIndex, predictionData, stayDuration, minStayDuration)); + currentTransitTime.computeIfAbsent(entryIndex, x -> -1); + validPredictionEntries.add(entryIndex); + + pred.updateRealTime( + predictionData.destination, + predictionData.ticks + ); + predictionsChronologically.add(pred); + return pred; + } + + public void changeCurrentSection(int sectionEntryIndex) { + this.currentTravelSectionIndex = sectionsByIndex.containsKey(sectionEntryIndex) ? sectionEntryIndex : INVALID; + sectionChanged = true; + lastSectionDelayOffset = Math.max(0, getHighestDeviation()); + this.refreshTimingsCounter++; + } + + private void clearAll() { + predictionsByIndex.clear(); + sectionsByIndex.clear(); + defaultSection.clear(); + predictionsChronologically.clear(); + validPredictionEntries.clear(); + currentStatusInfos.clear(); + measuredTransitTimes.clear(); + transitTimeHistory.clear(); + currentTransitTime.clear(); + lastScheduleIndex = INVALID; + hasStarted = false; + } + + /** Called every ~5 seconds */ + public synchronized void refreshPre() { + if (train.runtime.paused) { + return; + } + + if (hardResetPredictions) { + hardResetPredictions = false; + clearAll(); + } + + validPredictionEntries.clear(); + predictionsChronologically.clear(); + } + + /** Called every ~5 seconds */ + public synchronized void refreshPost() { + + // [] Remove invalid prediction data + if (hasStarted && !train.runtime.paused) { + predictionsByIndex.keySet().retainAll(validPredictionEntries); + measuredTransitTimes.keySet().retainAll(validPredictionEntries); + transitTimeHistory.keySet().retainAll(validPredictionEntries); + currentTransitTime.keySet().retainAll(validPredictionEntries); + } + + // [] Called after the schedule index has been changed. + if (lastScheduleIndex >= 0 && lastScheduleIndex != currentScheduleIndex && predictionsByIndex.containsKey(lastScheduleIndex)) { + predictionsByIndex.get(lastScheduleIndex).nextCycle(); + } + if (!hasCustomTravelSections() && lastScheduleIndex > currentScheduleIndex) { // Manually call section change event if there are no sections defined. + changeCurrentSection(currentTravelSectionIndex); + } + lastScheduleIndex = currentScheduleIndex; + + // [] train cancelled manager + boolean isNowCancelled = !(TrainUtils.isTrainValid(train) && isInitialized()) || train.runtime.paused; + if (this.cancelled && !isNowCancelled) { // Train should no longer be cancelled -> restart + hasStarted = false; + initializationCompleted = false; + initializationFinishTask = false; + sessionId = UUID.randomUUID(); + resetPredictions(); + } + this.cancelled = isNowCancelled; + + applyStatus(); + + if (destinationChanged) { + destinationChanged = false; + notifyListeners(EVENT_DESTINATION_CHANGED, this); + } + + if (initializationFinishTask) { + initializationFinishTask = false; + onInitialize(); + } + } + + /** Called every tick */ + public void tick() { + if (train.runtime.paused) { + return; + } + + if (!isAtStation()) { + transitTime++; // CRN transit time measurement + } + + // Waiting for signal processor + boolean isWaitingForSignal = train.navigation.waitingForSignal != null; + if (wasWaitingForSignal != isWaitingForSignal) { // The moment in which the state has been changed + if (isWaitingForSignal) { // currently waiting + waitingForSignalId = train.navigation.waitingForSignal.getFirst(); + occupyingTrains.clear(); + occupyingTrains.addAll(TrainUtils.isSignalOccupied(waitingForSignalId, Set.of(train.id))); + } else { // no longer waiting + delaysBySignal.put(waitingForSignalId, waitingForSignalTicks); + waitingForSignalTicks = 0; + occupyingTrains.clear(); + } + } + + if (isWaitingForSignal) { + waitingForSignalTicks++; + } + + this.wasWaitingForSignal = isWaitingForSignal; + } + + public void updateTotalDuration() { + // measuredTransitTimes + int newDuration = currentTransitTime.values().stream().mapToInt(x -> x).sum() + getPredictions().stream().mapToInt(x -> x.getStayDuration()).sum(); + int oldTotalDuration = this.totalDuration; + if (CRNEventsManager.isRegistered(TotalDurationTimeChangedEvent.class) && this.totalDuration > 0 && this.totalDuration != newDuration) { + CRNEventsManager.getEvent(TotalDurationTimeChangedEvent.class).run(train, this.totalDuration, newDuration); + } + this.totalDuration = newDuration; + if (oldTotalDuration != INVALID) { + notifyListeners(EVENT_TOTAL_DURATION_CHANGED, this); + } + resetPredictions(); + } + + /** + * Called when the train reaches a station. + * @param createTicksInTransit Ticks measured by Create. + */ + public void reachDestination(long destinationReachTime, int createTicksInTransit) { + this.destinationReachTime = destinationReachTime; + + if (hasStarted) { + processTransitHistory(transitTimeHistory.computeIfAbsent(currentScheduleIndex, x -> new PriorityQueue<>())); + this.measuredTransitTimes.put(currentScheduleIndex, ModCommonConfig.CUSTOM_TRANSIT_TIME_CALCULATION.get() ? createTicksInTransit : transitTime); + } + this.transitTime = 0; + this.waitingForSignalTicks = 0; + this.waitingForSignalId = null; + this.delaysBySignal.clear(); + this.hasStarted = true; + this.isAtStation = true; + + if (!initializationCompleted && isInitialized()) { + initializationCompleted = true; + initializationFinishTask = true; + } + + if (sectionChanged) { + sectionChanged = false; + if (!isDynamic() || (ModCommonConfig.AUTO_RESET_TIMINGS.get() > 0 && refreshTimingsCounter >= ModCommonConfig.AUTO_RESET_TIMINGS.get())) { + resetPredictions(); + } else { + resetStatus(true); + } + notifyListeners(EVENT_SECTION_CHANGED, this); + } + + notifyListeners(EVENT_STATION_REACHED, this); + } + + public void leaveDestination() { + this.currentScheduleIndex = getTrain().runtime.currentEntry; + this.isAtStation = false; + } + + public void onInitialize() { + updateTotalDuration(); + } + + /** Checks and calculates a new total duration time if necessary. */ + private void processTransitHistory(Queue history) { + // First initialization + if (!currentTransitTime.containsKey(currentScheduleIndex) || currentTransitTime.get(currentScheduleIndex) < 0) { + fillHistory(history, transitTime); + currentTransitTime.put(currentScheduleIndex, transitTime); // Set initial reference transit time + } + + // remove excess elements + while (history.size() >= getHistoryBufferSize()) { + history.poll(); + } + history.offer(transitTime); // add current transit time to the history + + int refCurrentTransitTime = currentTransitTime.get(currentScheduleIndex); + double median = ModUtils.calculateMedian(history, ModCommonConfig.TOTAL_DURATION_DEVIATION_THRESHOLD.get(), x -> true); + + if (Math.abs(refCurrentTransitTime - median) > ModCommonConfig.TOTAL_DURATION_DEVIATION_THRESHOLD.get()) { // Deviation is too large -> change transit time for this section + int newValue = ModUtils.calculateMedian(history, ModCommonConfig.TOTAL_DURATION_DEVIATION_THRESHOLD.get(), x -> Math.abs(refCurrentTransitTime - x) > ModCommonConfig.TOTAL_DURATION_DEVIATION_THRESHOLD.get()); + currentTransitTime.put(currentScheduleIndex, newValue); // save transit time for this section + fillHistory(history, newValue); // Reset the history + updateTotalDuration(); + } else if (Math.abs(refCurrentTransitTime - transitTime) < ModCommonConfig.TOTAL_DURATION_DEVIATION_THRESHOLD.get()) { // new value is smaller than current -> reset history (no changes needed) + fillHistory(history, refCurrentTransitTime); + } + } + + private void fillHistory(Queue history, int value) { + history.clear(); + for (int i = 0; i < getHistoryBufferSize(); i++) { + history.add(value); + } + } + + @Override + public Map>> getListeners() { + return listeners; + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putInt(NBT_VERSION, VERSION); + + CompoundTag predictions = new CompoundTag(); + for (Entry entry : predictionsByIndex.entrySet()) { + predictions.put(String.valueOf(entry.getKey()), entry.getValue().toNbt()); + } + + CompoundTag transitTimes = new CompoundTag(); + for (Entry entry : this.currentTransitTime.entrySet()) { + transitTimes.putInt(String.valueOf(entry.getKey()), entry.getValue()); + } + + nbt.putUUID(NBT_ID, getSessionId()); + nbt.putUUID(NBT_TRAIN_ID, getTrainId()); + nbt.put(NBT_PREDICTIONS, predictions); + nbt.put(NBT_TRANSIT_TIMES, transitTimes); + nbt.putInt(NBT_CURRENT_SCHEDULE_INDEX, currentScheduleIndex); + nbt.putLong(NBT_LAST_DELAY_OFFSET, lastSectionDelayOffset); + nbt.putBoolean(NBT_CANCELLED, cancelled); + nbt.putString(NBT_LINE_ID, lineId == null ? "" : lineId); + return nbt; + } + + public static TrainData fromNbt(CompoundTag nbt) { + UUID trainId = nbt.getUUID(NBT_TRAIN_ID); + UUID sessionId = nbt.getUUID(NBT_ID); + TrainData data = new TrainData(TrainUtils.getTrain(trainId).get(), sessionId); // TODO + data.deserializeNbt(nbt); + return data; + } + + protected void deserializeNbt(CompoundTag nbt) { + CompoundTag predictions = nbt.getCompound(NBT_PREDICTIONS); + for (String key : predictions.getAllKeys()) { + try { + int idx = Integer.parseInt(key); + this.predictionsByIndex.put(idx, TrainPrediction.fromNbt(this, predictions.getCompound(key))); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.warn("Unable to load prediction with index '" + key + "': The value is not an integer.", e); + } + } + + CompoundTag transitTimes = nbt.getCompound(NBT_TRANSIT_TIMES); + for (String key : transitTimes.getAllKeys()) { + try { + int idx = Integer.parseInt(key); + int time = transitTimes.getInt(key); + if (time > 0) { + fillHistory(transitTimeHistory.computeIfAbsent(idx, x -> new PriorityQueue<>()), time); + this.measuredTransitTimes.put(idx, time); + this.currentTransitTime.put(idx, time); + } + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.warn("Unable to load transit time with index '" + key + "': The value is not an integer.", e); + } + } + + this.currentScheduleIndex = nbt.getInt(NBT_CURRENT_SCHEDULE_INDEX); + this.lastScheduleIndex = currentScheduleIndex; + this.currentTravelSectionIndex = getSectionForIndex(currentScheduleIndex).getScheduleIndex(); + this.lineId = nbt.getString(NBT_LINE_ID); + this.lastSectionDelayOffset = nbt.getLong(NBT_LAST_DELAY_OFFSET); + this.cancelled = nbt.getBoolean(NBT_CANCELLED); + } + + public synchronized void shiftTime(long l) { + this.destinationReachTime += l; + predictionsByIndex.values().forEach(x -> x.shiftTime(l)); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java new file mode 100644 index 00000000..54da4195 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java @@ -0,0 +1,225 @@ +package de.mrjulsen.crn.data.train; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.Set; + +import com.simibubi.create.content.trains.entity.Train; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.crn.event.events.CreateTrainPredictionEvent; +import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPost; +import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPre; +import de.mrjulsen.crn.event.events.ScheduleResetEvent; +import de.mrjulsen.crn.event.events.SubmitTrainPredictionsEvent; +import de.mrjulsen.crn.event.events.TotalDurationTimeChangedEvent; +import de.mrjulsen.crn.event.events.TrainArrivalAndDepartureEvent; +import de.mrjulsen.crn.event.events.TrainDestinationChangedEvent; +import de.mrjulsen.crn.mixin.ScheduleRuntimeAccessor; +import de.mrjulsen.mcdragonlib.DragonLib; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.storage.LevelResource; + +/** Monitors all trains in the world and processes their data and information to make it available for use. */ +public final class TrainListener { + + private transient static final String FILENAME = CreateRailwaysNavigator.MOD_ID + "_train_data.nbt"; + + public static final ConcurrentHashMap data = new ConcurrentHashMap<>(); + + private transient static boolean trainDataListenerActive = false; + private transient static long currentTrainDataListenerId = 0L; + private transient static final Queue trainDataHookTasks = new ConcurrentLinkedQueue<>(); + + + public static void init() { + // Register Event Listeners + CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPre.class).register(CreateRailwaysNavigator.MOD_ID, () -> { + queueTrainListenerTask(TrainListener::refreshPre); + }); + + CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPost.class).register(CreateRailwaysNavigator.MOD_ID, () -> { + queueTrainListenerTask(TrainListener::refreshPost); + }); + + CRNEventsManager.getEvent(TrainDestinationChangedEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, current, next, nextIndex) -> { + }); + + CRNEventsManager.getEvent(TotalDurationTimeChangedEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, old, newDuration) -> { + CreateRailwaysNavigator.LOGGER.warn("The total duration of the train " + train.name.getString() + " (" + train.id + ") has changed from " + old + " Ticks to " + newDuration + " Ticks. This will result in changes to the scheduled departure times!"); + }); + + CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, station, isArrival) -> { + queueTrainListenerTask(() -> { + if (data.containsKey(train.id)) { + if (isArrival) { + data.get(train.id).reachDestination(DragonLib.getCurrentWorldTime(), ((ScheduleRuntimeAccessor)train.runtime).crn$getTicksInTransit()); + } else { + data.get(train.id).leaveDestination(); + } + } + }); + }); + + CRNEventsManager.getEvent(ScheduleResetEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, soft) -> { + queueTrainListenerTask(() -> { + if (data.containsKey(train.id)) { + TrainData trainData = data.get(train.id); + if (soft) { + trainData.resetPredictions(); + } else { + trainData.hardResetPredictions(); + } + } + }); + }); + + CRNEventsManager.getEvent(SubmitTrainPredictionsEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, predictions, entryCount, accumulatedTime, current) -> { + + }); + + CRNEventsManager.getEvent(CreateTrainPredictionEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, schedule, predictables, index, stayDuration, minStayDuration, prediction) -> { + queueTrainListenerTask(() -> { + ScheduleRuntimeAccessor accessor = (ScheduleRuntimeAccessor)(Object)schedule; + UUID trainId = accessor.crn$getTrain().id; + if (data.containsKey(trainId) && prediction != null) { + TrainData trainData = data.get(trainId); + TrainPrediction pred = trainData.setPredictionData(index, schedule.currentEntry, schedule.getSchedule().entries.size(), stayDuration, minStayDuration, accessor.crn$predictionTicks().get(index), prediction); + predictables.values().forEach(x -> x.predictForStation(trainData, pred, schedule, index, accessor.crn$getTrain())); + } + }); + }); + } + + public static Set getAllTrains() { + return data.values().stream().map(x -> x.getTrain()).collect(Collectors.toSet()); + } + + public static boolean allTrainsInitialized() { + return data.values().stream().filter(x -> + !GlobalSettings.getInstance().isTrainBlacklisted(x.getTrain()) && + !x.getPredictionsRaw().isEmpty() && + !x.getTrain().runtime.paused && + !x.getTrain().derailed && + !x.getTrain().runtime.completed && + TrainUtils.isTrainValid(x.getTrain()) + ).allMatch(x -> x.isInitialized() && !x.isPreparing()); + } + + public static void start() { + new Thread(() -> { + init(); + long id; + do { + id = System.nanoTime(); + } while (currentTrainDataListenerId == id); + + currentTrainDataListenerId = id; + trainDataListenerActive = true; + trainDataHookTasks.clear(); + TrainListener.data.clear(); + try { + TrainListener.load(); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Unable to load train listener data.", e); + } + + final long threadId = id; + new Thread(() -> { + try { + while (currentTrainDataListenerId == threadId && trainDataListenerActive) { + while (!trainDataHookTasks.isEmpty()) { + try { + trainDataHookTasks.poll().run(); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Error while executing train listener task.", e); + } + } + + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + CreateRailwaysNavigator.LOGGER.error("Error while waiting for next task.", e); + } + } + save(); + TrainListener.data.clear(); + trainDataHookTasks.clear(); + CreateRailwaysNavigator.LOGGER.info("Train listener has been stopped."); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Error while executing Train Listener.", e); + } + }, "CRN Train Listener").start(); + CreateRailwaysNavigator.LOGGER.info("Train listener has been started."); + }, "CRN Train Listener Launcher").start(); + } + + public static void stop() { + trainDataListenerActive = false; + CreateRailwaysNavigator.LOGGER.info("Stopping train listener..."); + } + + public static synchronized void save() { + if (!trainDataListenerActive) { + return; + } + + CompoundTag nbt = new CompoundTag(); + data.entrySet().forEach(x -> nbt.put(x.getKey().toString(), x.getValue().toNbt())); + + try { + NbtIo.writeCompressed(nbt, new File(ModCommonEvents.getCurrentServer().get().getWorldPath(new LevelResource("data/" + FILENAME)).toString())); + CreateRailwaysNavigator.LOGGER.debug("Saved train listener data."); + } catch (IOException e) { + CreateRailwaysNavigator.LOGGER.error("Unable to save train listener data.", e); + } + } + + private static void load() throws IOException { + File settingsFile = new File(ModCommonEvents.getCurrentServer().get().getWorldPath(new LevelResource("data/" + FILENAME)).toString()); + if (!settingsFile.exists()) { + return; + } + CompoundTag nbt = NbtIo.readCompressed(settingsFile); + for (String key : nbt.getAllKeys()) { + try { + UUID id = UUID.fromString(key); + data.put(id, TrainData.fromNbt(nbt.getCompound(key))); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.warn("Unable to read train listener train data with ID '" + key + "'.", e); + } + } + } + + private static void queueTrainListenerTask(Runnable task) { + trainDataHookTasks.add(task); + } + + public synchronized static void refreshPre() { + if (!trainDataListenerActive) return; + Set trains = TrainUtils.getTrains(true); + data.keySet().retainAll(trains.stream().filter(x -> !GlobalSettings.getInstance().isTrainBlacklisted(x)).map(x -> x.id).toList()); + trains.forEach(x -> { + data.computeIfAbsent(x.id, a -> TrainData.of(x)).refreshPre(); + }); + } + + public synchronized static void refreshPost() { + if (!trainDataListenerActive) return; + data.values().forEach(x -> x.refreshPost()); + } + + public synchronized static void tick() { + if (!trainDataListenerActive) return; + data.values().forEach(x -> x.tick()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java new file mode 100644 index 00000000..5ebd35bb --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java @@ -0,0 +1,509 @@ +package de.mrjulsen.crn.data.train; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import com.google.common.base.Objects; +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; + +/** Data about one single station of a train. */ +public class TrainPrediction implements Comparable { + + private static final String NBT_ENTRY_INDEX = "EntryIndex"; + private static final String NBT_STATION_NAME = "StationName"; + private static final String NBT_TITLE = "Title"; + private static final String NBT_SCHEDULED_TICKS = "ScheduledTicks"; + private static final String NBT_SCHEDULED_REFRESH_TIME = "ScheduledRefreshTime"; + private static final String NBT_REAL_TIME_TICKS = "RealTimeTicks"; + private static final String NBT_REAL_TIME_REFRESH_TIME = "RealTimeRefreshTime"; + private static final String NBT_CURRENT_TICKS_CORRECTION = "CurrentTicksCorrection"; + private static final String NBT_STOPOVERS = "Stopovers"; + private static final String NBT_CYCLE = "Cycle"; + private static final String NBT_CYCLE_TIME = "CycleTime"; + private static final String NBT_STAY_DURATION = "StayDuration"; + private static final String NBT_MIN_STAY_DURATION = "MinStayDuration"; + + private transient final TrainData data; + + private final int entryIndex; + private final String title; + private String stationName; + + private int scheduledTicks; + private long scheduleRefreshTime; + private int realTimeTicks; + private long realTimeRefreshTime; + + private long arrivalTicksCorrection; + private long departureTicksCorrection; + private List stopovers = new ArrayList<>(); + private int cycle; + private long cycleTime; + private final int stayDuration; + private final int minStayDuration; + + // History + private long previousScheduledArrivalTime; + private long previousScheduledDepartureTime; + private long previousRealTimeArrivalTime; + private long previousRealTimeDepartureTime; + + private final Cache isCustomTitle = new Cache<>(() -> { + if (this.getData().getPredictionsChronologically().isEmpty()) { + return false; + } + TrainPrediction nextPrediction = this.getData().getPredictionsChronologically().get((this.getData().getPredictionsChronologically().indexOf(this) + 1) % this.getData().getPredictionsChronologically().size()); + return !getTitle().matches(nextPrediction.getStationName()); + }); + private final Cache isLastStopOfSection = new Cache<>(() -> { + TrainTravelSection section = getSection(); + return section.isFinalStop(this); + }); + private final Cache section; + + private TrainPrediction(TrainData data, int entryIndex, String stationName, String title, int ticks, int stayDuration, int minStayDuration) { + this.entryIndex = entryIndex; + this.data = data; + this.title = title; + this.stayDuration = stayDuration; + this.minStayDuration = minStayDuration; + this.stationName = stationName; + this.realTimeTicks = ticks; + this.realTimeRefreshTime = DragonLib.getCurrentWorldTime(); + this.section = new Cache<>(() -> data.getSectionForIndex(entryIndex)); + + reset(); + } + + public TrainPrediction(TrainData data, int entryIndex, TrainDeparturePrediction prediction, int stayDuration, int minStayDuration) { + this(data, entryIndex, prediction.destination, prediction.scheduleTitle.getString(), prediction.ticks, stayDuration, minStayDuration); + } + + public static TrainPrediction unpredictable(TrainData data) { + CreateRailwaysNavigator.LOGGER.warn("Train " + data.getTrain().name.getString() + " (" + data.getTrain().id + ") is unpredictable!"); + return new TrainPrediction(data, -1, "", "", 0, 0, 0); + } + + /** Resets the scheduled time to the current real time. Called when the total duration changes to prevent deviations. */ + public void reset() { + this.cycleTime = 0; + this.departureTicksCorrection = 0; + this.arrivalTicksCorrection = 0; + this.scheduledTicks = realTimeTicks; + this.scheduleRefreshTime = realTimeRefreshTime; + } + + /** General data about the train. */ + public TrainData getData() { + return data; + } + + /** The index of this entry in the train schedule. */ + public int getEntryIndex() { + return entryIndex; + } + + /** The name of the station. */ + public String getStationName() { + return stationName; + } + + /** The title, the train has when arriving at this station. */ + public String getTitle() { + return title; + } + + public boolean hasCustomTitle() { + return isCustomTitle.get(); + } + + /** The scheduled time the train will stay at this station. */ + public int getStayDuration() { + return stayDuration; + } + + public int getMinStayDuration() { + return minStayDuration; + } + + public List getStopovers() { + return stopovers; + } + + /** The world time when the scheduled time was calculated. */ + public long getScheduleRefreshTime() { + return scheduleRefreshTime; + } + + + + + /** The time in ticks of all cycles that have elapsed since the last update. */ + public long getCycleTime() { + return cycleTime; + } + + /** The scheduled time until the train stops here. */ + public int getScheduledArrivalTicks() { + return scheduledTicks; + } + + /** The scheduled world time when the train arrives at this station. */ + public long getScheduledArrivalTime() { + return getScheduleRefreshTime() + (long)getScheduledArrivalTicks() + getCycleTime(); + } + + + + + /** The world time when the real time data was last refreshed. */ + public long getRealTimeRefreshTime() { + return realTimeRefreshTime; + } + + /** The current time until the train stops here. */ + public int getRealTimeArrivalTicks() { + return realTimeTicks; + } + + private long getRealTimeArrivalTimeRaw() { + return getRealTimeRefreshTime() + getRealTimeArrivalTicks(); + } + + /** The actual deviation from real time and schedule time. Cycles are not taken into account! */ + private long getArrivalTimeRawDeviation() { + return getRealTimeArrivalTimeRaw() - getScheduledArrivalTime(); + } + + + /** The current world time the train will arrive at this station. */ + public long getRealTimeArrivalTime() { + return getRealTimeArrivalTimeRaw() - arrivalTicksCorrection; + } + + /** The actual deviation from real time and schedule time. */ + public long getArrivalTimeDeviation() { + return getArrivalTimeRawDeviation() - arrivalTicksCorrection; + } + + + + + + + + + + + + + /** The departure time from this stop when the schedule was updated. */ + public int getScheduledDepartureTicks() { + return getScheduledArrivalTicks() + getStayDuration(); + } + + /** The scheduled world time when the train departs from this station. */ + public long getScheduledDepartureTime() { + return getScheduledArrivalTime() + getStayDuration(); + } + + /** The current world time at which the train will depart. */ + public long getRealTimeDepartureTime() { + return getRealTimeArrivalTime() + getStayDuration() - departureTicksCorrection;// - Math.min(getBufferTime(), getDeviationArrivalTime()); + } + + /** The deviation of the departure time from the schedule. */ + public long getDepartureTimeDeviation() { + return getArrivalTimeDeviation() - departureTicksCorrection; + } + + + + + /** The time it took the train to get here from the last station. */ + public int getLastTransitTime() { + return data.getTransitTicks(); + } + + public long getBufferTime() { + return Math.max(getStayDuration() - getMinStayDuration(), 0); + } + + /** The remaining buffer time that the train can use to catch up for delays. */ + public long getBufferTimeLeft() { + return getBufferTime() - data.waitingAtStationTicks(); + } + + public long getScheduledArrivalDay() { + return getScheduledArrivalTime() / DragonLib.TICKS_PER_DAY; + } + + public long getScheduledDepartureDay() { + return getScheduledDepartureDay() / DragonLib.TICKS_PER_DAY; + } + + public long getRealTimeArrivalDay() { + return getRealTimeArrivalTime() / DragonLib.TICKS_PER_DAY; + } + + public long getRealTimeDepartureDay() { + return getRealTimeDepartureTime() / DragonLib.TICKS_PER_DAY; + } + + public void setStopovers(List stopovers) { + this.stopovers = stopovers; + } + + + /** Change this stop to the next cycle. */ + public void nextCycle() { + this.previousScheduledArrivalTime = getScheduledArrivalTime(); + this.previousScheduledDepartureTime = getScheduledDepartureTime(); + this.previousRealTimeArrivalTime = getRealTimeArrivalTime(); + this.previousRealTimeDepartureTime = getRealTimeDepartureTime(); + + cycle++; + this.cycleTime += data.getTotalDuration(); + } + + /** The cycle the train is currently in. */ + public int getCurrentCycle() { + return cycle; + } + + + public long getPreviousScheduledArrivalTime() { + return previousScheduledArrivalTime; + } + + public long getPreviousScheduledDepartureTime() { + return previousScheduledDepartureTime; + } + + public long getPreviousRealTimeArrivalTime() { + return previousRealTimeArrivalTime; + } + + public long getPreviousRealTimeDepartureTime() { + return previousRealTimeDepartureTime; + } + + + + /** Calculates in which cycle the train will be when it arrives back here in the specified time.*/ + public int estimateCycleIn(int ticks) { + return getCurrentCycle() + ticks / data.getTotalDuration(); + } + + /** Time since start of recording. */ + public long getRuntime() { + return DragonLib.getCurrentWorldTime() - getScheduleRefreshTime(); + } + + public boolean hasDepartedOnce() { + return getCurrentCycle() > 0; + } + + public boolean isArrivalDelayed() { + return getRealTimeArrivalTime() - ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get() > getScheduledArrivalTime(); + } + + public boolean isDepartureDelayed() { + return getRealTimeDepartureTime() - ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get() > getScheduledDepartureTime(); + } + + public boolean isAnyDelayed() { + return isArrivalDelayed() || isDepartureDelayed(); + } + + /** + * Get the station tag for this station. Server-side only! + * @return The StationTag for this stop. + * @throws RuntimeSideException Thrown when called on the wrong logical side. + */ + public StationTag getStationTag() throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + return GlobalSettings.getInstance().getOrCreateStationTagFor(stationName); + } + + public TrainTravelSection getSection() { + TrainTravelSection sec = section.get(); + if (sec.isDefault()) { + section.clear(); + } + return sec; + } + + public String getSectionDestinationText() { + TrainTravelSection sec = section.get(); + if (sec.isDefault()) { + section.clear(); + } + return isLastStopOfSection.get() ? sec.nextSection().getDisplayText() : sec.getDisplayText(); + } + + + + public void updateRealTime(String stationName, int realTimeTicks) { + isCustomTitle.clear(); + this.stationName = stationName == null ? this.stationName : stationName; + this.realTimeRefreshTime = DragonLib.getCurrentWorldTime(); + this.realTimeTicks = realTimeTicks; + + List prevPreds = data.getPredictionsChronologically(); + Optional currentPrediction = data.getNextStopPrediction(); + this.arrivalTicksCorrection = 0; + this.departureTicksCorrection = 0; + + if (data.isWaitingAtStation() && data.getCurrentScheduleIndex() == getEntryIndex()) { + this.arrivalTicksCorrection = Math.min(data.waitingAtStationTicks(), getStayDuration()); + //this.arrivalTicksCorrection = data.waitingAtStationTicks(); + } + + if (currentPrediction.isPresent()) { + this.arrivalTicksCorrection = currentPrediction.get().arrivalTicksCorrection; + } + + long tempDepartureCorrection = getBufferTime(); + long tempArrivalCorrection = 0;//getBufferTime(); + for (int i = 0; i < prevPreds.size(); i++) { + final TrainPrediction pred = prevPreds.get(i); + //tempArrivalCorrection += getBufferTime(); + tempArrivalCorrection += pred.getBufferTime(); + if (pred == this) break; + } + this.arrivalTicksCorrection += Math.min(tempArrivalCorrection, getArrivalTimeDeviation()); + this.departureTicksCorrection += Math.min(tempDepartureCorrection, getArrivalTimeDeviation()); + } + + @Override + public boolean equals(Object obj) { + return + obj instanceof TrainPrediction o && + scheduledTicks == o.scheduledTicks && + scheduleRefreshTime == o.scheduleRefreshTime && + entryIndex == o.entryIndex && + stationName.equals(o.stationName) + ; + } + + @Override + public int hashCode() { + return Objects.hashCode(scheduledTicks, scheduleRefreshTime, entryIndex, stationName); + } + + public boolean similarTo(Object obj) { + return + obj instanceof TrainPrediction o && + stationName.equals(o.stationName) && + entryIndex == o.entryIndex + ; + } + + @Override + public String toString() { + return formattedText().getString(); + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + + ListTag stopovers = new ListTag(); + stopovers.addAll(this.stopovers.stream().map(x -> StringTag.valueOf(x)).toList()); + + nbt.putInt(NBT_ENTRY_INDEX, entryIndex); + nbt.putString(NBT_STATION_NAME, stationName == null ? "" : stationName); + nbt.putString(NBT_TITLE, title == null ? "" : title); + nbt.putInt(NBT_SCHEDULED_TICKS, scheduledTicks); + nbt.putLong(NBT_SCHEDULED_REFRESH_TIME, scheduleRefreshTime); + nbt.putInt(NBT_REAL_TIME_TICKS, realTimeTicks); + nbt.putLong(NBT_REAL_TIME_REFRESH_TIME, realTimeRefreshTime); + nbt.putLong(NBT_CURRENT_TICKS_CORRECTION, departureTicksCorrection); + nbt.put(NBT_STOPOVERS, stopovers); + nbt.putInt(NBT_CYCLE, cycle); + nbt.putLong(NBT_CYCLE_TIME, cycleTime); + nbt.putInt(NBT_STAY_DURATION, stayDuration); + nbt.putInt(NBT_MIN_STAY_DURATION, minStayDuration); + return nbt; + } + + public static TrainPrediction fromNbt(TrainData data, CompoundTag nbt) { + TrainPrediction pred = new TrainPrediction( + data, + nbt.getInt(NBT_ENTRY_INDEX), + nbt.getString(NBT_STATION_NAME), + nbt.getString(NBT_TITLE), + nbt.getInt(NBT_SCHEDULED_TICKS), + nbt.getInt(NBT_STAY_DURATION), + nbt.getInt(NBT_MIN_STAY_DURATION) + ); + pred.deserializeNbt(nbt); + return pred; + } + + protected void deserializeNbt(CompoundTag nbt) { + this.scheduledTicks = nbt.getInt(NBT_SCHEDULED_TICKS); + this.scheduleRefreshTime = nbt.getLong(NBT_SCHEDULED_REFRESH_TIME); + this.realTimeTicks = nbt.getInt(NBT_REAL_TIME_TICKS); + this.realTimeRefreshTime = nbt.getLong(NBT_REAL_TIME_REFRESH_TIME); + this.departureTicksCorrection = nbt.getLong(NBT_CURRENT_TICKS_CORRECTION); + this.stopovers = new ArrayList<>(nbt.getList(NBT_STOPOVERS, Tag.TAG_STRING).stream().map(x -> x.getAsString()).toList()); + this.cycle = nbt.getInt(NBT_CYCLE); + this.cycleTime = nbt.getLong(NBT_CYCLE_TIME); + } + + /** + * DEBUG ONLY! + */ + public Component formattedText() { + return TextUtils.text("[ " + entryIndex + " ]: ").withStyle(ChatFormatting.WHITE) + .append(TextUtils.text(stationName).withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("*" + getCurrentCycle()).withStyle(ChatFormatting.YELLOW)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("CT: " + getRealTimeArrivalTime()).withStyle(ChatFormatting.GREEN)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("D: " + (getArrivalTimeDeviation() + " / " + getDepartureTimeDeviation())).withStyle(ChatFormatting.GOLD)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("B: " + (departureTicksCorrection)).withStyle(ChatFormatting.DARK_GREEN)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("P: " + (getScheduledArrivalTime())).withStyle(ChatFormatting.BLUE)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("S: " + getStayDuration()).withStyle(ChatFormatting.AQUA)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("Tr: " + getLastTransitTime()).withStyle(ChatFormatting.DARK_RED)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("T: " + getSection()).withStyle(ChatFormatting.RED)) + .append(TextUtils.text(", ").withStyle(ChatFormatting.WHITE)) + .append(TextUtils.text("Ti: " + title).withStyle(ChatFormatting.LIGHT_PURPLE)) + ; + } + + public void shiftTime(long l) { + this.scheduleRefreshTime += l; + this.realTimeRefreshTime += l; + } + + @Override + public int compareTo(TrainPrediction o) { + return Long.compare(getScheduledArrivalTime(), o.getScheduledArrivalTime()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainState.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainState.java new file mode 100644 index 00000000..76ebb0f1 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainState.java @@ -0,0 +1,35 @@ +package de.mrjulsen.crn.data.train; + +import java.util.Arrays; + +/** The status of the train measured at the current station. */ +public enum TrainState { + /** The train will arrive at this station in the future. */ + BEFORE((byte)-2, '-'), + /** The next stop was announced. */ + ANNOUNCED((byte)-1, '.'), + /** The train is waiting at this station. */ + STAYING((byte)0, '~'), + /** The train has already departed from this station. */ + AFTER((byte)1, '+'); + + private byte position; + private char indicator; + + private TrainState(byte position, char indicator) { + this.position = position; + this.indicator = indicator; + } + + public byte getPositionMultiplier() { + return position; + } + + public char getIndicator() { + return indicator; + } + + public static TrainState getByPositionInt(int position) { + return Arrays.stream(values()).filter(x -> x.getPositionMultiplier() == position).findFirst().orElse(STAYING); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java new file mode 100644 index 00000000..0c19d13d --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java @@ -0,0 +1,226 @@ +package de.mrjulsen.crn.data.train; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableMap; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.data.Single; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; + +public class TrainStatus { + + public static final MutableComponent textOperationalDisruption = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.operational_disruption"); // Betriebsstörung + public static final MutableComponent textTooFewTracks = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.too_few_tracks"); // Verfügbarkeit der Gleise eingeschränkt + public static final MutableComponent textOperationalStabilization = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.operational_stabilization"); // Betriebsstabilisierung + public static final MutableComponent textStaffShortage = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.staff_shortage"); // Kurzfristiger Personalausfall + public static final MutableComponent textTrackClosed = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.track_closed"); + + private static final Registry REGISTRY = Registry.create(CreateRailwaysNavigator.MOD_ID); + public static final TrainStatus DEFAULT_DELAY = REGISTRY.registerDefault("default_delay", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.unknown_delay"), null)); + public static final TrainStatus DELAY_FROM_PREVIOUS_JOURNEY = REGISTRY.registerDefault("delay_from_previous_journey", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.delay_previous_journey"), null)); + public static final TrainStatus CANCELLED = REGISTRY.registerDefault("cancelled", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.cancelled"), null)); + + public static final int HEIGHT = 9; + + private final TrainStatusType importance; + private final TrainStatusCategory category; + private final Function text; + //private final Function reason; + private final Predicate trigger; + + private ResourceLocation location; + + public TrainStatus(TrainStatusCategory category, TrainStatusType importance, Function text, /*Function reason,*/ Predicate trigger) { + this.importance = importance; + this.category = category; + this.text = text; + //this.reason = reason; + this.trigger = trigger; + } + + public TrainStatusType getImportance() { + return importance; + } + + public MutableComponent getText(TrainData data) { + return text.apply(data); + } + + /* + public MutableComponent getReason(NewTrainData data) { + return reason.apply(data); + } + */ + + public boolean isTriggerd(TrainData data) { + return trigger != null && trigger.test(data); + } + + public CompiledTrainStatus compile(TrainData data) { + return new CompiledTrainStatus(category, importance, getText(data));//, getReason(data)); + } + + public ResourceLocation getLocation() { + return location; + } + + + private void setLocation(ResourceLocation location) { + this.location = location; + } + + + public static record CompiledTrainStatus(TrainStatusCategory category, TrainStatusType type, Component text/*, Component reason*/) { + + public static final String NBT_CATEGORY = "Category"; + public static final String NBT_TYPE = "Type"; + public static final String NBT_TEXT = "Text"; + public static final String NBT_REASON = "Reason"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putByte(NBT_CATEGORY, category().getIndex()); + nbt.putByte(NBT_TYPE, type().getIndex()); + nbt.putString(NBT_TEXT, text().getString()); + //nbt.putString(NBT_REASON, reason().getString()); + return nbt; + } + + public static CompiledTrainStatus fromNbt(CompoundTag nbt) { + return new CompiledTrainStatus( + TrainStatusCategory.getByIndex(nbt.getByte(NBT_CATEGORY)), + TrainStatusType.getByIndex(nbt.getByte(NBT_TYPE)), + TextUtils.text(nbt.getString(NBT_TEXT)) + //TextUtils.text(nbt.getString(NBT_REASON)) + ); + } + + public int render(Graphics graphics, Single font, int x, int y, int maxWidth) { + final int color = type().getColor(); + final float scale = 0.75f; + graphics.poseStack().pushPose(); + graphics.poseStack().translate(x, y, 0); + GuiUtils.setTint(color); + ModGuiIcons.IMPORTANT.render(graphics, -4, -3); + graphics.poseStack().scale(scale, scale, 1); + int height = (int)(ClientWrapper.renderMultilineLabelSafe(graphics, (int)(10 / scale), (int)(2 / scale), font.getFirst(), text(), (int)(maxWidth / scale), color) * scale); + graphics.poseStack().popPose(); + + return Math.max(HEIGHT, height + 2); + } + } + + + public static class Registry { + + private static final Map registeredStatusInfos = new HashMap<>(); + + private final String modid; + + private Registry(String modid) { + this.modid = modid; + } + + public static Registry create(String modid) { + return new Registry(modid); + } + + public static ImmutableMap getRegisteredStatus() { + return ImmutableMap.copyOf(registeredStatusInfos); + } + + public TrainStatus register(String name, TrainStatus statusPattern) { + ResourceLocation loc = new ResourceLocation(modid, name); + statusPattern.setLocation(loc); + registeredStatusInfos.put(loc, statusPattern); + return statusPattern; + } + + public TrainStatus registerDefault(String name, TrainStatus statusPattern) { + ResourceLocation loc = new ResourceLocation(modid, name); + statusPattern.setLocation(loc); + registeredStatusInfos.put(loc, statusPattern); + return statusPattern; + } + + public TrainStatus unregister(ResourceLocation location) { + return registeredStatusInfos.remove(location); + } + + public TrainStatus get(ResourceLocation location) { + return registeredStatusInfos.get(location); + } + + public boolean isRegistered(ResourceLocation location) { + return registeredStatusInfos.containsKey(location); + } + + public void delete(String modid) { + registeredStatusInfos.keySet().removeIf(x -> x.getNamespace().equals(modid)); + } + } + + public static enum TrainStatusType { + MESSAGE_DEFAULT((byte)0, 0xFFFFFFFF), + MESSAGE_WARN((byte)1, ChatFormatting.GOLD.getColor()), + MESSAGE_IMPORTANT((byte)2, Constants.COLOR_DELAYED), + DELAY((byte)3, Constants.COLOR_DELAYED); + + private final byte index; + private final int color; + + private TrainStatusType(byte index, int color) { + this.index = index; + this.color = color; + } + + public byte getIndex() { + return index; + } + + public int getColor() { + return color; + } + + public static TrainStatusType getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(MESSAGE_DEFAULT); + } + } + + public static enum TrainStatusCategory { + /** Information about a train. */ + TRAIN((byte)0), + /** Information about a single station. */ + STATION((byte)1); + + private final byte index; + + private TrainStatusCategory(byte index) { + this.index = index; + } + + public byte getIndex() { + return index; + } + + public static TrainStatusCategory getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(TRAIN); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java new file mode 100644 index 00000000..907a6e9e --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java @@ -0,0 +1,417 @@ +package de.mrjulsen.crn.data.train; + +import java.util.UUID; +import com.simibubi.create.content.trains.entity.TrainIconType; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.config.ModCommonConfig; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TagName; +import de.mrjulsen.crn.data.StationTag.ClientStationTag; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.TrainInfo; +import de.mrjulsen.mcdragonlib.DragonLib; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +/** + * A small variant of the {@code NewTrainPrediction} class, representing a stop of a train on its route with some important information. + */ +public class TrainStop implements Comparable { + + protected static final String NBT_SCHEDULE_INDEX = "ScheduleIndex"; + protected static final String NBT_SECTION_INDEX = "SectionIndex"; + protected static final String NBT_TRAIN_ID = "TrainId"; + protected static final String NBT_TRAIN_NAME = "TrainName"; + protected static final String NBT_TRAIN_ICON = "TrainIcon"; + protected static final String NBT_TRAIN_INFO = "TrainInfo"; + protected static final String NBT_SCHEDULE_TITLE = "ScheduleTitle"; + protected static final String NBT_TERMINUS_TEXT = "TerminusText"; + protected static final String NBT_STAY_DURATION = "StayDuration"; + protected static final String NBT_IS_CUSTOM_TITLE = "IsCustomTitle"; + protected static final String NBT_SIMULATED_TIME = "SimulationTime"; + + protected static final String NBT_SCHEDULED_DEPARTURE_TIME = "ScheduledDeparture"; + protected static final String NBT_SCHEDULED_ARRIVAL_TIME = "ScheduledArrival"; + protected static final String NBT_CYCLE = "Cycle"; + protected static final String NBT_TAG = "StationTag"; + + protected static final String NBT_REAL_TIME_ARRIVAL_TIME = "RealArrival"; + protected static final String NBT_REAL_TIME_DEPARTURE_TIME = "RealDeparture"; + protected static final String NBT_REAL_CYCLE = "RealCycle"; + protected static final String NBT_REAL_TIME_TAG = "RealTimeTag"; + protected static final String NBT_STATE = "State"; + + protected final int scheduleIndex; + protected final int sectionIndex; + protected final UUID trainId; + protected final String trainName; + protected final TrainIconType trainIcon; + protected final TrainInfo trainInfo; + protected final String scheduleTitle; + protected final String terminusText; + protected final int stayDuration; + protected final boolean isCustomTitle; + + protected boolean simulated; + protected long simulationTime; + + protected long scheduledDepartureTime; + protected long scheduledArrivalTime; + protected int cycle; // Der Zyklus, für den dieser Eintrag gültig ist. Wenn der real-time Zyklus kleiner ist, dann ist der Zug noch nicht vorhersagbar, falls größer, ist er abgefahren. + protected ClientStationTag tag; + + protected long realTimeArrivalTime; + protected long realTimeDepartureTime; + protected int realTimeCycle = -1; + protected ClientStationTag realTimeTag; + + protected long arrivalTimeDeviation; + protected long departureTimeDeviation; + protected int realTimeTicksUntilArrival = -1; + + + // state + protected TrainState trainState = TrainState.BEFORE; + + + public TrainStop(int scheduleIndex, int sectionIndex, UUID trainId, String trainName, TrainIconType trainIcon, TrainInfo trainInfo, + String scheduleTitle, boolean isCustomTitle, String terminusText, int stayDuration, boolean simulated, + long scheduledDepartureTime, long scheduledArrivalTime, int cycle, ClientStationTag tag, long realTimeArrivalTime, + long realTimeDepartureTime, int realTimeCycle, ClientStationTag realTimeTag, long arrivalTimeDeviation, + long departureTimeDeviation, int realTimeTicksUntilArrival, TrainState trainPosition) { + this.scheduleIndex = scheduleIndex; + this.sectionIndex = sectionIndex; + this.trainId = trainId; + this.trainName = trainName; + this.trainIcon = trainIcon; + this.trainInfo = trainInfo; + this.scheduleTitle = scheduleTitle; + this.isCustomTitle = isCustomTitle; + this.terminusText = terminusText; + this.stayDuration = stayDuration; + this.simulated = simulated; + this.scheduledDepartureTime = scheduledDepartureTime; + this.scheduledArrivalTime = scheduledArrivalTime; + this.cycle = cycle; + this.tag = tag; + this.realTimeArrivalTime = realTimeArrivalTime; + this.realTimeDepartureTime = realTimeDepartureTime; + this.realTimeCycle = realTimeCycle; + this.realTimeTag = realTimeTag; + this.arrivalTimeDeviation = arrivalTimeDeviation; + this.departureTimeDeviation = departureTimeDeviation; + this.realTimeTicksUntilArrival = realTimeTicksUntilArrival; + this.trainState = trainPosition; + } + + public TrainStop(TrainPrediction prediction) { + this(prediction.getStationTag(), prediction, false); + } + + public TrainStop(StationTag tag, TrainPrediction prediction, boolean lastCycle) { + this( + prediction.getEntryIndex(), + prediction.getSection().getScheduleIndex(), + prediction.getData().getTrainId(), + prediction.getData().getTrain().name.getString(), + prediction.getData().getTrain().icon, + prediction.getData().getTrainInfo(prediction.getEntryIndex()), + prediction.getTitle(), + prediction.hasCustomTitle(), + prediction.getSectionDestinationText(), + prediction.getStayDuration(), + false, + lastCycle ? prediction.getPreviousScheduledDepartureTime() : prediction.getScheduledDepartureTime(), + lastCycle ? prediction.getPreviousScheduledArrivalTime() : prediction.getScheduledArrivalTime(), + prediction.getCurrentCycle() - (lastCycle ? 1 : 0), + prediction.getStationTag().getClientTag(prediction.getStationName()), + lastCycle ? prediction.getPreviousRealTimeArrivalTime() : prediction.getRealTimeArrivalTime(), + lastCycle ? prediction.getPreviousRealTimeDepartureTime() : prediction.getRealTimeDepartureTime(), + prediction.getCurrentCycle() - (lastCycle ? 1 : 0), + prediction.getStationTag().getClientTag(prediction.getStationName()), + prediction.getArrivalTimeDeviation(), + prediction.getDepartureTimeDeviation(), + prediction.getRealTimeArrivalTicks(), + TrainState.BEFORE + ); + //updateRealTime(prediction); + } + + public TrainStop copy() { + return new TrainStop( + this.scheduleIndex, + this.sectionIndex, + this.trainId, + this.trainName, + this.trainIcon, + this.trainInfo, + this.scheduleTitle, + this.isCustomTitle, + this.terminusText, + this.stayDuration, + this.simulated, + this.scheduledDepartureTime, + this.scheduledArrivalTime, + this.cycle, + this.tag, + this.realTimeArrivalTime, + this.realTimeDepartureTime, + this.realTimeCycle, + this.realTimeTag, + this.arrivalTimeDeviation, + this.departureTimeDeviation, + this.realTimeTicksUntilArrival, + this.trainState + ); + } + + public void simulateTicks(long ticks) { + this.simulated = true; + int totalDuration = TrainListener.data.get(getTrainId()).getTotalDuration(); + long scheduledTimeUntilArrival = getScheduledArrivalTime() - DragonLib.getCurrentWorldTime(); + int simulationCycles = (int)(ticks / totalDuration); + long simulationRemaining = ticks % totalDuration; + if (simulationRemaining > 0 && simulationRemaining >= scheduledTimeUntilArrival) { + simulationCycles++; + } + + this.cycle += simulationCycles; + this.scheduledArrivalTime += simulationCycles * totalDuration; + this.scheduledDepartureTime += simulationCycles * totalDuration; + + this.realTimeArrivalTime += simulationCycles * totalDuration; + this.realTimeDepartureTime += simulationCycles * totalDuration; + this.realTimeTicksUntilArrival = -1; + this.simulationTime += ticks; + } + + public void simulateCycles(int cycles) { + if (cycles == 0) { + return; + } + simulateTicks(cycles * TrainListener.data.get(getTrainId()).getTotalDuration()); + } + + + public static TrainStop simulateCyclesBack(TrainPrediction prediction) { + return new TrainStop(prediction.getStationTag(), prediction, true); + } + + public boolean isSimulated() { + return simulated; + } + + public long getSimulationTime() { + return simulationTime; + } + + public UUID getTrainId() { + return trainId; + } + + public String getTrainName() { + return trainName; + } + + public TrainIconType getTrainIcon() { + return trainIcon; + } + + public TrainInfo getTrainInfo() { + return trainInfo; + } + + public int getScheduleIndex() { + return scheduleIndex; + } + + public int getSectionIndex() { + return sectionIndex; + } + + public String getTerminusText() { + return terminusText; + } + + public boolean hasCustomTitle() { + return isCustomTitle; + } + + public String getScheduleTitle() { + return scheduleTitle; + } + + public String getDisplayTitle() { + return hasCustomTitle() || getTerminusText() == null || getTerminusText().isEmpty() ? getScheduleTitle() : getTerminusText(); + } + + public String getTrainDisplayName() { + return getTrainInfo() == null || getTrainInfo().line() == null || getTrainInfo().line().getLineName().isEmpty() ? getTrainName() : getTrainInfo().line().getLineName(); + } + + public int getTrainDisplayColor() { + if (getTrainInfo() != null && getTrainInfo().line() != null && getTrainInfo().line().getColor() != 0) { + return getTrainInfo().line().getColor(); + } else if (getTrainInfo() != null && getTrainInfo().group() != null && getTrainInfo().group().getColor() != 0) { + return getTrainInfo().group().getColor(); + } + return Constants.COLOR_TRAIN_BACKGROUND; + } + + public int getStayTime() { + return stayDuration; + } + + public StationTag getTag() { + return GlobalSettings.getInstance().getStationTag(getClientTag().tagId()).orElse(GlobalSettings.getInstance().getOrCreateStationTagFor(TagName.of(getClientTag().tagName()))); + } + + public ClientStationTag getClientTag() { + return tag; + } + + public int getScheduledCycle() { + return cycle; + } + + public int getRealTimeCycle() { + return realTimeCycle; + } + + public ClientStationTag getRealTimeStationTag() { + return realTimeTag; + } + + public long getScheduledDepartureTime() { + return scheduledDepartureTime; + } + + public long getScheduledArrivalTime() { + return scheduledArrivalTime; + } + + public long getRealTimeArrivalTime() { + return realTimeArrivalTime; + } + + public long getRealTimeDepartureTime() { + return realTimeDepartureTime; + } + + public long getArrivalTimeDeviation() { + return arrivalTimeDeviation; + } + + public long getDepartureTimeDeviation() { + return departureTimeDeviation; + } + + public int getTicksUntilArrival() { + return realTimeTicksUntilArrival; + } + + public long getScheduledArrivalDay() { + return getScheduledArrivalTime() / DragonLib.TICKS_PER_DAY; + } + + public long getScheduledDepartureDay() { + return getScheduledDepartureDay() / DragonLib.TICKS_PER_DAY; + } + + public long getRealTimeArrivalDay() { + return getRealTimeArrivalTime() / DragonLib.TICKS_PER_DAY; + } + + public long getRealTimeDepartureDay() { + return getRealTimeDepartureTime() / DragonLib.TICKS_PER_DAY; + } + + /** + * The state of this train at this station. + */ + public TrainState getState() { + return trainState; + } + + public boolean isArrivalDelayed() { + return getArrivalTimeDeviation() >= ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get(); + } + + public boolean isDepartureDelayed() { + return getDepartureTimeDeviation() >= ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get(); + } + + public boolean isAnyDelayed() { + return isArrivalDelayed() || isDepartureDelayed(); + } + + public boolean shouldRenderRealTime() { + return getRealTimeCycle() >= getScheduledCycle(); + } + + public boolean isStationInfoChanged() { + return !getClientTag().info().equals(getRealTimeStationTag().info()); + } + + public boolean isDeparted() { + return trainState == TrainState.AFTER; + } + + @Override + public int compareTo(TrainStop o) { + return Long.compare(getScheduledArrivalTime(), o.getScheduledArrivalTime()); + } + + public CompoundTag toNbt(boolean includeRealTime) { + CompoundTag nbt = new CompoundTag(); + nbt.putInt(NBT_SCHEDULE_INDEX, scheduleIndex); + nbt.putInt(NBT_SECTION_INDEX, sectionIndex); + nbt.putUUID(NBT_TRAIN_ID, trainId); + nbt.putString(NBT_TRAIN_NAME, trainName); + nbt.putString(NBT_TRAIN_ICON, trainIcon.getId().toString()); + nbt.put(NBT_TRAIN_INFO, trainInfo.toNbt()); + nbt.putString(NBT_SCHEDULE_TITLE, scheduleTitle); + nbt.putString(NBT_TERMINUS_TEXT, terminusText); + nbt.putInt(NBT_STAY_DURATION, stayDuration); + nbt.putBoolean(NBT_IS_CUSTOM_TITLE, isCustomTitle); + nbt.put(NBT_TAG, tag.toNbt()); + nbt.putLong(NBT_SIMULATED_TIME, simulationTime); + nbt.putLong(NBT_SCHEDULED_ARRIVAL_TIME, scheduledArrivalTime); + nbt.putLong(NBT_SCHEDULED_DEPARTURE_TIME, scheduledDepartureTime); + nbt.putInt(NBT_CYCLE, cycle); + nbt.putLong(NBT_REAL_TIME_ARRIVAL_TIME, realTimeArrivalTime); + nbt.putLong(NBT_REAL_TIME_DEPARTURE_TIME, realTimeDepartureTime); + nbt.putInt(NBT_REAL_CYCLE, realTimeCycle); + nbt.put(NBT_REAL_TIME_TAG, realTimeTag.toNbt()); + return nbt; + } + + public static TrainStop fromNbt(CompoundTag nbt) { + return new TrainStop( + nbt.getInt(NBT_SCHEDULE_INDEX), + nbt.getInt(NBT_SECTION_INDEX), + nbt.getUUID(NBT_TRAIN_ID), + nbt.getString(NBT_TRAIN_NAME), + TrainIconType.byId(new ResourceLocation(nbt.getString(NBT_TRAIN_ICON))), + TrainInfo.fromNbt(nbt.getCompound(NBT_TRAIN_INFO)), + nbt.getString(NBT_SCHEDULE_TITLE), + nbt.getBoolean(NBT_IS_CUSTOM_TITLE), + nbt.getString(NBT_TERMINUS_TEXT), + nbt.getInt(NBT_STAY_DURATION), + nbt.getLong(NBT_SIMULATED_TIME) != 0, + nbt.getLong(NBT_SCHEDULED_DEPARTURE_TIME), + nbt.getLong(NBT_SCHEDULED_ARRIVAL_TIME), + nbt.getInt(NBT_CYCLE), + ClientStationTag.fromNbt(nbt.getCompound(NBT_TAG)), + nbt.getLong(NBT_REAL_TIME_ARRIVAL_TIME), + nbt.getLong(NBT_REAL_TIME_DEPARTURE_TIME), + nbt.getInt(NBT_REAL_CYCLE), + ClientStationTag.fromNbt(nbt.getCompound(NBT_REAL_TIME_TAG)), + nbt.contains(NBT_REAL_TIME_ARRIVAL_TIME) ? nbt.getLong(NBT_REAL_TIME_ARRIVAL_TIME) - nbt.getLong(NBT_SCHEDULED_ARRIVAL_TIME) : 0, + nbt.contains(NBT_REAL_TIME_DEPARTURE_TIME) ? nbt.getLong(NBT_REAL_TIME_DEPARTURE_TIME) - nbt.getLong(NBT_SCHEDULED_DEPARTURE_TIME) : 0, + 0, + TrainState.BEFORE + ); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java new file mode 100644 index 00000000..6c2a6f25 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java @@ -0,0 +1,264 @@ +package de.mrjulsen.crn.data.train; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.mcdragonlib.data.Cache; + +public class TrainTravelSection { + + private static final int INVALID = -1; + + private transient final TrainData data; + private transient final int scheduleIndex; + private transient final boolean isDefault; + + private final boolean includeLastStationOfLastSection; + private final boolean usable; + private final TrainGroup trainGroup; + private final TrainLine trainLine; + + private final Cache> predictions = new Cache<>(() -> getPredictions(INVALID, false)); + private final Cache nextSection; + private final Cache previousSection; + + public TrainTravelSection(TrainData data, int indexInSchedule, TrainGroup group, TrainLine line, boolean includePreviousStation, boolean usable) { + this(false, data, indexInSchedule, group, line, includePreviousStation, usable); + } + + private TrainTravelSection(boolean isDefault, TrainData data, int indexInSchedule, TrainGroup group, TrainLine line, boolean includePreviousStation, boolean usable) { + this.data = data; + this.scheduleIndex = indexInSchedule; + this.isDefault = isDefault; + this.trainGroup = group; + this.trainLine = line; + this.includeLastStationOfLastSection = includePreviousStation; + this.usable = usable; + + nextSection = new Cache<>(() -> { + if (data.isSingleSection()) { + return this; + } + + List sections = data.getSections(); + if (sections.isEmpty()) { + return this; + } + int selfIndex = sections.indexOf(this); + if (selfIndex < 0 || selfIndex >= sections.size()) { + return sections.get(0); + } + return sections.get((selfIndex + 1) % sections.size()); + }); + + + previousSection = new Cache<>(() -> { + if (data.isSingleSection()) { + return this; + } + + List sections = data.getSections(); + if (sections.isEmpty()) { + return this; + } + int selfIndex = sections.indexOf(this); + if (selfIndex < 0 || selfIndex >= sections.size()) { + return sections.get(0); + } + int prevIndex = selfIndex - 1; + return sections.get(prevIndex < 0 ? sections.size() - 1 : prevIndex); + }); + } + + public static final TrainTravelSection def(TrainData data) { + return new TrainTravelSection(true, data, 0, null, null, true, true); + } + + public boolean isDefault() { + return isDefault; + } + + public TrainData getData() { + return data; + } + + public int getScheduleIndex() { + return scheduleIndex; + } + + public boolean shouldIncludeNextStationOfNextSection() { + return includeLastStationOfLastSection; + } + + public boolean isUsable() { + return usable; + } + + public TrainGroup getTrainGroup() { + return trainGroup; + } + + public TrainLine getTrainLine() { + return trainLine; + } + + public TrainTravelSection nextSection() { + return nextSection.get(); + } + + public TrainTravelSection previousSection() { + return previousSection.get(); + } + + /** + * Creates a list of all stops assigned to this section. + * @param startingAtIndex The schedule index from which found stops should be added, or {@code < 0} to get all elements of the section. + * @return A list of all stops assigned to this section. + */ + public List getPredictions(int startingAtIndex, boolean ignoreIncludeLastStationRule) { + if (data.getTrain() == null || data.getTrain().runtime == null || data.getTrain().runtime.getSchedule() == null) { + return List.of(); + } + List result = new ArrayList<>(); + TrainTravelSection nextSection = nextSection(); + Map predictions = data.getPredictionsRaw().entrySet().stream().filter(x -> !GlobalSettings.getInstance().isStationBlacklisted(x.getValue().getStationName())).collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); + final int startIndex = getScheduleIndex(); + final int stopIndex = nextSection.getScheduleIndex(); + final int count = data.getTrain().runtime.getSchedule().entries.size(); + + boolean customStartFound = false; + boolean endReached = false; + TrainPrediction pred = null; + for (int i = 0; i < count * 2; i++) { + final int j = (startIndex + i) % count; + if (i != 0 && j == stopIndex) { + if (!ignoreIncludeLastStationRule && shouldIncludeNextStationOfNextSection()) { + endReached = true; + } else return result; + } + customStartFound = customStartFound || startingAtIndex < 0 || j == startingAtIndex; + if (!predictions.containsKey(j) || !customStartFound) continue; + pred = predictions.get(j); + result.add(pred); + if (endReached) break; + } + return result; + } + + public int getFirstIndexFor(StationTag tag) { + return getPredictions(INVALID, false).stream().filter(x -> x.getStationTag().equals(tag)).map(x -> x.getEntryIndex()).findFirst().orElse(0); + } + + /** + * Creates a list as a route with all stations in this section. + * @param simulationTime How far ahead the predictions should be calculated. + * @param currentIndex The schedule index of the current station as a reference point. The route is calculated so that the station with the specified index fits with it's timing into the generated route. + * @return A list as a route with all stations in this section + */ + public List getAllStops(long simulationTime, int currentIndex) { + List result = new ArrayList<>(); + List predictions = getPredictions(INVALID, false); + + TrainStop lastStop = null; + for (TrainPrediction prediction : predictions) { + TrainStop stop = new TrainStop(prediction); + stop.simulateTicks(simulationTime); + if (lastStop != null && lastStop.getScheduledArrivalTime() > stop.getScheduledArrivalTime()) { + if (prediction.getEntryIndex() == currentIndex) { + result.forEach(x -> x.simulateCycles(-1)); + } else { + stop.simulateCycles(1); + } + } + result.add(stop); + lastStop = stop; + } + return result; + } + + public List getStopovers() { + List predictions = this.predictions.get(); + return predictions.stream().limit(predictions.size() - 1).skip(1).map(x -> x.getStationTag().getTagName().get()).toList(); /* TODO */ + } + + public List getStopoversFrom(int startIndex) { + List predictions = new ArrayList<>(); + boolean startFound = false; + for (int i = 0; i < this.predictions.get().size() - 1; i++) { + TrainPrediction prediction = this.predictions.get().get(i); + boolean wasStartFound = startFound; + if (prediction.getEntryIndex() == startIndex) startFound = true; + if (!wasStartFound) continue; + predictions.add(prediction.getStationTag().getTagName().get()); + } + return predictions; + } + + public Optional getFinalStop() { + List predictions = this.predictions.get(); + return predictions.isEmpty() ? Optional.empty() : Optional.ofNullable(predictions.get(predictions.size() - 1)); + } + + public boolean isFinalStop(TrainPrediction prediction) { + Optional pred = getFinalStop(); + return pred.isPresent() && pred.get() == prediction; + } + + public boolean isFirstStop(TrainPrediction prediction) { + Optional pred = getFirstStop(); + return pred.isPresent() && pred.get() == prediction; + } + + public boolean isFinalStop(int scheduleIndex) { + Optional pred = getFinalStop(); + return pred.isPresent() && pred.get().getEntryIndex() == scheduleIndex; + } + + public boolean isFirstStop(int scheduleIndex) { + Optional pred = getFirstStop(); + return pred.isPresent() && pred.get().getEntryIndex() == scheduleIndex; + } + + public Optional getFirstStop() { + List predictions = this.predictions.get(); + return predictions.isEmpty() ? Optional.empty() : Optional.ofNullable(predictions.get(0)); + } + + public Optional getNextStop() { + List predictions = this.predictions.get(); + return predictions.isEmpty() ? Optional.empty() : Optional.ofNullable(predictions.get(0)); + } + + public String getDisplayText() { + if (!isUsable()) { + return ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service").getString(); + } + return getFinalStop().map(x -> GlobalSettings.getInstance().getOrCreateStationTagFor(x.getStationName()).getTagName().get()).orElse("?"); + } + + public String getDisplayTextStart() { + return !isUsable() ? ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service").getString() : getFirstStop().map(x -> GlobalSettings.getInstance().getOrCreateStationTagFor(x.getStationName()).getTagName().get()).orElse("?"); + } + + public String getStartStationName() { + return getFirstStop().map(x -> x.getStationName()).orElse("?"); + } + + public String getDestinationStationName() { + return getFinalStop().map(x -> x.getStationName()).orElse("?"); + } + + @Override + public String toString() { + return getDisplayText(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java new file mode 100644 index 00000000..a6936c5c --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java @@ -0,0 +1,253 @@ +package de.mrjulsen.crn.data.train; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.simibubi.create.Create; +import com.simibubi.create.content.decoration.slidingDoor.DoorControlBehaviour; +import com.simibubi.create.content.trains.GlobalRailwayManager; +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData; +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; +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.signal.SignalBoundary; +import com.simibubi.create.content.trains.signal.TrackEdgePoint; +import com.simibubi.create.content.trains.station.GlobalStation; +import com.simibubi.create.content.trains.station.StationBlockEntity; +import com.simibubi.create.foundation.utility.Couple; + +import de.mrjulsen.crn.data.NearestTrackStationResult; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TrainExitSide; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.crn.data.navigation.TrainSchedule; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import de.mrjulsen.mcdragonlib.util.MathUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +public final class TrainUtils { + + public static GlobalRailwayManager getRailwayManager() { + return Create.RAILWAYS; + } + + /** + * Get data about all trains and when they arrive where. + * @return a Map where the key is the station name and the value is a list of data from all trains that will arrive at this stations. + */ + public static Map> allPredictionsRaw() { + return new HashMap<>(GlobalTrainDisplayData.statusByDestination); + } + + public static boolean isStationKnown(String station) { + return allPredictionsRaw().keySet().stream().anyMatch(x -> TrainUtils.stationMatches(station, x)); + } + + /** + * A list of all stations in the world. + * @return a list containing all track stations. + */ + public static Collection getAllStations() { + final Collection stations = new ArrayList<>(); + getRailwayManager().trackNetworks.forEach((uuid, graph) -> { + Collection foundStations = graph.getPoints(EdgePointType.STATION); + stations.addAll(foundStations); + }); + return stations; + } + + public static Optional getTrain(UUID trainId) { + return Optional.ofNullable(getRailwayManager().trains.get(trainId)); + } + + public static Set getTrainIds() { + return new HashSet<>(getRailwayManager().trains.keySet()); + } + + public static Set getTrains(boolean onlyValid) { + return new HashSet<>(getRailwayManager().trains.values().stream().filter(x -> !onlyValid || isTrainValid(x)).toList()); + } + + public static Set getAllSignals() { + return new HashSet<>(getRailwayManager().trackNetworks.values().stream().flatMap(x -> x.getPoints(EdgePointType.SIGNAL).stream()).toList()); + } + + public static Set getDepartingTrainsAt(StationTag station) { + return ImmutableMap.copyOf(GlobalTrainDisplayData.statusByDestination).entrySet().stream().filter(x -> station.contains(x.getKey())).flatMap(x -> x.getValue().stream()).map(x -> x.train).collect(Collectors.toSet()); + } + + public static Set getDepartingTrainsAt(String station) { + return ImmutableMap.copyOf(GlobalTrainDisplayData.statusByDestination).entrySet().stream().filter(x -> station.equals(x.getKey())).flatMap(x -> x.getValue().stream()).map(x -> x.train).collect(Collectors.toSet()); + } + + public static List getDeparturesAt(StationTag station, UUID selfTrain) { + return getDeparturesAt(x -> x.getStationTag().equals(station), selfTrain); + } + + public static List getDeparturesAtStationName(String stationName, UUID selfTrain) { + return getDeparturesAt(x -> TrainUtils.stationMatches(x.getStationName(), stationName), selfTrain); + } + + public static List getDeparturesAt(Predicate stationFilter, UUID selfTrain) { + + MutableSingle selfSchedule = new MutableSingle(null); + TrainUtils.getTrain(selfTrain).ifPresent(x -> { + selfSchedule.setFirst(new TrainSchedule(TrainListener.data.containsKey(x.id) ? TrainListener.data.get(x.id).getSessionId() : new UUID(0, 0), x)); + }); + + List list = TrainListener.data.values().stream() + .filter(x -> !x.getTrainId().equals(selfTrain) && TrainUtils.isTrainUsable(x.getTrain())) + .flatMap(x -> x.getPredictions().stream().filter(stationFilter)) + .map(TrainStop::new) + .filter(x -> { + if (selfSchedule.getFirst() != null) { + return true; + } + Optional train = TrainUtils.getTrain(x.getTrainId()); + if (!train.isPresent()) { + return false; + } + TrainSchedule sched = new TrainSchedule(TrainListener.data.containsKey(train.get().id) ? TrainListener.data.get(train.get().id).getSessionId() : new UUID(0, 0), train.get()); + return !sched.isEqual(selfSchedule.getFirst()); + }) + .sorted((a, b) -> Long.compare(a.getScheduledDepartureTime(), b.getScheduledDepartureTime())) + .toList(); + + List results = new ArrayList<>(); + Set usedTrains = new HashSet<>(); + usedTrains.add(selfTrain); + for (TrainStop stop : list) { + if (!TrainListener.data.containsKey(stop.getTrainId())) continue; + TrainData data = TrainListener.data.get(stop.getTrainId()); + TrainTravelSection section = data.getSectionByIndex(stop.getSectionIndex()); + if (!section.isUsable() && !(section.isFirstStop(stop.getScheduleIndex()) && section.previousSection().isUsable() && section.previousSection().shouldIncludeNextStationOfNextSection())) { + continue; + } + + if (!usedTrains.contains(stop.getTrainId())) { + usedTrains.add(stop.getTrainId()); + results.add(stop); + } + } + + return results; + } + + public static Set isSignalOccupied(UUID signalId, Set excludedTrains) { + Optional signal = getAllSignals().stream().filter(x -> x.getId().equals(signalId)).findFirst(); + if (!signal.isPresent()) { + return Set.of(); + } + + Set occupyingTrains = getTrains(false).stream().filter(x -> !excludedTrains.contains(x.id) && x.occupiedSignalBlocks.keySet().stream().anyMatch(y -> y.equals(signal.get().groups.getFirst()) || y.equals(signal.get().groups.getSecond()))).collect(Collectors.toSet()); + return occupyingTrains; + } + + + public static NearestTrackStationResult getNearestTrackStation(Level level, Vec3i pos) { + Optional station = getAllStations().stream().filter(x -> + isStationKnown(x.name) && + x.getBlockEntityDimension().equals(level.dimension()) && + !GlobalSettings.getInstance().isStationBlacklisted(x.name) + ).min((a, b) -> Double.compare(a.getBlockEntityPos().distSqr(pos), b.getBlockEntityPos().distSqr(pos))); + + double distance = station.isPresent() ? station.get().getBlockEntityPos().distSqr(pos) : 0; + return new NearestTrackStationResult(station, distance); + } + + public static TrainExitSide getTrainStationExit(GlobalStation station, Direction stationDirection, Level level) { + DoorControlBehaviour dcb = getTrainStationDoorControl(station, level); + if (dcb == null) { + return TrainExitSide.UNKNOWN; + } + + if (dcb.mode.matches(stationDirection.getClockWise())) { + return TrainExitSide.RIGHT; + } else if (dcb.mode.matches(stationDirection.getCounterClockWise())) { + return TrainExitSide.LEFT; + } + return TrainExitSide.UNKNOWN; + } + + public static DoorControlBehaviour getTrainStationDoorControl(GlobalStation station, Level level) { + BlockPos stationPos = station.getBlockEntityPos(); + if (level == null || !level.isLoaded(stationPos)) { + return null; + } + if (level.getBlockEntity(stationPos) instanceof StationBlockEntity be) { + return be.doorControls; + } + return null; + } + + + public static Optional getEdge(GlobalStation station) { + MutableSingle edge = new MutableSingle(null); + Create.RAILWAYS.trackNetworks.forEach((uuid, graph) -> { + if (edge.getFirst() != null) return; + TrackEdge e = graph.getConnection(Couple.create(graph.locateNode(station.edgeLocation.getFirst()), graph.locateNode(station.edgeLocation.getSecond()))); + if (e == null) return; + edge.setFirst(e); + }); + return Optional.ofNullable(edge.getFirst()); + } + + public static double angleOn(TrackEdgePoint point, TrackEdge edge) { + double basePos = point.isPrimary(edge.node1) ? edge.getLength() - point.position : point.position; + Vec3 vec = edge.getDirectionAt(basePos); + return point.isPrimary(edge.node1) ? MathUtils.getVectorAngle(vec) : MathUtils.getVectorAngle(vec.reverse()); + } + + public static TrainExitSide getExitSide(GlobalStation station) { + Level level = ModCommonEvents.getPhysicalLevel(); + if (level == null || station == null || !level.isLoaded(station.getBlockEntityPos())) { + return TrainExitSide.UNKNOWN; + } + final Optional edge = level != null ? getEdge(station) : Optional.empty(); + if (!edge.isPresent()) { + return TrainExitSide.UNKNOWN; + } + TrainExitSide side = getTrainStationExit(station, Direction.fromYRot(angleOn(station, edge.get())), level); + + return side; + } + + + public static boolean stationMatches(String stationName, String filter) { + String regex = filter.isBlank() ? filter : "\\Q" + filter.replace("*", "\\E.*\\Q"); + return stationName.matches(regex); + } + + public static boolean isTrainValid(Train train) { + return //!train.derailed && + !train.invalid && + //!train.runtime.paused && + train.runtime.getSchedule() != null && + train.graph != null + ; + } + + public static boolean isTrainUsable(Train train) { + return isTrainValid(train) && + TrainListener.data.containsKey(train.id) && + TrainListener.data.get(train.id).isInitialized() && + !TrainListener.data.get(train.id).isPreparing() + ; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java new file mode 100644 index 00000000..862b2af4 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java @@ -0,0 +1,144 @@ +package de.mrjulsen.crn.data.train.portable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import com.simibubi.create.content.trains.entity.TrainIconType; + +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.event.ModCommonEvents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; + +/** Contains data about one train arrival at a specific station. This data is used by displays and does not provide any additional functionality. */ +public class BasicTrainDisplayData { + private final UUID id; + private final String name; + private final TrainIconType icon; + private final List status; + private final boolean cancelled; + + private static final String NBT_ID = "Id"; + private static final String NBT_NAME = "Name"; + private static final String NBT_ICON = "Icon"; + private static final String NBT_STATUS = "Status"; + private static final String NBT_CANCELLED = "Cancelled"; + + public BasicTrainDisplayData( + UUID id, + String name, + TrainIconType icon, + List status, + boolean cancelled + ) { + this.id = id; + this.name = name; + this.icon = icon; + this.status = status; + this.cancelled = cancelled; + } + + public static BasicTrainDisplayData empty() { + return new BasicTrainDisplayData(new UUID(0, 0), "", TrainIconType.getDefault(), List.of(), true); + } + + /** Server-side only! */ + public static BasicTrainDisplayData of(UUID train) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + if (!TrainListener.data.containsKey(train)) { + return empty(); + } + TrainData data = TrainListener.data.get(train); + return new BasicTrainDisplayData( + data.getTrainId(), + data.getTrainName(), + TrainIconType.getDefault(), // TODO + new ArrayList<>(data.getStatus()), + data.isCancelled() + ); + } + + public static BasicTrainDisplayData of(TrainStop stop) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + if (!TrainListener.data.containsKey(stop.getTrainId())) { + return empty(); + } + TrainData data = TrainListener.data.get(stop.getTrainId()); + return new BasicTrainDisplayData( + stop.getTrainId(), + stop.getTrainName(), + stop.getTrainIcon(), + new ArrayList<>(data.getStatus()), + data.isCancelled() + ); + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public TrainIconType getIcon() { + return icon; + } + + public List getStatus() { + return status; + } + + public boolean isCancelled() { + return cancelled; + } + + public boolean hasStatusInfo() { + return !getStatus().isEmpty(); + } + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + + ListTag statusList = new ListTag(); + statusList.addAll(status.stream().map(x -> x.toNbt()).toList()); + + nbt.putUUID(NBT_ID, id); + nbt.putString(NBT_NAME, name); + nbt.putString(NBT_ICON, icon.getId().toString()); + nbt.put(NBT_STATUS, statusList); + nbt.putBoolean(NBT_CANCELLED, cancelled); + return nbt; + } + + public static BasicTrainDisplayData fromNbt(CompoundTag nbt) { + return new BasicTrainDisplayData( + nbt.getUUID(NBT_ID), + nbt.getString(NBT_NAME), + TrainIconType.byId(new ResourceLocation(nbt.getString(NBT_ICON))), + nbt.getList(NBT_STATUS, Tag.TAG_COMPOUND).stream().map(x -> CompiledTrainStatus.fromNbt(((CompoundTag)x))).toList(), + nbt.getBoolean(NBT_CANCELLED) + ); + } + + @Override + public final boolean equals(Object obj) { + return obj instanceof BasicTrainDisplayData o && o.getId().equals(getId()); + } + + @Override + public final int hashCode() { + return Objects.hash(getId()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java new file mode 100644 index 00000000..641bacd6 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java @@ -0,0 +1,60 @@ +package de.mrjulsen.crn.data.train.portable; + +import java.util.List; +import java.util.UUID; + +import de.mrjulsen.crn.data.TagName; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.data.train.TrainUtils; +import de.mrjulsen.crn.event.ModCommonEvents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +/** Contains data about one train arrival at a specific station. This data is used by displays and does not provide any additional functionality. */ +public class NextConnectionsDisplayData { + private final List stops; + + private static final String NBT_STOPS = "Stops"; + + public NextConnectionsDisplayData( + List stops + ) { + this.stops = stops; + } + + public static NextConnectionsDisplayData empty() { + return new NextConnectionsDisplayData(List.of()); + } + + /** Server-side only! */ + public static NextConnectionsDisplayData at(String stationName, UUID selfTrainId) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + + return new NextConnectionsDisplayData( + TrainUtils.getDeparturesAt(GlobalSettings.getInstance().getOrCreateStationTagFor(TagName.of(stationName)), selfTrainId).stream().map(x -> TrainStopDisplayData.of(x)).toList() + ); + } + public List getConnections() { + return stops; + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + + ListTag list = new ListTag(); + list.addAll(stops.stream().map(x -> x.toNbt()).toList()); + + nbt.put(NBT_STOPS, list); + return nbt; + } + + public static NextConnectionsDisplayData fromNbt(CompoundTag nbt) { + return new NextConnectionsDisplayData( + nbt.getList(NBT_STOPS, Tag.TAG_COMPOUND).stream().map(x -> TrainStopDisplayData.fromNbt((CompoundTag)x)).toList() + ); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java new file mode 100644 index 00000000..dece556a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java @@ -0,0 +1,149 @@ +package de.mrjulsen.crn.data.train.portable; + +import java.util.List; +import java.util.Objects; + +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.event.ModCommonEvents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +public class StationDisplayData { + private final BasicTrainDisplayData trainData; + private final TrainStopDisplayData stationData; + private final String firstStopName; + private final boolean isLastStop; + private final List stopovers; + + private static final String NBT_TRAIN = "Train"; + private static final String NBT_STATION = "Station"; + private static final String NBT_STOPOVERS = "Stopovers"; + private static final String NBT_FIRST_STOP = "FirstStop"; + private static final String NBT_IS_LAST = "IsLast"; + + + + public StationDisplayData( + BasicTrainDisplayData trainData, + TrainStopDisplayData stationData, + String firstStopName, + boolean isLastStop, + List stopovers + ) { + this.trainData = trainData; + this.stationData = stationData; + this.stopovers = stopovers; + this.firstStopName = firstStopName; + this.isLastStop = isLastStop; + } + + public static StationDisplayData empty() { + return new StationDisplayData(BasicTrainDisplayData.empty(), TrainStopDisplayData.empty(), "", false, List.of()); + } + + /** Server-side only! */ + public static StationDisplayData of(TrainStop stop) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + if (!TrainListener.data.containsKey(stop.getTrainId())) { + return empty(); + } + TrainData data = TrainListener.data.get(stop.getTrainId()); + TrainTravelSection section = data.getSectionByIndex(stop.getSectionIndex()); + TrainTravelSection previousSection = section.previousSection(); + String firstStop = section.getFirstStop().isPresent() ? section.getFirstStop().get().getStationTag().getTagName().get() : ""; + boolean isLastStopOfSection = section.getFinalStop().isPresent() && (previousSection.shouldIncludeNextStationOfNextSection() && previousSection.getFinalStop().isPresent() ? previousSection.getFinalStop().get() : section.getFinalStop().get()).getEntryIndex() == stop.getScheduleIndex(); + if (isLastStopOfSection) { + if (previousSection.shouldIncludeNextStationOfNextSection() && previousSection.getFinalStop().isPresent() && previousSection.getFinalStop().get().getEntryIndex() == stop.getScheduleIndex()) { + firstStop = previousSection.getFirstStop().isPresent() ? previousSection.getFirstStop().get().getStationTag().getTagName().get() : ""; + if ((data.isWaitingAtStation() && data.getCurrentScheduleIndex() == stop.getScheduleIndex()) || !previousSection.isUsable()) { + isLastStopOfSection = false; + } + if (!section.isUsable()) { + isLastStopOfSection = true; + } + } + } + return new StationDisplayData( + BasicTrainDisplayData.of(stop), + TrainStopDisplayData.of(stop), + firstStop, + isLastStopOfSection, + section.getStopoversFrom(stop.getScheduleIndex()) + ); + } + + public BasicTrainDisplayData getTrainData() { + return trainData; + } + + public TrainStopDisplayData getStationData() { + return stationData; + } + + public List getStopovers() { + return stopovers; + } + + public String getFirstStopName() { + return firstStopName; + } + + public boolean isLastStop() { + return isLastStop; + } + + public boolean isDelayed() { + return isLastStop() ? getStationData().isArrivalDelayed() : getStationData().isDepartureDelayed(); + } + + public long getScheduledTime() { + return isLastStop() ? getStationData().getScheduledArrivalTime() : getStationData().getScheduledDepartureTime(); + } + + public long getRealTime() { + return isLastStop() ? getStationData().getRealTimeArrivalTime() : getStationData().getRealTimeDepartureTime(); + } + + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + + ListTag stopoversList = new ListTag(); + stopoversList.addAll(getStopovers().stream().map(x -> StringTag.valueOf(x)).toList()); + + nbt.put(NBT_TRAIN, trainData.toNbt()); + nbt.put(NBT_STATION, stationData.toNbt()); + nbt.putString(NBT_FIRST_STOP, firstStopName); + nbt.putBoolean(NBT_IS_LAST, isLastStop); + nbt.put(NBT_STOPOVERS, stopoversList); + return nbt; + } + + public static StationDisplayData fromNbt(CompoundTag nbt) { + return new StationDisplayData( + BasicTrainDisplayData.fromNbt(nbt.getCompound(NBT_TRAIN)), + TrainStopDisplayData.fromNbt(nbt.getCompound(NBT_STATION)), + nbt.getString(NBT_FIRST_STOP), + nbt.getBoolean(NBT_IS_LAST), + nbt.getList(NBT_STOPOVERS, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList() + ); + } + + @Override + public final boolean equals(Object obj) { + return obj instanceof StationDisplayData o && o.getTrainData().equals(getTrainData()) && o.getStationData().equals(getStationData()); + } + + @Override + public final int hashCode() { + return Objects.hash(getTrainData(), getStationData()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java new file mode 100644 index 00000000..f26a097d --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java @@ -0,0 +1,208 @@ +package de.mrjulsen.crn.data.train.portable; + +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.data.TrainExitSide; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.data.train.TrainUtils; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +public class TrainDisplayData { + private final BasicTrainDisplayData trainData; + private final List stops; + private final int currentScheduleIndex; + private final double speed; + private final boolean oppositeDirection; + private final TrainExitSide exitSide; + private final boolean isWaitingAtStation; + private final boolean empty; + + private final Cache> stopsFromHere; + private final Cache> stopovers; + + private static final String NBT_TRAIN = "Train"; + private static final String NBT_STOPS = "Stops"; + private static final String NBT_INDEX = "CurrentIndex"; + private static final String NBT_SPEED = "Speed"; + private static final String NBT_OPPOSITE_DIRECTION = "Opposite"; + private static final String NBT_EXIT_SIDE = "ExitSide"; + private static final String NBT_AT_STATION = "AtStation"; + private static final String NBT_IS_EMPTY = "Empty"; + + + private TrainDisplayData() { + this.trainData = BasicTrainDisplayData.empty(); + this.stops = List.of(); + this.currentScheduleIndex = -1; + this.speed = 0; + this.oppositeDirection = false; + this.exitSide = TrainExitSide.UNKNOWN; + this.stopsFromHere = new Cache<>(() -> List.of()); + this.stopovers = new Cache<>(() -> List.of()); + this.isWaitingAtStation = false; + this.empty = true; + } + + public TrainDisplayData( + BasicTrainDisplayData trainData, + List stops, + int currentScheduleIndex, + TrainExitSide exitSide, + double speed, + boolean oppositeDirection, + boolean isWaitingAtStation, + boolean empty + ) { + this.trainData = trainData; + this.stops = stops; + this.currentScheduleIndex = currentScheduleIndex; + this.speed = speed; + this.oppositeDirection = oppositeDirection; + this.exitSide = exitSide; + this.isWaitingAtStation = isWaitingAtStation; + this.stopsFromHere = new Cache<>(() -> { + boolean startFound = false; + List list = new ArrayList<>(); + for (TrainStopDisplayData stop : getAllStops()) { + if (stop.getStationEntryIndex() == getCurrentScheduleIndex()) startFound = true; + if (!startFound) continue; + list.add(stop); + } + return list; + }); + this.stopovers = new Cache<>(() -> getStopsFromCurrentStation().size() > 2 ? getStopsFromCurrentStation().stream().limit(getStopsFromCurrentStation().size() - 1).skip(1).toList() : List.of()); + this.empty = empty; + } + + public static TrainDisplayData empty() { + return new TrainDisplayData(); + } + + /** Server-side only! */ + public static TrainDisplayData of(Train train) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + if (!TrainListener.data.containsKey(train.id) || train.runtime.getSchedule() == null) { + return empty(); + } + + MutableSingle sideHolder = new MutableSingle<>(null); + ModCommonEvents.getCurrentServer().ifPresent(x -> { + x.execute(() -> sideHolder.setFirst(TrainUtils.getExitSide(train.navigation.destination))); + while (sideHolder.getFirst() == null) { + try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) {} + } + }); + TrainExitSide side = sideHolder.getFirst() == null ? TrainExitSide.UNKNOWN : sideHolder.getFirst(); + + TrainData data = TrainListener.data.get(train.id); + TrainTravelSection section = data.getCurrentSection(); + return new TrainDisplayData( + BasicTrainDisplayData.of(train.id), + section.isUsable() ? data.getCurrentSection().getPredictions(-1, false).stream().map(x -> TrainStopDisplayData.of(new TrainStop(x))).toList() : List.of(), + data.getCurrentScheduleIndex(), + side, + train.speed, + train.currentlyBackwards, + data.isWaitingAtStation(), + !section.isUsable() + ); + } + + public BasicTrainDisplayData getTrainData() { + return trainData; + } + + public List getAllStops() { + return stops; + } + + public List getStopsFromCurrentStation() { + return stopsFromHere.get(); + } + + public List getStopovers() { + return stopovers.get(); + } + + public double getSpeed() { + return speed; + } + + public boolean isOppositeDirection() { + return oppositeDirection; + } + + public TrainExitSide getNextStopExitSide() { + return exitSide; + } + + public boolean isWaitingAtStation() { + return isWaitingAtStation; + } + + public boolean isEmpty() { + return empty; + } + + public int getCurrentScheduleIndex() { + return currentScheduleIndex; + } + + public Optional getNextStop() { + return !getStopsFromCurrentStation().isEmpty() ? Optional.of(getStopsFromCurrentStation().get(0)) : Optional.empty(); + } + + public Optional getLastStop() { + return !getStopsFromCurrentStation().isEmpty() ? Optional.of(getStopsFromCurrentStation().get(getStopsFromCurrentStation().size() - 1)) : Optional.empty(); + } + + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + + ListTag stopsList = new ListTag(); + stopsList.addAll(getAllStops().stream().map(x -> x.toNbt()).toList()); + + nbt.put(NBT_TRAIN, trainData.toNbt()); + nbt.put(NBT_STOPS, stopsList); + nbt.putInt(NBT_INDEX, currentScheduleIndex); + nbt.putDouble(NBT_SPEED, speed); + nbt.putBoolean(NBT_OPPOSITE_DIRECTION, oppositeDirection); + nbt.putByte(NBT_EXIT_SIDE, exitSide.getAsByte()); + nbt.putBoolean(NBT_AT_STATION, isWaitingAtStation); + nbt.putBoolean(NBT_IS_EMPTY, isEmpty()); + return nbt; + } + + public static TrainDisplayData fromNbt(CompoundTag nbt) { + if (nbt.getBoolean(NBT_IS_EMPTY)) { + return new TrainDisplayData(); + } + return new TrainDisplayData( + BasicTrainDisplayData.fromNbt(nbt.getCompound(NBT_TRAIN)), + nbt.getList(NBT_STOPS, Tag.TAG_COMPOUND).stream().map(x -> TrainStopDisplayData.fromNbt((CompoundTag)x)).toList(), + nbt.getInt(NBT_INDEX), + TrainExitSide.getFromByte(nbt.getByte(NBT_EXIT_SIDE)), + nbt.getDouble(NBT_SPEED), + nbt.getBoolean(NBT_OPPOSITE_DIRECTION), + nbt.getBoolean(NBT_AT_STATION), + nbt.getBoolean(NBT_IS_EMPTY) + ); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java new file mode 100644 index 00000000..7414ba06 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java @@ -0,0 +1,170 @@ +package de.mrjulsen.crn.data.train.portable; + +import java.util.Objects; +import de.mrjulsen.crn.config.ModCommonConfig; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.event.ModCommonEvents; +import net.minecraft.nbt.CompoundTag; + +/** Contains data about one train arrival at a specific station. This data is used by displays and does not provide any additional functionality. */ +public class TrainStopDisplayData { + private final int stationEntryIndex; + private final String name; + private final long scheduledDepartureTime; + private final long scheduledArrivalTime; + private final long realTimeDepartureTime; + private final long realTimeArrivalTime; + private final String destination; + private final String trainName; + private final StationInfo stationInfo; + + private static final String NBT_STATION_INDEX = "Index"; + private static final String NBT_NAME = "Name"; + private static final String NBT_SCHEDULED_DEPARTURE_TIME = "ScheduledDeparture"; + private static final String NBT_SCHEDULED_ARRIVAL_TIME = "ScheduledArrival"; + private static final String NBT_REAL_TIME_DEPARTURE_TIME = "RealTimeArrival"; + private static final String NBT_REAL_TIME_ARRIVAL_TIME = "RealTimeDeparture"; + private static final String NBT_DESTINATION = "Destination"; + private static final String NBT_TRAIN_NAME = "TrainName"; + private static final String NBT_STATION_INFO = "StationInfo"; + + public TrainStopDisplayData( + int stationEntryIndex, + String name, + long scheduledDepartureTime, + long scheduledArrivalTime, + long realTimeDepartureTime, + long realTimeArrivalTime, + String destination, + String trainName, + StationInfo stationInfo + ) { + this.stationEntryIndex = stationEntryIndex; + this.name = name; + this.scheduledDepartureTime = scheduledDepartureTime; + this.scheduledArrivalTime = scheduledArrivalTime; + this.realTimeDepartureTime = realTimeDepartureTime; + this.realTimeArrivalTime = realTimeArrivalTime; + this.destination = destination; + this.trainName = trainName; + this.stationInfo = stationInfo; + } + + public static TrainStopDisplayData empty() { + return new TrainStopDisplayData(-1, "", 0, 0, 0, 0, "", "", StationInfo.empty()); + } + + /** Server-side only! */ + public static TrainStopDisplayData of(TrainStop stop) throws RuntimeSideException { + if (!ModCommonEvents.hasServer()) { + throw new RuntimeSideException(false); + } + return new TrainStopDisplayData( + stop.getScheduleIndex(), + stop.getRealTimeStationTag().tagName(), + stop.getScheduledDepartureTime(), + stop.getScheduledArrivalTime(), + stop.getRealTimeDepartureTime(), + stop.getRealTimeArrivalTime(), + stop.getDisplayTitle(),//stop.getRealTimeStationTag().stationName(), + stop.getTrainName(), + stop.getRealTimeStationTag().info() + ); + } + + public int getStationEntryIndex() { + return stationEntryIndex; + } + + public String getName() { + return name; + } + + public long getScheduledDepartureTime() { + return scheduledDepartureTime; + } + + public long getScheduledArrivalTime() { + return scheduledArrivalTime; + } + + public long getRealTimeDepartureTime() { + return realTimeDepartureTime; + } + + public long getRealTimeArrivalTime() { + return realTimeArrivalTime; + } + + public String getDestination() { + return destination; + } + + public StationInfo getStationInfo() { + return stationInfo; + } + + public String getTrainName() { + return trainName; + } + + + + + public long getDepartureTimeDeviation() { + return getRealTimeDepartureTime() - getScheduledDepartureTime(); + } + + public long getArrivalTimeDeviation() { + return getRealTimeArrivalTime() - getScheduledArrivalTime(); + } + + public boolean isDepartureDelayed() { + return getDepartureTimeDeviation() > ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get(); + } + + public boolean isArrivalDelayed() { + return getArrivalTimeDeviation() > ModCommonConfig.SCHEDULE_DEVIATION_THRESHOLD.get(); + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + + nbt.putInt(NBT_STATION_INDEX, stationEntryIndex); + nbt.putString(NBT_NAME, name); + nbt.putLong(NBT_SCHEDULED_DEPARTURE_TIME, scheduledDepartureTime); + nbt.putLong(NBT_SCHEDULED_ARRIVAL_TIME, scheduledArrivalTime); + nbt.putLong(NBT_REAL_TIME_DEPARTURE_TIME, realTimeDepartureTime); + nbt.putLong(NBT_REAL_TIME_ARRIVAL_TIME, realTimeArrivalTime); + nbt.putString(NBT_DESTINATION, destination); + nbt.putString(NBT_TRAIN_NAME, trainName); + nbt.put(NBT_STATION_INFO, stationInfo.toNbt()); + return nbt; + } + + public static TrainStopDisplayData fromNbt(CompoundTag nbt) { + return new TrainStopDisplayData( + nbt.getInt(NBT_STATION_INDEX), + nbt.getString(NBT_NAME), + nbt.getLong(NBT_SCHEDULED_DEPARTURE_TIME), + nbt.getLong(NBT_SCHEDULED_ARRIVAL_TIME), + nbt.getLong(NBT_REAL_TIME_DEPARTURE_TIME), + nbt.getLong(NBT_REAL_TIME_ARRIVAL_TIME), + nbt.getString(NBT_DESTINATION), + nbt.getString(NBT_TRAIN_NAME), + StationInfo.fromNbt(nbt.getCompound(NBT_STATION_INFO)) + ); + } + + @Override + public final boolean equals(Object obj) { + return obj instanceof TrainStopDisplayData o && o.getDestination().equals(getDestination()) && o.getStationEntryIndex() == getStationEntryIndex(); + } + + @Override + public final int hashCode() { + return Objects.hash(getDestination(), getStationEntryIndex()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java b/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java new file mode 100644 index 00000000..a46bebf8 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java @@ -0,0 +1,127 @@ +package de.mrjulsen.crn.debug; + +import org.lwjgl.glfw.GLFW; + +import com.simibubi.create.content.trains.entity.Carriage; + +import java.lang.StringBuilder; +import java.util.Map; + +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.mixin.TrainStatusAccessor; +import de.mrjulsen.crn.util.ESpeedUnit; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.mcdragonlib.client.OverlayManager; +import de.mrjulsen.mcdragonlib.client.gui.DLOverlayScreen; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public class DebugOverlay extends DLOverlayScreen { + + private static long debugOverlayId = -1; + + public static void toggle() { + OverlayManager.remove(debugOverlayId); + if (debugOverlayId == -1) { + debugOverlayId = OverlayManager.add(new DebugOverlay()); + } else { + debugOverlayId = -1; + } + } + + int line = 0; + int trainIndex = 0; + + int lastKey = -1; + + @Override + public void render(Graphics graphics, float partialTicks, int screenWidth, int screenHeight) { + graphics.poseStack().pushPose(); + graphics.poseStack().scale(0.75f, 0.75f, 0.75f); + line = 0; + if (!TrainListener.data.isEmpty()) { + trainIndex %= TrainListener.data.size(); + TrainData data = TrainListener.data.values().stream().skip(trainIndex).findFirst().get(); + drawLine(graphics, data.getTrain().name.getString() + " (" + (data.isPreparing() ? "PREPARING" : (data.isInitialized() ? "READY" : "INITIALIZING")) + ") SessionId: " + data.getSessionId()); + drawLine(graphics, "Display: " + data.getCurrentTravelSection().getDisplayText() + ", Title: " + data.getCurrentTitle() + ", IsDynamic: " + data.isDynamic()); + drawLine(graphics, "Track: " + !((TrainStatusAccessor)data.getTrain().status).crn$track() + ", Conductor: " + !((TrainStatusAccessor)data.getTrain().status).crn$conductor() + ", Navigation: " + !((TrainStatusAccessor)data.getTrain().status).crn$navigation() + ", Paused: " + data.getTrain().runtime.paused + ", Auto: " + data.getTrain().runtime.isAutoSchedule + ", Manual: " + data.isManualControlled + ", Cancelled: " + data.isCancelled()); + drawLine(graphics, "Duration: " + data.getTotalDuration() + ", Ticks: " + data.getTransitTicks() + "/" + data.waitingAtStationTicks() + "/" + data.waitingForSignalTicks + ", Dest: " + (data.getTrain().navigation.destination == null ? "(at station)" : (data.getTrain().navigation.destination.name + ", DestID: " + data.getTrain().navigation.destination.id)) + ", Delay: " + data.getHighestDeviation() + " (-" + data.getDeviationDelayOffset() + "), Status: " + data.debug_statusInfoCount()); + data.getPredictions().forEach(a -> { + drawLine(graphics, TextUtils.text(" - ").append(a.formattedText())); + }); + + StringBuilder builder = new StringBuilder(); + builder.append("( " + data.getCurrentScheduleIndex() + " ): "); + data.getPredictionsChronologically().forEach(a -> { + if (a == null) { + return; + } + builder.append(" > " + a.getStationName() + " (" + a.getRealTimeArrivalTicks() + ")"); + }); + drawLine(graphics, builder.toString()); + + boolean stalled= false; + for (int i = 0; i < data.getTrain().carriages.size(); i++) { + Carriage carriage = data.getTrain().carriages.get(i); + if (carriage.stalled) { + stalled = true; + break; + } + } + + drawLine(graphics, TextUtils.text("Speed: " + (int)ModUtils.calcSpeed(data.getTrain().speed, ESpeedUnit.MS) + " / " + (int)ModUtils.calcSpeed(data.getTrain().targetSpeed, ESpeedUnit.MS) + ", Waiting: " + (data.getTrain().navigation.waitingForSignal == null ? "No" : data.getTrain().navigation.waitingForSignal.getSecond()) + " (" + data.getTrain().navigation.ticksWaitingForSignal + "/" + data.waitingForSignalId + "), Stalled: " + stalled + " (" + data.getTrain().speedBeforeStall + ")").withStyle(data.getTrain().navigation.waitingForSignal == null ? ChatFormatting.RESET : ChatFormatting.RED)); + + for (Map.Entry transitTime : data.currentTransitTime.entrySet()) { + String suffix = "?"; + if (data.transitTimeHistory.containsKey(transitTime.getKey())) { + suffix = String.join(" | ", data.transitTimeHistory.get(transitTime.getKey()).stream().map(x -> String.valueOf(x)).toList()); + } + drawLine(graphics, " - [ " + transitTime.getKey() + " ]: " + transitTime.getValue() + " > " + suffix); + } + + drawLine(graphics, "Sections:"); + for (TrainTravelSection section : data.getSections()) { + drawLine(graphics, " - [ " + section.getScheduleIndex() + " ]: " + section.getDisplayText() + " (" + section.getStartStationName() + " -> " + section.getDestinationStationName() + "), Group: " + (section.getTrainGroup() == null ? "none" : section.getTrainGroup().getGroupName()) + ", Line: " + (section.getTrainLine() == null ? "none" : section.getTrainLine().getLineName()) + ", Include: " + section.shouldIncludeNextStationOfNextSection() + ", Navigable: " + section.isUsable() + ", Next: " + section.nextSection().getScheduleIndex()); + } + } + drawLine(graphics, TextUtils.text("Press K to switch train.").withStyle(ChatFormatting.AQUA)); + + graphics.poseStack().popPose(); + } + + private void drawLine(Graphics graphics, String str) { + drawLine(graphics, TextUtils.text(str)); + } + + private void drawLine(Graphics graphics, Component str) { + int x = 2; + int y = 2; + GuiUtils.fill(graphics, x - 1, y - 1 + line * (getFont().lineHeight + 2), getFont().width(str) + 2, getFont().lineHeight + 2, 0x44000000); + GuiUtils.drawString(graphics, getFont(), x, y + 1 + line * (getFont().lineHeight + 2), str, 0xFFFFFFFF, EAlignment.LEFT, true); + line++; + } + + @Override + public boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) { + if (lastKey != pKeyCode) { + lastKey = pKeyCode; + } else if (lastKey == pKeyCode){ + lastKey = GLFW.GLFW_KEY_UNKNOWN; + if (pKeyCode == GLFW.GLFW_KEY_K) { + trainIndex++; + return true; + } + } + if (pKeyCode == GLFW.GLFW_KEY_P) { + return true; + } + return super.keyPressed(pKeyCode, pScanCode, pModifiers); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/debug/TrainDebugData.java b/common/src/main/java/de/mrjulsen/crn/debug/TrainDebugData.java new file mode 100644 index 00000000..4d77bb39 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/debug/TrainDebugData.java @@ -0,0 +1,61 @@ +package de.mrjulsen.crn.debug; + +import java.util.UUID; + +import de.mrjulsen.crn.data.train.TrainData; +import net.minecraft.nbt.CompoundTag; + +public record TrainDebugData( + UUID sessionId, + UUID trainId, + String trainName, + int totalDuration, + int predictionsCount, + int predictionsInitialized, + TrainDebugState state +) { + + private static final String NBT_SESSION_ID = "SessionId"; + private static final String NBT_ID = "Id"; + private static final String NBT_NAME = "Name"; + private static final String NBT_DURATION = "Duration"; + private static final String NBT_PREDICTIONS = "Predictions"; + private static final String NBT_INITIALIZED_PREDICTIONS = "InitializedPredictions"; + private static final String NBT_STATE = "State"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putUUID(NBT_SESSION_ID, sessionId); + nbt.putUUID(NBT_ID, trainId); + nbt.putString(NBT_NAME, trainName); + nbt.putInt(NBT_DURATION, totalDuration); + nbt.putInt(NBT_PREDICTIONS, predictionsCount); + nbt.putInt(NBT_INITIALIZED_PREDICTIONS, predictionsInitialized); + nbt.putByte(NBT_STATE, state.getId()); + return nbt; + } + + public static TrainDebugData fromNbt(CompoundTag nbt) { + return new TrainDebugData( + nbt.getUUID(NBT_SESSION_ID), + nbt.getUUID(NBT_ID), + nbt.getString(NBT_NAME), + nbt.getInt(NBT_DURATION), + nbt.getInt(NBT_PREDICTIONS), + nbt.getInt(NBT_INITIALIZED_PREDICTIONS), + TrainDebugState.getStateById(nbt.getByte(NBT_STATE)) + ); + } + + public static TrainDebugData fromTrain(TrainData train) { + return new TrainDebugData( + train.getSessionId(), + train.getTrainId(), + train.getTrainName(), + train.getTotalDuration(), + train.getPredictionsRaw().size(), + train.debug_initializedStationsCount(), + train.isPreparing() ? TrainDebugState.PREPARING : (train.isInitialized() ? TrainDebugState.READY : TrainDebugState.INITIALIZING) + ); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/debug/TrainDebugState.java b/common/src/main/java/de/mrjulsen/crn/debug/TrainDebugState.java new file mode 100644 index 00000000..9d2e4649 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/debug/TrainDebugState.java @@ -0,0 +1,44 @@ +package de.mrjulsen.crn.debug; + +import java.util.Arrays; + +import de.mrjulsen.crn.Constants; +import net.minecraft.util.StringRepresentable; + +public enum TrainDebugState implements StringRepresentable { + PREPARING((byte)-1, "Preparing", Constants.COLOR_DELAYED), + INITIALIZING((byte)1, "Initializing", Constants.COLOR_DELAYED), + READY((byte)0, "Ready", Constants.COLOR_ON_TIME); + + byte id; + String name; + int color; + + TrainDebugState(byte id, String name, int color) { + this.id = id; + this.name = name; + this.color = color; + } + + public byte getId() { + return id; + } + + public int getColor() { + return color; + } + + public String getName() { + return name; + } + + public static TrainDebugState getStateById(int id) { + return Arrays.stream(values()).filter(x -> x.getId() == id).findFirst().orElse(INITIALIZING); + } + + @Override + public String getSerializedName() { + return name; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/CRNClientEventsRegistryEvent.java b/common/src/main/java/de/mrjulsen/crn/event/CRNClientEventsRegistryEvent.java new file mode 100644 index 00000000..99a37499 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/CRNClientEventsRegistryEvent.java @@ -0,0 +1,12 @@ +package de.mrjulsen.crn.event; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractBuiltInCRNEvent; + +/** + * Called after all CRN client events have been registered. From this point on, all CRN client events can be accessed without any problems. + */ +public final class CRNClientEventsRegistryEvent extends AbstractBuiltInCRNEvent { + public void run() { + listeners.values().forEach(Runnable::run); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/CRNCommonEventsRegistryEvent.java b/common/src/main/java/de/mrjulsen/crn/event/CRNCommonEventsRegistryEvent.java new file mode 100644 index 00000000..e1c6c475 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/CRNCommonEventsRegistryEvent.java @@ -0,0 +1,12 @@ +package de.mrjulsen.crn.event; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractBuiltInCRNEvent; + +/** + * Called after all CRN common events have been registered. From this point on, all CRN common events can be accessed without any problems. + */ +public final class CRNCommonEventsRegistryEvent extends AbstractBuiltInCRNEvent { + public void run() { + listeners.values().forEach(Runnable::run); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/CRNEventsManager.java b/common/src/main/java/de/mrjulsen/crn/event/CRNEventsManager.java new file mode 100644 index 00000000..40202a80 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/CRNEventsManager.java @@ -0,0 +1,111 @@ +package de.mrjulsen.crn.event; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import de.mrjulsen.crn.CreateRailwaysNavigator; + +import java.util.HashMap; +import java.util.HashSet; + +public final class CRNEventsManager { + + @SuppressWarnings("rawtypes") + protected static final Map, AbstractCRNEvent> registeredEvents = new HashMap<>(); + + static { + registerEventInternal(new CRNClientEventsRegistryEvent()); + registerEventInternal(new CRNCommonEventsRegistryEvent()); + } + + /** + * Returns the instance of the given Event, if registered, otherwiese a {@code NullPointerException} will be thrown. + * @param The type of the event to return. + * @param clazz The class of the event. + * @return The event, if available. + */ + @SuppressWarnings("unchecked") + public static > T getEvent(Class clazz) { + if (registeredEvents.containsKey(clazz)) { + return (T)registeredEvents.get(clazz); + } + throw new NullPointerException("The Event " + clazz.getName() + " is not registered!"); + } + + @SuppressWarnings("unchecked") + public static > Optional getEventOptional(Class clazz) { + if (registeredEvents.containsKey(clazz)) { + return Optional.ofNullable((T)registeredEvents.get(clazz)); + } + return Optional.empty(); + } + + /** + * Registers a new event. + * @param eventInstance The instance of the new event. + */ + public static void registerEvent(Supplier> eventInstance) { + registerEvent(eventInstance.get()); + } + + /** + * Registers a new event. + * @param eventInstance The instance of the new event. + */ + public static void registerEvent(AbstractCRNEvent eventInstance) { + if (eventInstance instanceof AbstractBuiltInCRNEvent) { + throw new IllegalArgumentException("Cannot register CRN System events!"); + } + registerEventInternal(eventInstance); + } + + static void registerEventInternal(AbstractCRNEvent eventInstance) { + registeredEvents.put(eventInstance.getClass(), eventInstance); + } + + /** + * Removes all registered events. Usually done when stopping the server/world. + */ + public static void clearEvents() { + registeredEvents.values().removeIf(x -> !(x instanceof AbstractBuiltInCRNEvent)); + CreateRailwaysNavigator.LOGGER.info("All events have been closed."); + } + + /** + * Checks if the given event is registered to prevent errors. + * @param The type of the event to return. + * @param clazz The class of the event. + * @return {@true} if the event is registered. + */ + public static > boolean isRegistered(Class clazz) { + return registeredEvents.containsKey(clazz); + } + + + + public static abstract class AbstractCRNEvent implements IEvent { + protected final Map listeners = new HashMap<>(); + protected final Set idsToRemove = new HashSet<>(); + + @Override + public void register(String modid, T event) { + listeners.put(modid, event); + } + + @Override + public void unregister(String modid) { + if (listeners.containsKey(modid)) { + idsToRemove.add(modid); + } + } + + public void tickPost() { + listeners.keySet().removeAll(idsToRemove); + idsToRemove.clear(); + } + } + + static abstract class AbstractBuiltInCRNEvent extends AbstractCRNEvent {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/IEvent.java b/common/src/main/java/de/mrjulsen/crn/event/IEvent.java new file mode 100644 index 00000000..a87de698 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/IEvent.java @@ -0,0 +1,16 @@ +package de.mrjulsen.crn.event; + +public interface IEvent { + /** + * Registers a new event listener. + * @param modid The mod ID of the mod listening for this event. This does not have to be the mod ID, but it is recommended to be. + * @param event Your callback which will be executed when the event triggers. + */ + void register(String modid, T event); + + /** + * Removes the event listener. + * @param modid The mod ID of event listener to be removed. + */ + void unregister(String modid); +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/ClientEvents.java b/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java similarity index 53% rename from common/src/main/java/de/mrjulsen/crn/event/ClientEvents.java rename to common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java index cffc17f2..8951f20e 100644 --- a/common/src/main/java/de/mrjulsen/crn/event/ClientEvents.java +++ b/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java @@ -4,25 +4,27 @@ import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.client.input.ModKeys; import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; -import de.mrjulsen.crn.event.listeners.TrainListener; +import de.mrjulsen.crn.data.SavedRoutesManager; +import de.mrjulsen.crn.data.navigation.ClientTrainListener; +import de.mrjulsen.crn.event.events.DefaultTrainDataRefreshEvent; +import de.mrjulsen.crn.event.events.RouteDetailsActionsEvent; import de.mrjulsen.crn.network.InstanceManager; import de.mrjulsen.crn.registry.ModDisplayTags; import de.mrjulsen.crn.registry.ModExtras; import de.mrjulsen.mcdragonlib.client.OverlayManager; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; import dev.architectury.event.events.client.ClientGuiEvent; import dev.architectury.event.events.client.ClientLifecycleEvent; import dev.architectury.event.events.client.ClientPlayerEvent; import dev.architectury.event.events.client.ClientTickEvent; -import net.minecraft.client.Minecraft; -public class ClientEvents { +public class ModClientEvents { + + private static int tickTime; private static int langCheckerTicks = 0; + private static MutableSingle inGame = new MutableSingle(false); - @SuppressWarnings("resource") public static void init() { ClientLifecycleEvent.CLIENT_SETUP.register((mc) -> { @@ -31,46 +33,54 @@ public static void init() { }); ClientTickEvent.CLIENT_POST.register((mc) -> { - JourneyListenerManager.tick(); langCheckerTicks++; if ((langCheckerTicks %= 20) == 0) { ClientWrapper.updateLanguage(ModClientConfig.LANGUAGE.get(), false); } + + if (!inGame.getFirst()) return; + + tickTime++; + if ((tickTime %= 100) == 0) { + ClientTrainListener.tick(() -> { + CRNEventsManager.getEvent(DefaultTrainDataRefreshEvent.class).run(); + }); + } }); ClientLifecycleEvent.CLIENT_LEVEL_LOAD.register((level) -> { - ModExtras.register(); + ModExtras.init(); }); ClientPlayerEvent.CLIENT_PLAYER_JOIN.register((player) -> { - JourneyListenerManager.start(); ClientWrapper.updateLanguage(ModClientConfig.LANGUAGE.get(), true); + + // Register Events + CRNEventsManager.registerEvent(DefaultTrainDataRefreshEvent::new); + CRNEventsManager.registerEvent(RouteDetailsActionsEvent::new); + + CRNEventsManager.getEvent(CRNClientEventsRegistryEvent.class).run(); + + SavedRoutesManager.pull(true, null); + + inGame.setFirst(true); }); ClientPlayerEvent.CLIENT_PLAYER_QUIT.register((player) -> { - InstanceManager.removeRouteOverlay(); + inGame.setFirst(false); + OverlayManager.clear(); CreateRailwaysNavigator.LOGGER.info("Removed all overlays."); - - ClientTrainStationSnapshot.getInstance().dispose(); - InstanceManager.clearAll(); - JourneyListenerManager.stop(); - GlobalSettingsManager.close(); + SavedRoutesManager.removeAllRoutes(); + CRNEventsManager.clearEvents(); + InstanceManager.removeRouteOverlay(); + ClientTrainListener.clear(); }); ClientGuiEvent.DEBUG_TEXT_LEFT.register((texts) -> { - boolean b1 = TrainListener.getInstance() != null; - boolean b2 = ClientTrainStationSnapshot.getInstance() != null; - - if (Minecraft.getInstance().options.renderDebug) { - texts.add(String.format("CRN | T: %s/%s, JL: %s, O: %s, I: %s", - b1 ? TrainListener.getInstance().getListeningTrainCount() : (b2 ? ClientTrainStationSnapshot.getInstance().getListeningTrainCount() : 0), - b1 ? TrainListener.getInstance().getTotalTrainCount() : (b2 ? ClientTrainStationSnapshot.getInstance().getTrainCount() : 0), - JourneyListenerManager.getInstance() == null ? 0 : JourneyListenerManager.getInstance().getCacheSize(), - OverlayManager.count(), - InstanceManager.getInstancesCountString() - )); - } + texts.add(String.format("CRN | RL: %s", + ClientTrainListener.debug_registeredListenersCount() + )); }); } } diff --git a/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java b/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java new file mode 100644 index 00000000..992c9244 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java @@ -0,0 +1,121 @@ +package de.mrjulsen.crn.event; + +import java.util.Optional; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.AdvancedDisplayTarget; +import de.mrjulsen.crn.cmd.DebugCommand; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.event.events.CreateTrainPredictionEvent; +import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPost; +import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPre; +import de.mrjulsen.crn.event.events.ScheduleResetEvent; +import de.mrjulsen.crn.event.events.SubmitTrainPredictionsEvent; +import de.mrjulsen.crn.event.events.TotalDurationTimeChangedEvent; +import de.mrjulsen.crn.event.events.TrainArrivalAndDepartureEvent; +import de.mrjulsen.crn.event.events.TrainDestinationChangedEvent; +import de.mrjulsen.crn.registry.ModExtras; +import de.mrjulsen.crn.web.SimpleWebServer; +import de.mrjulsen.mcdragonlib.internal.ClientWrapper; +import dev.architectury.event.events.common.CommandRegistrationEvent; +import dev.architectury.event.events.common.LifecycleEvent; +import dev.architectury.event.events.common.TickEvent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; + +public class ModCommonEvents { + + private static long lastTicks = 0; + private static MinecraftServer currentServer; + + + public static void init() { + + LifecycleEvent.SETUP.register(() -> { + CreateRailwaysNavigator.LOGGER.info("Welcome to the CREATE RAILWAYS NAVIGATOR mod by MRJULSEN."); + }); + + LifecycleEvent.SERVER_LEVEL_LOAD.register((level) -> { + ModExtras.init(); + }); + + LifecycleEvent.SERVER_STARTED.register((server) -> { + currentServer = server; + // Register Events + CRNEventsManager.registerEvent(GlobalTrainDisplayDataRefreshEventPost::new); + CRNEventsManager.registerEvent(GlobalTrainDisplayDataRefreshEventPre::new); + CRNEventsManager.registerEvent(TrainDestinationChangedEvent::new); + CRNEventsManager.registerEvent(TrainArrivalAndDepartureEvent::new); + CRNEventsManager.registerEvent(SubmitTrainPredictionsEvent::new); + CRNEventsManager.registerEvent(CreateTrainPredictionEvent::new); + CRNEventsManager.registerEvent(ScheduleResetEvent::new); + CRNEventsManager.registerEvent(TotalDurationTimeChangedEvent::new); + + CRNEventsManager.getEvent(CRNCommonEventsRegistryEvent.class).run(); + + TrainListener.start(); + AdvancedDisplayTarget.start(); + + try { + SimpleWebServer.start(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + + LifecycleEvent.SERVER_STOPPING.register((server) -> { + GlobalSettings.clearInstance(); + + TrainListener.stop(); + AdvancedDisplayTarget.stop(); + CRNEventsManager.clearEvents(); + + SimpleWebServer.stop(); + }); + + LifecycleEvent.SERVER_STOPPED.register((server) -> { + currentServer = null; + }); + + LifecycleEvent.SERVER_STARTING.register((server) -> { + }); + + TickEvent.SERVER_POST.register((server) -> { + if (ModCommonEvents.hasServer()) { + long currentTicks = ModCommonEvents.getPhysicalLevel().dayTime(); + long diff = currentTicks - lastTicks; + if (Math.abs(diff) > 1) { + TrainListener.data.values().forEach(x -> x.shiftTime(diff)); + CreateRailwaysNavigator.LOGGER.info("All times have been corrected: " + (diff) + " Ticks"); + } + lastTicks = currentTicks; + } + + TrainListener.tick(); + }); + + CommandRegistrationEvent.EVENT.register((dispatcher, buildContext, selection) -> { + DebugCommand.register(dispatcher, selection); + }); + + LifecycleEvent.SERVER_LEVEL_SAVE.register((server) -> { + TrainListener.save(); + if (GlobalSettings.hasInstance()) GlobalSettings.getInstance().save(); + }); + } + + public static boolean hasServer() { + return currentServer != null; + } + + public static Optional getCurrentServer() { + return Optional.ofNullable(currentServer); + } + + public static Level getPhysicalLevel() { + return hasServer() ? getCurrentServer().get().overworld() : ClientWrapper.getClientLevel(); + } +} + + diff --git a/common/src/main/java/de/mrjulsen/crn/event/ModEvents.java b/common/src/main/java/de/mrjulsen/crn/event/ModEvents.java deleted file mode 100644 index 1190b1fd..00000000 --- a/common/src/main/java/de/mrjulsen/crn/event/ModEvents.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.mrjulsen.crn.event; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.event.listeners.TrainListener; -import de.mrjulsen.crn.network.packets.stc.TimeCorrectionPacket; -import de.mrjulsen.crn.registry.ModExtras; -import dev.architectury.event.events.common.LifecycleEvent; -import dev.architectury.event.events.common.TickEvent; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; - -public class ModEvents { - - private static long lastTicks = 0; - private static ServerLevel serverLevel; - - public static void init() { - - LifecycleEvent.SETUP.register(() -> { - CreateRailwaysNavigator.LOGGER.info("Welcome to the CREATE RAILWAYS NAVIGATOR mod by MRJULSEN."); - }); - - LifecycleEvent.SERVER_LEVEL_LOAD.register((level) -> { - ModExtras.register(); - }); - - LifecycleEvent.SERVER_STARTED.register((server) -> { - TrainListener.start(server.overworld()); - serverLevel = server.overworld(); - }); - - LifecycleEvent.SERVER_STOPPING.register((server) -> { - TrainListener.stop(); - }); - - LifecycleEvent.SERVER_STOPPED.register((server) -> { - GlobalSettingsManager.close(); - }); - - TickEvent.SERVER_POST.register((server) -> { - if (serverLevel != null) { - long currentTicks = serverLevel.dayTime(); - if (Math.abs(currentTicks - lastTicks) > 1) { - serverLevel.players().stream().filter(p -> p instanceof ServerPlayer).forEach(x -> CreateRailwaysNavigator.net().CHANNEL.sendToPlayer(x, new TimeCorrectionPacket((int)(currentTicks - lastTicks)))); - } - lastTicks = currentTicks; - } - }); - } -} - - diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/CreateTrainPredictionEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/CreateTrainPredictionEvent.java new file mode 100644 index 00000000..b390b9c3 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/CreateTrainPredictionEvent.java @@ -0,0 +1,22 @@ +package de.mrjulsen.crn.event.events; + +import java.util.Map; + +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; +import de.mrjulsen.crn.data.schedule.instruction.IStationPredictableInstruction; + +public final class CreateTrainPredictionEvent extends AbstractCRNEvent { + public void run(Train train, ScheduleRuntime schedule, Map, IStationPredictableInstruction> predictables, int index, int stayDuration, int minStayDuration, TrainDeparturePrediction prediction) { + listeners.values().forEach(x -> x.run(train, schedule, predictables, index, stayDuration, minStayDuration, prediction)); + tickPost(); + } + + @FunctionalInterface + public static interface ICreateTrainPredictionEventData { + void run(Train train, ScheduleRuntime schedule, Map, IStationPredictableInstruction> predictables, int index, int stayDuration, int minStayDuration, TrainDeparturePrediction prediction); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/DefaultTrainDataRefreshEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/DefaultTrainDataRefreshEvent.java new file mode 100644 index 00000000..193a2526 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/DefaultTrainDataRefreshEvent.java @@ -0,0 +1,11 @@ +package de.mrjulsen.crn.event.events; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public class DefaultTrainDataRefreshEvent extends AbstractCRNEvent { + public void run() { + listeners.values().forEach(Runnable::run); + tickPost(); + } +} + diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPost.java b/common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPost.java new file mode 100644 index 00000000..597f11f1 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPost.java @@ -0,0 +1,10 @@ +package de.mrjulsen.crn.event.events; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class GlobalTrainDisplayDataRefreshEventPost extends AbstractCRNEvent { + public void run() { + listeners.values().forEach(Runnable::run); + tickPost(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPre.java b/common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPre.java new file mode 100644 index 00000000..2db2d367 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/GlobalTrainDisplayDataRefreshEventPre.java @@ -0,0 +1,10 @@ +package de.mrjulsen.crn.event.events; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class GlobalTrainDisplayDataRefreshEventPre extends AbstractCRNEvent { + public void run() { + listeners.values().forEach(Runnable::run); + tickPost(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/RouteDetailsActionsEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/RouteDetailsActionsEvent.java new file mode 100644 index 00000000..23821279 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/RouteDetailsActionsEvent.java @@ -0,0 +1,23 @@ +package de.mrjulsen.crn.event.events; + +import java.util.Collection; +import java.util.List; + +import de.mrjulsen.crn.client.gui.widgets.routedetails.RoutePartWidget.RoutePartDetailsActionBuilder; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.ClientRoutePart; +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public class RouteDetailsActionsEvent extends AbstractCRNEvent { + + public Collection run(ClientRoute route, ClientRoutePart part, boolean expanded) { + List res = listeners.values().stream().flatMap(x -> x.run(route, part, expanded).stream()).toList(); + tickPost(); + return res; + } + + @FunctionalInterface + public static interface IRouteDetailsActionsEventData { + Collection run(ClientRoute route, ClientRoutePart part, boolean expanded); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/ScheduleResetEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/ScheduleResetEvent.java new file mode 100644 index 00000000..69dfaec8 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/ScheduleResetEvent.java @@ -0,0 +1,17 @@ +package de.mrjulsen.crn.event.events; + +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class ScheduleResetEvent extends AbstractCRNEvent { + public void run(Train train, boolean soft) { + listeners.values().forEach(x -> x.run(train, soft)); + tickPost(); + } + + @FunctionalInterface + public static interface IScheduleResetEventData { + void run(Train train, boolean soft); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/SubmitTrainPredictionsEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/SubmitTrainPredictionsEvent.java new file mode 100644 index 00000000..ea9afd33 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/SubmitTrainPredictionsEvent.java @@ -0,0 +1,20 @@ +package de.mrjulsen.crn.event.events; + +import java.util.Collection; + +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class SubmitTrainPredictionsEvent extends AbstractCRNEvent { + public void run(Train train, Collection predictions, int entryCount, int accumulatedTime, int current) { + listeners.values().forEach(x -> x.run(train, predictions, entryCount, accumulatedTime, current)); + tickPost(); + } + + @FunctionalInterface + public static interface ISubmitTrainPredictionsEventData { + void run(Train train, Collection predictions, int entryCount, int accumulatedTime, int current); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/TotalDurationTimeChangedEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/TotalDurationTimeChangedEvent.java new file mode 100644 index 00000000..bc5ffd36 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/TotalDurationTimeChangedEvent.java @@ -0,0 +1,16 @@ +package de.mrjulsen.crn.event.events; + +import com.simibubi.create.content.trains.entity.Train; +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class TotalDurationTimeChangedEvent extends AbstractCRNEvent { + public void run(Train train, long oldTotalDuration, long totalDuration) { + listeners.values().forEach(x -> x.run(train, oldTotalDuration, totalDuration)); + tickPost(); + } + + @FunctionalInterface + public static interface ITotalDurationTimeChangedEventData { + void run(Train train, long oldTotalDuration, long totalDuration); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java new file mode 100644 index 00000000..40bcde92 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java @@ -0,0 +1,23 @@ +package de.mrjulsen.crn.event.events; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.station.GlobalStation; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class TrainArrivalAndDepartureEvent extends AbstractCRNEvent { + public void run(Train train, GlobalStation current, boolean arrival) { + listeners.values().forEach(x -> x.run(train, current, arrival)); + tickPost(); + } + + @FunctionalInterface + public static interface ITrainApprochEventData { + /** + * @param train The current train. + * @param current The current station. + * @param departure {@code true} if the train is arriving at the current station, {@code false} when leaving. + */ + void run(Train train, GlobalStation current, boolean arrival); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/TrainDestinationChangedEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/TrainDestinationChangedEvent.java new file mode 100644 index 00000000..e6bf0b41 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/event/events/TrainDestinationChangedEvent.java @@ -0,0 +1,18 @@ +package de.mrjulsen.crn.event.events; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.station.GlobalStation; + +import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; + +public final class TrainDestinationChangedEvent extends AbstractCRNEvent { + public void run(Train train, GlobalStation current, GlobalStation next, int nextIndex) { + listeners.values().forEach(x -> x.run(train, current, next, nextIndex)); + tickPost(); + } + + @FunctionalInterface + public static interface ITrainDepartureEventData { + void run(Train train, GlobalStation current, GlobalStation next, int nextIndex); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/event/listeners/IJourneyListenerClient.java b/common/src/main/java/de/mrjulsen/crn/event/listeners/IJourneyListenerClient.java deleted file mode 100644 index 973748bc..00000000 --- a/common/src/main/java/de/mrjulsen/crn/event/listeners/IJourneyListenerClient.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.mrjulsen.crn.event.listeners; - -import java.util.UUID; - -public interface IJourneyListenerClient { - UUID getJourneyListenerClientId(); -} diff --git a/common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListener.java b/common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListener.java deleted file mode 100644 index 6c3d59c4..00000000 --- a/common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListener.java +++ /dev/null @@ -1,881 +0,0 @@ -package de.mrjulsen.crn.event.listeners; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.config.ModClientConfig; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.SimpleRoute.StationEntry; -import de.mrjulsen.crn.data.SimpleRoute.StationTag; -import de.mrjulsen.crn.data.TrainStationAlias.StationInfo; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.RealtimeRequestPacket; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.util.TextUtils; -import de.mrjulsen.mcdragonlib.util.TimeUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.network.chat.Component; - -public class JourneyListener { - - public static final int ID = 1; - private static final int REALTIME_REFRESH_TIME = 100; - - private final SimpleRoute route; - private int stationIndex = 0; - private State currentState = State.BEFORE_JOURNEY; - private int realTimeRefreshTimer = 0; - private boolean isStarted; - - private static final String keyJourneyBegins = "gui.createrailwaysnavigator.route_overview.journey_begins"; - private static final String keyJourneyBeginsWithPlatform = "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform"; - private static final String keyNextStop = "gui.createrailwaysnavigator.route_overview.next_stop"; - private static final String keyTransfer = "gui.createrailwaysnavigator.route_overview.transfer"; - private static final String keyTransferWithPlatform = "gui.createrailwaysnavigator.route_overview.transfer_with_platform"; - private static final String keyAfterJourney = "gui.createrailwaysnavigator.route_overview.after_journey"; - private static final String keyJourneyInterruptedTitle = "gui.createrailwaysnavigator.route_overview.train_canceled_title"; - private static final String keyJourneyInterrupted = "gui.createrailwaysnavigator.route_overview.train_canceled_info"; - private static final String keyConnectionMissedInfo = "gui.createrailwaysnavigator.route_overview.connection_missed_info"; - private static final String keyOptionsText = "gui.createrailwaysnavigator.route_overview.options"; - private static final String keyKeybindOptions = "key.createrailwaysnavigator.route_overlay_options"; - private static final String keyNotificationJourneyBeginsTitle = "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title"; - private static final String keyNotificationJourneyBegins = "gui.createrailwaysnavigator.route_overview.notification.journey_begins"; - private static final String keyNotificationJourneyBeginsWithPlatform = "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform"; - private static final String keyNotificationPlatformChangedTitle = "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title"; - private static final String keyNotificationPlatformChanged = "gui.createrailwaysnavigator.route_overview.notification.platform_changed"; - private static final String keyNotificationTrainDelayedTitle = "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title"; - private static final String keyNotificationTrainDelayed = "gui.createrailwaysnavigator.route_overview.notification.train_delayed"; - private static final String keyNotificationTransferTitle = "gui.createrailwaysnavigator.route_overview.notification.transfer.title"; - private static final String keyNotificationTransfer = "gui.createrailwaysnavigator.route_overview.notification.transfer"; - private static final String keyNotificationTransferWithPlatform = "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform"; - private static final String keyNotificationConnectionEndangeredTitle = "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title"; - private static final String keyNotificationConnectionEndangered = "gui.createrailwaysnavigator.route_overview.notification.connection_endangered"; - private static final String keyNotificationConnectionMissedTitle = "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title"; - private static final String keyNotificationConnectionMissed = "gui.createrailwaysnavigator.route_overview.notification.connection_missed"; - private static final String keyNotificationJourneyCompletedTitle = "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title"; - private static final String keyNotificationJourneyCompleted = "gui.createrailwaysnavigator.route_overview.notification.journey_completed"; - - // Events - public static enum TransferState { - NONE, - DEFAULT, - CONNECTION_MISSED, - CONNECTION_CANCELLED - } - - public static record NotificationData(State state, Component title, Component text) {} - public static record JourneyBeginData(State state, Component infoText, String narratorText) {} - public static record JourneyInterruptData(State state, Component title, Component text, String narratorText) {} - public static record ReachNextStopData(State state, Component infoText, String narratorText, TransferState transferState) {} - public static record ContinueData(State state) {} - public static record FinishJourneyData(State state, Component infoText) {} - public static record AnnounceNextStopData(State state, Component infoText, String narratorText, boolean isTransfer) {} - - private Map> onUpdateRealtime = new HashMap<>(); - private Map>> onInfoTextChange = new HashMap<>(); - private Map>> onNotificationSend = new HashMap<>(); - private Map>> onStateChange = new HashMap<>(); - private Map>> onNarratorAnnounce = new HashMap<>(); - private Map>> onJourneyBegin = new HashMap<>(); - private Map>> onJourneyInterrupt = new HashMap<>(); - private Map>> onReachNextStop = new HashMap<>(); - private Map>> onContinue = new HashMap<>(); - private Map>> onFinishJourney = new HashMap<>(); - private Map>> onAnnounceNextStop = new HashMap<>(); - - private Component lastInfoText = TextUtils.empty(); - private NotificationData lastNotification = null; - private String lastNarratorText = ""; - - private boolean beginAnnounced = false; - - public JourneyListener(SimpleRoute route) { - this.route = route; - } - - public static JourneyListener listenTo(SimpleRoute route) { - return new JourneyListener(route); - } - - public JourneyListener start() { - Component text = currentStation().getInfo().platform() == null || currentStation().getInfo().platform().isBlank() ? - ELanguage.translate(keyJourneyBegins, - currentStation().getTrain().trainName(), - currentStation().getTrain().scheduleTitle(), - TimeUtils.parseTime((int)((currentStation().getEstimatedTimeWithThreshold() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()) - ) : - ELanguage.translate(keyJourneyBeginsWithPlatform, - currentStation().getTrain().trainName(), - currentStation().getTrain().scheduleTitle(), - TimeUtils.parseTime((int)((currentStation().getEstimatedTimeWithThreshold() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()), - currentStation().getInfo().platform() - ); - String narratorText = text.getString() + ". " + ELanguage.translate(keyOptionsText, TextUtils.keybind(keyKeybindOptions)).getString(); - - onJourneyBegin.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new JourneyBeginData(currentState, text, narratorText)); - } - }); - setInfoText(text); - setNarratorText(narratorText); - - isStarted = true; - requestRealtimeData(); - return this; - } - - public JourneyListener stop() { - isStarted = false; - return this; - } - - public JourneyListener registerOnUpdateRealtime(IJourneyListenerClient client, Runnable m) { - unregisterOnUpdateRealtime(client); - this.onUpdateRealtime.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnStateChange(IJourneyListenerClient client, Consumer m) { - unregisterOnUpdateRealtime(client); - this.onStateChange.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnNarratorAnnounce(IJourneyListenerClient client, Consumer m) { - unregisterOnNarratorAnnounce(client); - this.onNarratorAnnounce.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnInfoTextChange(IJourneyListenerClient client, Consumer m) { - unregisterOnInfoTextChange(client); - this.onInfoTextChange.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnNotification(IJourneyListenerClient client, Consumer m) { - unregisterOnNotification(client); - this.onNotificationSend.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnReachNextStop(IJourneyListenerClient client, Consumer m) { - unregisterOnReachNextStop(client); - this.onReachNextStop.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnContinueWithJourneyAfterStop(IJourneyListenerClient client, Consumer m) { - unregisterOnContinueWithJourneyAfterStop(client); - this.onContinue.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnFinishJourney(IJourneyListenerClient client, Consumer m) { - unregisterOnFinishJourney(client); - this.onFinishJourney.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnAnnounceNextStop(IJourneyListenerClient client, Consumer m) { - unregisterOnAnnounceNextStop(client); - this.onAnnounceNextStop.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnJourneyBegin(IJourneyListenerClient client, Consumer m) { - unregisterOnJourneyBegin(client); - this.onJourneyBegin.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - public JourneyListener registerOnJourneyInterrupt(IJourneyListenerClient client, Consumer m) { - unregisterOnJourneyInterrupt(client); - this.onJourneyInterrupt.put(client.getJourneyListenerClientId(), Optional.of(m)); - return this; - } - - - public void unregisterOnUpdateRealtime(IJourneyListenerClient client) { - if (onUpdateRealtime.containsKey(client.getJourneyListenerClientId())) { - onUpdateRealtime.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnStateChange(IJourneyListenerClient client) { - if (onStateChange.containsKey(client.getJourneyListenerClientId())) { - onStateChange.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnNarratorAnnounce(IJourneyListenerClient client) { - if (onNarratorAnnounce.containsKey(client.getJourneyListenerClientId())) { - onNarratorAnnounce.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnNotification(IJourneyListenerClient client) { - if (onNotificationSend.containsKey(client.getJourneyListenerClientId())) { - onNotificationSend.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnInfoTextChange(IJourneyListenerClient client) { - if (onInfoTextChange.containsKey(client.getJourneyListenerClientId())) { - onInfoTextChange.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnReachNextStop(IJourneyListenerClient client) { - if (onReachNextStop.containsKey(client.getJourneyListenerClientId())) { - onReachNextStop.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnContinueWithJourneyAfterStop(IJourneyListenerClient client) { - if (onContinue.containsKey(client.getJourneyListenerClientId())) { - onContinue.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnFinishJourney(IJourneyListenerClient client) { - if (onFinishJourney.containsKey(client.getJourneyListenerClientId())) { - onFinishJourney.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnAnnounceNextStop(IJourneyListenerClient client) { - if (onAnnounceNextStop.containsKey(client.getJourneyListenerClientId())) { - onAnnounceNextStop.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnJourneyBegin(IJourneyListenerClient client) { - if (onJourneyBegin.containsKey(client.getJourneyListenerClientId())) { - onJourneyBegin.remove(client.getJourneyListenerClientId()); - } - } - - public void unregisterOnJourneyInterrupt(IJourneyListenerClient client) { - if (onJourneyInterrupt.containsKey(client.getJourneyListenerClientId())) { - onJourneyInterrupt.remove(client.getJourneyListenerClientId()); - } - } - - public void unregister(IJourneyListenerClient client) { - unregisterOnAnnounceNextStop(client); - unregisterOnContinueWithJourneyAfterStop(client); - unregisterOnFinishJourney(client); - unregisterOnInfoTextChange(client); - unregisterOnJourneyBegin(client); - unregisterOnNarratorAnnounce(client); - unregisterOnReachNextStop(client); - unregisterOnStateChange(client); - unregisterOnUpdateRealtime(client); - unregisterOnNotification(client); - unregisterOnJourneyInterrupt(client); - } - - - - - public void tick() { - if (!isStarted) { - return; - } - - if (currentState != State.AFTER_JOURNEY && currentState != State.JOURNEY_INTERRUPTED) { - realTimeRefreshTimer++; - if (realTimeRefreshTimer > REALTIME_REFRESH_TIME) { - realTimeRefreshTimer = 0; - requestRealtimeData(); - } - } - - if (!beginAnnounced && firstStation().getEstimatedTime() - ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get() < Minecraft.getInstance().level.getDayTime()) { - Component title = ELanguage.translate(keyNotificationJourneyBeginsTitle, - lastStation().getStationName() - ); - Component description = currentStation().getInfo().platform() == null || currentStation().getInfo().platform().isBlank() ? - ELanguage.translate(keyNotificationJourneyBegins, - currentStation().getTrain().trainName(), - currentStation().getTrain().scheduleTitle(), - TimeUtils.parseTime((int)((currentStation().getEstimatedTimeWithThreshold() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()) - ) - : - ELanguage.translate(keyNotificationJourneyBeginsWithPlatform, - currentStation().getTrain().trainName(), - currentStation().getTrain().scheduleTitle(), - TimeUtils.parseTime((int)((currentStation().getEstimatedTimeWithThreshold() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()), - currentStation().getInfo().platform() - ); - - setNotificationText(new NotificationData(currentState, title, description)); - setNarratorText(title.getString() + " " + description.getString()); - beginAnnounced = true; - } - } - - private void requestRealtimeData() { - final Collection ids = Arrays.stream(route.getStationArray()).map(x -> x.getTrain().trainId()).distinct().toList(); - - long id = InstanceManager.registerClientRealtimeResponseAction((predictions, time) -> { - Map> predMap = predictions.stream().collect(Collectors.groupingBy(SimpleDeparturePrediction::trainId)); - - if (predMap.containsKey(currentStation().getTrain().trainId())) { - SimpleDeparturePrediction currentTrainNextStop = predMap.get(currentStation().getTrain().trainId()).get(0); - List currentTrainSchedule = predMap.get(currentStation().getTrain().trainId()); - - if (currentState != State.BEFORE_JOURNEY && currentState != State.JOURNEY_INTERRUPTED) { - if (currentState != State.WHILE_TRAVELING && currentState != State.WHILE_TRANSFER) { - while (!currentTrainNextStop.stationTagName().equals(currentStation().getStationName()) && currentState != State.AFTER_JOURNEY) { - if (currentStation().getTag() != StationTag.END) { - nextStop(); - } - } - } - } - - if (((!currentState.isWaitingForNextTrainToDepart() || currentState == State.BEFORE_JOURNEY || currentState == State.WHILE_TRANSFER) && currentStation().shouldRenderRealtime()) - && isStationValidForShedule(currentTrainSchedule, currentStation().getTrain().trainId(), stationIndex) && time >= currentStation().getEstimatedTime()) { - if (currentStation().getTag() == StationTag.PART_END) { - if (route.getStationArray()[stationIndex + 1].isTrainCanceled()) { - journeyInterrupt(route.getStationArray()[stationIndex + 1]); - } else if (route.getStationArray()[stationIndex + 1].isDeparted()) { - reachTransferStopConnectionMissed(); - } else { - reachTransferStop(); - } - } else if (currentStation().getTag() == StationTag.END) { - finishJourney(); - } else { - reachNextStop(); - } - } - - if (currentState == State.AFTER_JOURNEY) { - return; - } - } else { - journeyInterrupt(currentStation()); - return; - } - - Map> mappedRoute = Arrays.stream(route.getStationArray()).skip(stationIndex).collect(Collectors.groupingBy(x -> x.getTrain().trainId(), LinkedHashMap::new, Collectors.toList())); - - // Update realtime data - for (int i = stationIndex; i < route.getStationCount(true); i++) { - StationEntry e = route.getStationArray()[i]; - if (!predMap.containsKey(e.getTrain().trainId()) || e.isTrainCanceled()) { - e.setTrainCanceled(true, "", e.getTrain().trainName()); - continue; - } - - List preds = predMap.get(e.getTrain().trainId()); - List stations = mappedRoute.get(e.getTrain().trainId()); - updateRealtime(preds, stations, e.getTrain().trainId(), stationIndex, time); - } - - boolean departed = false; - // check if connection train has departed - for (List routePart : mappedRoute.values()) { - if (mappedRoute.size() < 2) { - continue; - } - - if (routePart.get(0).isDeparted()) { - continue; - } - - if (departed) { - routePart.forEach(x -> x.setDeparted(true)); - continue; - } - - long min = routePart.stream().filter(x -> x.getCurrentTime() + ModClientConfig.TRANSFER_TIME.get() > x.getScheduleTime()).mapToLong(x -> x.getCurrentTime()).min().orElse(-1); - long currentTime = routePart.get(0).getCurrentTime(); - - if (min > 0 && currentTime > min && currentTime + ModClientConfig.TRANSFER_TIME.get() > routePart.get(0).getScheduleTime()) { - routePart.forEach(x -> x.setDeparted(true)); - departed = true; - - Component title = ELanguage.translate(keyNotificationConnectionMissedTitle); - Component description = ELanguage.translate(keyNotificationConnectionMissed, - routePart.get(0).getTrain().trainName(), - routePart.get(0).getTrain().scheduleTitle() - ); - setNotificationText(new NotificationData(currentState, title, description)); - setNarratorText(title.getString() + " " + description.getString()); - } - } - - checkStationAccessibility(); - - // PROGRESS ANIMATION - if (currentState != State.BEFORE_JOURNEY && currentState != State.JOURNEY_INTERRUPTED) { - if (!currentState.nextStopAnnounced() && !currentState.isWaitingForNextTrainToDepart() // state check - && time >= route.getStationArray()[stationIndex].getEstimatedTime() - ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) // train check - { - announceNextStop(); - } - } - - onUpdateRealtime.values().forEach(x -> { - if (x.isPresent()) { - x.get().run(); - } - }); - }); - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new RealtimeRequestPacket(id, ids)); - } - - private boolean isStationValidForShedule(List schedule, UUID trainId, int startIndex) { - List filteredStationEntryList = new ArrayList<>(); - for (int i = startIndex; i < route.getStationCount(true); i++) { - StationEntry entry = route.getStationArray()[i]; - if (!entry.getTrain().trainId().equals(trainId)) { - break; - } - filteredStationEntryList.add(entry.getStationName()); - } - String[] filteredStationEntries = filteredStationEntryList.toArray(String[]::new); - String[] sched = schedule.stream().map(x -> x.stationTagName()).toArray(String[]::new); - - int k = 0; - for (int i = 0; i < filteredStationEntries.length; i++) { - if (!filteredStationEntries[i].equals(sched[k])) { - return false; - } - - k++; - if (k > sched.length) { - k = 0; - } - } - return true; - } - - private void updateRealtime(List schedule, List route, UUID trainId, int startIndex, long updateTime) { - boolean b = false; - long lastTime = -1; - - List routePart = route.stream().filter(x -> x.getTrain().trainId().equals(trainId)).toList(); - StationEntry first = routePart.get(0); - if (first.getTag() != StationTag.PART_START && first.getTag() != StationTag.START) { - first = null; - } - StationEntry last = routePart.get(routePart.size() - 1); - boolean wasDelayed = last.isDelayed(); - StationInfo oldInfo = first == null ? null : first.getUpdatedInfo(); - - for (int i = 0, k = 0; i < schedule.size() && k < route.size(); i++) { - SimpleDeparturePrediction current = schedule.get(i); - long newTime = current.departureTicks() + updateTime; - if (route.get(0).getStationName().equals(current.stationTagName())) { - k = 0; - b = true; - } - - if (route.get(k).getStationName().equals(current.stationTagName()) && b == true) { - if (newTime > lastTime/* && newTime + EARLY_ARRIVAL_THRESHOLD > route.get(k).station().getScheduleTime()*/) { - route.get(k).updateRealtimeData(current.departureTicks(), updateTime, current.stationInfo(), () -> { - - }); - lastTime = route.get(k).getCurrentTime(); - } - k++; - } else { - b = false; - } - } - - if (oldInfo != null && !first.getUpdatedInfo().equals(oldInfo)) { - setNotificationText(new NotificationData(currentState, ELanguage.translate(keyNotificationPlatformChangedTitle), ELanguage.translate(keyNotificationPlatformChanged, - first.getStationName(), - first.getUpdatedInfo().platform() - ))); - } - - if (!wasDelayed && last.isDelayed()) { - setNotificationText(new NotificationData(currentState, ELanguage.translate(keyNotificationTrainDelayedTitle, - last.getTrain().trainName(), - TimeUtils.parseDuration(last.getDifferenceTime()) - ), ELanguage.translate(keyNotificationTrainDelayed, - TimeUtils.parseTime((int)(last.getEstimatedTimeWithThreshold() % DragonLib.TICKS_PER_DAY) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), - TimeUtils.parseTime((int)(last.getScheduleTime() % DragonLib.TICKS_PER_DAY) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), - last.getStationName() - ))); - } - } - - - - private void checkStationAccessibility() { - boolean willMiss = false; - for (int i = stationIndex; i < route.getStationCount(true); i++) { - StationEntry station = route.getStationArray()[i]; - StationEntry nextStation = i < route.getStationCount(true) - 1 ? route.getStationArray()[i + 1] : null; - - boolean wasWillMiss = station.willMissStop(); - - if (nextStation == null) { - continue; - } - - if (station.isDeparted()) { - willMiss = true; - } - - if (!willMiss) { - long transferTime = -1; - if (nextStation != null && !nextStation.isDeparted()) { - if (nextStation.getCurrentTime() + ModClientConfig.TRANSFER_TIME.get() < nextStation.getScheduleTime()) { - transferTime = nextStation.getScheduleTime() - station.getScheduleTime(); - } else { - transferTime = nextStation.getCurrentTime() - station.getCurrentTime(); - } - } - - if (transferTime < 0) { - willMiss = true; - } - } - - station.setWillMiss(willMiss); - - if (station.getTag() == StationTag.PART_START && !wasWillMiss && station.willMissStop()) { - setNotificationText(new NotificationData(currentState, ELanguage.translate(keyNotificationConnectionEndangeredTitle), ELanguage.translate(keyNotificationConnectionEndangered, - station.getTrain().trainName(), - station.getTrain().scheduleTitle() - ))); - } - } - } - - public Component getLastInfoText() { - return lastInfoText; - } - - public String lastNarratorText() { - return lastNarratorText; - } - - public NotificationData getLastNotification() { - return lastNotification; - } - - private void setState(State state) { - this.currentState = state; - onStateChange.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(currentState); - } - }); - } - - private void setInfoText(Component text) { - this.lastInfoText = text; - onInfoTextChange.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(text); - } - }); - } - - private void setNotificationText(NotificationData data) { - this.lastNotification = data; - onNotificationSend.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(data); - } - }); - } - - private void setNarratorText(String text) { - this.lastNarratorText = text; - onNarratorAnnounce.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(text); - } - }); - } - - - private void nextStop() { - if (!changeCurrentStation()) { - return; - } - - setState(State.WHILE_TRAVELING); - - onContinue.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new ContinueData(currentState)); - } - }); - } - - private boolean changeCurrentStation() { - if (stationIndex + 1 >= route.getStationCount(true)) { - finishJourney(); - return false; - } - stationIndex++; - return true; - } - - private void finishJourney() { - Component text = ELanguage.translate(keyAfterJourney, route.getStationArray()[route.getStationCount(true) - 1].getStationName()); - setState(State.AFTER_JOURNEY); - - onFinishJourney.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new FinishJourneyData(currentState, text)); - } - }); - setInfoText(text); - setNotificationText(new NotificationData(currentState, ELanguage.translate(keyNotificationJourneyCompletedTitle), ELanguage.translate(keyNotificationJourneyCompleted))); - - stop(); - } - - private void announceNextStop() { - Component textA = TextUtils.empty(); - Component textB = TextUtils.empty(); - Component text; - text = textA = ELanguage.translate(keyNextStop, - currentStation().getStationName() - ); - if (currentStation().getTag() == StationTag.PART_END && currentStation().getIndex() + 1 < route.getStationCount(true)) { - Component transferText = textB = nextStation().get().getInfo().platform() == null || nextStation().get().getInfo().platform().isBlank() ? - ELanguage.translate(keyTransfer, - nextStation().get().getTrain().trainName(), - nextStation().get().getTrain().scheduleTitle() - ) : - ELanguage.translate(keyTransferWithPlatform, - nextStation().get().getTrain().trainName(), - nextStation().get().getTrain().scheduleTitle(), - nextStation().get().getInfo().platform() - ); - text = TextUtils.concatWithStarChars(text, transferText); - setState(State.BEFORE_TRANSFER); - setNotificationText(new NotificationData(currentState, ELanguage.translate(keyNotificationTransferTitle), - nextStation().get().getInfo().platform() == null || nextStation().get().getInfo().platform().isBlank() ? - ELanguage.translate(keyNotificationTransfer, - nextStation().get().getTrain().trainName(), - nextStation().get().getTrain().scheduleTitle() - ) : - ELanguage.translate(keyNotificationTransferWithPlatform, - nextStation().get().getTrain().trainName(), - nextStation().get().getTrain().scheduleTitle(), - nextStation().get().getInfo().platform() - ) - )); - } else { - setState(State.BEFORE_NEXT_STOP); - } - - String narratorText = textA.getString() + ". " + textB.getString(); - boolean transfer = currentStation().getTag() == StationTag.PART_END && currentStation().getIndex() + 1 < route.getStationCount(true); - - final Component fText = text; - onAnnounceNextStop.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new AnnounceNextStopData(currentState, fText, narratorText, transfer)); - } - }); - setInfoText(text); - setNarratorText(narratorText); - } - - private void reachNextStop() { - Component text = TextUtils.text(currentStation().getStationName()); - String narratorText = text.getString(); - - setState(State.WHILE_NEXT_STOP); - onReachNextStop.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new ReachNextStopData(currentState, text, narratorText, TransferState.NONE)); - } - }); - - setInfoText(text); - setNarratorText(narratorText); - } - - private void reachTransferStop() { - Component text = nextStation().isPresent() ? ( - nextStation().get().getInfo().platform() == null || nextStation().get().getInfo().platform().isBlank() ? - ELanguage.translate(keyTransfer, - nextStation().get().getTrain().trainName(), - nextStation().get().getTrain().scheduleTitle() - ) : - ELanguage.translate(keyTransferWithPlatform, - nextStation().get().getTrain().trainName(), - nextStation().get().getTrain().scheduleTitle(), - nextStation().get().getInfo().platform() - ) - ) : TextUtils.empty(); - String narratorText = text.getString(); - - setState(State.WHILE_TRANSFER); - changeCurrentStation(); - - onReachNextStop.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new ReachNextStopData(currentState, text, narratorText, TransferState.DEFAULT)); - } - }); - setInfoText(text); - setNarratorText(narratorText); - } - - private void reachTransferStopConnectionMissed() { - Component text = TextUtils.concatWithStarChars( - TextUtils.text(currentStation().getStationName()), - ELanguage.translate(keyConnectionMissedInfo) - ); - String narratorText = ""; - - setState(State.JOURNEY_INTERRUPTED); - onReachNextStop.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new ReachNextStopData(currentState, text, narratorText, TransferState.CONNECTION_MISSED)); - } - }); - setInfoText(text); - setNarratorText(narratorText); - - stop(); - } - - private void journeyInterrupt(StationEntry station) { - Component text = ELanguage.translate(keyJourneyInterruptedTitle); - Component desc = ELanguage.translate(keyJourneyInterrupted, station.getTrain().trainName()); - String narratorText = ""; - - setState(State.JOURNEY_INTERRUPTED); - onJourneyInterrupt.values().forEach(x -> { - if (x.isPresent()) { - x.get().accept(new JourneyInterruptData(currentState, text, desc, narratorText)); - } - }); - setInfoText(text); - setNarratorText(narratorText); - - stop(); - } - - public long getTransferTime(int index) { - Optional station = getEntryAt(index); - Optional nextStation = getEntryAt(index + 1); - long transferTime = -1; - if (station.isPresent() && nextStation.isPresent() && !nextStation.get().isDeparted()) { - if (nextStation.get().getCurrentTime() + ModClientConfig.TRANSFER_TIME.get() < nextStation.get().getScheduleTime()) { - transferTime = nextStation.get().getScheduleTime() - station.get().getScheduleTime(); - } else { - transferTime = nextStation.get().getCurrentTime() - station.get().getCurrentTime(); - } - } - - return transferTime; - } - - - - - public StationEntry currentStation() { - return route.getStationArray()[stationIndex]; - } - - public Optional nextStation() { - return stationIndex + 1 < route.getStationCount(true) ? Optional.of(route.getStationArray()[stationIndex + 1]) : Optional.empty(); - } - - public Optional previousSation() { - return stationIndex > 0 ? Optional.of(route.getStationArray()[stationIndex - 1]) : Optional.empty(); - } - - public StationEntry firstStation() { - return route.getStationArray()[0]; - } - - public StationEntry lastStation() { - return route.getStationArray()[route.getStationCount(true) - 1]; - } - - public State getCurrentState() { - return currentState; - } - - public boolean isStarted() { - return isStarted; - } - - public long getTimeDifferenceForStation(int index) { - StationEntry station = route.getStationArray()[index]; - return station.getCurrentRefreshTime() + station.getCurrentTicks() - station.getRefreshTime() - station.getTicks(); - } - - public int getIndex() { - return stationIndex; - } - - public SimpleRoute getListeningRoute() { - return route; - } - - public Optional getEntryAt(int index) { - return index >= 0 && index < route.getStationCount(true) ? Optional.of(route.getStationArray()[index]) : Optional.empty(); - } - - - - public static enum State { - BEFORE_JOURNEY, - WHILE_TRAVELING, - BEFORE_NEXT_STOP, - WHILE_NEXT_STOP, - BEFORE_TRANSFER, - WHILE_TRANSFER, - AFTER_JOURNEY, - JOURNEY_INTERRUPTED; - - public boolean nextStopAnnounced() { - return this == BEFORE_NEXT_STOP || this == BEFORE_TRANSFER; - } - - public boolean isWhileTraveling() { - return this == WHILE_TRAVELING; - } - - public boolean isTranferring() { - return this == WHILE_TRANSFER; - } - - public boolean isAtNextStop() { - return this == WHILE_NEXT_STOP; - } - - public boolean isWaitingForNextTrainToDepart() { - return isTranferring() || isAtNextStop(); - } - - public boolean important() { - return this == State.WHILE_TRANSFER || this == State.BEFORE_TRANSFER || this == State.AFTER_JOURNEY || this == State.BEFORE_JOURNEY ||this == JOURNEY_INTERRUPTED; - } - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListenerManager.java b/common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListenerManager.java deleted file mode 100644 index 033f7198..00000000 --- a/common/src/main/java/de/mrjulsen/crn/event/listeners/JourneyListenerManager.java +++ /dev/null @@ -1,125 +0,0 @@ -package de.mrjulsen.crn.event.listeners; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.event.listeners.JourneyListener.State; -import net.minecraft.client.Minecraft; - -public class JourneyListenerManager { - - private static final int CLEANUP_INTERVALL = 100; - private static JourneyListenerManager instance; - - private int cleanupTimer = 0; - - private final Map journeyListenerCache = new HashMap<>(); - private final Map> dataListeners = new HashMap<>(); - - public UUID register(JourneyListener listener) { - UUID uuid = UUID.randomUUID(); - while (journeyListenerCache.containsKey(uuid)) { - uuid = UUID.randomUUID(); - } - - journeyListenerCache.put(uuid, listener); - return uuid; - } - - public UUID create(SimpleRoute route, IJourneyListenerClient initialListener) { - UUID id = register(new JourneyListener(route)); - dataListeners.put(id, new HashSet<>(Set.of(initialListener.getJourneyListenerClientId()))); - return id; - } - - public JourneyListener get(UUID id, IJourneyListenerClient addListener) { - if (addListener != null) { - if (dataListeners.containsKey(id)) { - dataListeners.get(id).add(addListener.getJourneyListenerClientId()); - } else { - dataListeners.put(id, new HashSet<>(Set.of(addListener.getJourneyListenerClientId()))); - } - } - return journeyListenerCache.get(id); - } - - public void removeClientListener(UUID listenerId, IJourneyListenerClient client) { - if (dataListeners.containsKey(listenerId)) { - dataListeners.get(listenerId).removeIf(x -> x.equals(client.getJourneyListenerClientId())); - } - } - - public void removeClientListenerForAll(IJourneyListenerClient client) { - dataListeners.values().forEach(x -> x.removeIf(a -> a.equals(client.getJourneyListenerClientId()))); - } - - - public static boolean hasInstance() { - return instance != null; - } - - @SuppressWarnings("resource") - public static void tick() { - if (!hasInstance() || Minecraft.getInstance().level == null) { - return; - } - - instance.tickInstance(); - } - - private void tickInstance() { - journeyListenerCache.values().forEach(x -> x.tick()); - - cleanupTimer++; - cleanupTimer = cleanupTimer % CLEANUP_INTERVALL; - if (cleanupTimer == 0) { - dataListeners.entrySet().removeIf(x -> x.getValue().isEmpty()); - journeyListenerCache.entrySet().removeIf(e -> !dataListeners.containsKey(e.getKey()) || e.getValue().getCurrentState() == State.AFTER_JOURNEY); - } - } - - public int getCacheSize() { - return journeyListenerCache.size(); - } - - public boolean exists(UUID listenerId) { - return journeyListenerCache.containsKey(listenerId); - } - - public Collection getAllListeners() { - return journeyListenerCache.values(); - } - - - public static JourneyListenerManager getInstance() { - return instance; - } - - public static JourneyListenerManager start() { - stop(); - instance = new JourneyListenerManager(); - CreateRailwaysNavigator.LOGGER.info("Journey Listener started."); - return instance; - } - - public static void stop() { - if (hasInstance()) { - instance.stopInstance(); - } - - instance = null; - } - - private void stopInstance() { - dataListeners.clear(); - journeyListenerCache.clear(); - CreateRailwaysNavigator.LOGGER.info("Journey Listener stopped."); - } - -} diff --git a/common/src/main/java/de/mrjulsen/crn/event/listeners/TrainListener.java b/common/src/main/java/de/mrjulsen/crn/event/listeners/TrainListener.java deleted file mode 100644 index 7549001b..00000000 --- a/common/src/main/java/de/mrjulsen/crn/event/listeners/TrainListener.java +++ /dev/null @@ -1,136 +0,0 @@ -package de.mrjulsen.crn.event.listeners; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.OptionalInt; -import java.util.UUID; - -import com.simibubi.create.content.trains.entity.Train; -import com.simibubi.create.content.trains.schedule.condition.ScheduleWaitCondition; -import com.simibubi.create.content.trains.schedule.condition.TimedWaitCondition; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.config.ModCommonConfig; -import de.mrjulsen.crn.mixin.ScheduleDataAccessor; -import de.mrjulsen.crn.util.TrainUtils; -import de.mrjulsen.mcdragonlib.util.ScheduledTask; -import de.mrjulsen.mcdragonlib.util.ScheduledTask.ScheduledTaskContext; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.level.Level; - -public class TrainListener { - - private static TrainListener instance; - - private boolean isRunning = true; - private int totalTrainCount; - private int listeingTrainCount; - private final Map> TRAIN_DURATIONS = new HashMap<>(); - private final Map lastTicks = new HashMap<>(); - - public int getDepartmentTime(Level level, Train train) { - List> conditions = train.runtime.getSchedule().entries.get(train.runtime.currentEntry).conditions; - if (conditions.isEmpty() || ((ScheduleDataAccessor)train.runtime).crn$conditionProgress().isEmpty() || ((ScheduleDataAccessor)train.runtime).crn$conditionContext().isEmpty()) - return 0; - - List list = conditions.get(0); - int progress = ((ScheduleDataAccessor)train.runtime).crn$conditionProgress().get(0); - if (progress >= list.size()) - return 0; - - CompoundTag tag = ((ScheduleDataAccessor)train.runtime).crn$conditionContext().get(0); - ScheduleWaitCondition condition = list.get(progress); - return ((TimedWaitCondition)condition).totalWaitTicks() - tag.getInt("Time"); - } - - private boolean performTask(TrainListener instance, ScheduledTaskContext context) { - - if (!isRunning) { - return false; - } - - new Thread(() -> { - Collection trains = TrainUtils.getAllTrains(); - listeingTrainCount = 0; - trains.forEach(train -> { - if (!TrainUtils.isTrainValid(train)) { - return; - } - - OptionalInt maxTrainDuration = TrainUtils.getTrainDeparturePredictions(train.id, null).stream().mapToInt(x -> x.getTicks()).max(); - - if (maxTrainDuration.isPresent()) { - if (!lastTicks.containsKey(train.id)) { - lastTicks.put(train.id, 0); - } - - if (lastTicks.get(train.id) < maxTrainDuration.getAsInt()) { - - } - if (!TRAIN_DURATIONS.containsKey(train.id)) { - TRAIN_DURATIONS.put(train.id, new ArrayList<>()); - } - - TRAIN_DURATIONS.get(train.id).add(maxTrainDuration.getAsInt()); - if (TRAIN_DURATIONS.get(train.id).size() > 30) { - TRAIN_DURATIONS.get(train.id).remove(0); - } - lastTicks.replace(train.id, maxTrainDuration.getAsInt()); - } - listeingTrainCount++; - }); - - this.totalTrainCount = trains.size(); - }, "Train Listener Worker").run(); - - return isRunning; - } - - public static TrainListener getInstance() { - return instance; - } - - public static TrainListener start(Level level) { - if (instance == null) - instance = new TrainListener(); - - ScheduledTask.create(instance, level, ModCommonConfig.TRAIN_WATCHER_INTERVALL.get(), Integer.MAX_VALUE, instance::performTask); - - CreateRailwaysNavigator.LOGGER.info("TrainListener started."); - return instance; - } - - public static void stop() { - if (instance == null) - return; - - instance.stopInstance(); - } - - private void stopInstance() { - isRunning = false; - CreateRailwaysNavigator.LOGGER.info("TrainListener stopped."); - } - - - - public int getApproximatedTrainDuration(Train train) { - int a = getApproximatedTrainDuration(train.id); - return a == 0 ? 1 : a; - } - - public int getApproximatedTrainDuration(UUID trainId) { - return TRAIN_DURATIONS.containsKey(trainId) ? TRAIN_DURATIONS.get(trainId).stream().mapToInt(v -> v).sum() / TRAIN_DURATIONS.get(trainId).size() : 0; - } - - public int getTotalTrainCount() { - return this.totalTrainCount; - } - - public int getListeningTrainCount() { - return this.listeingTrainCount; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/exceptions/RuntimeSideException.java b/common/src/main/java/de/mrjulsen/crn/exceptions/RuntimeSideException.java new file mode 100644 index 00000000..34b98466 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/exceptions/RuntimeSideException.java @@ -0,0 +1,7 @@ +package de.mrjulsen.crn.exceptions; + +public class RuntimeSideException extends RuntimeException { + public RuntimeSideException(boolean expectClient) { + super("This method can only be called on the " + (expectClient ? "client" : "server") + " side!"); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java b/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java index 06aa8030..01c7c2f9 100644 --- a/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java +++ b/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java @@ -17,7 +17,8 @@ public NavigatorItem(Properties props) { @Override public InteractionResultHolder use(Level pLevel, Player pPlayer, InteractionHand pUsedHand) { if (pLevel.isClientSide) { - ClientWrapper.showNavigatorGui(pLevel); + ClientWrapper.showNavigatorGui(); + return InteractionResultHolder.success(pPlayer.getItemInHand(pUsedHand)); } return super.use(pLevel, pPlayer, pUsedHand); } diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ControlsHandlerMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ControlsHandlerMixin.java new file mode 100644 index 00000000..13ce5823 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ControlsHandlerMixin.java @@ -0,0 +1,43 @@ +package de.mrjulsen.crn.mixin; + +import java.lang.ref.WeakReference; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.simibubi.create.content.contraptions.AbstractContraptionEntity; +import com.simibubi.create.content.contraptions.actors.trainControls.ControlsHandler; +import com.simibubi.create.content.trains.entity.CarriageContraption; +import com.simibubi.create.content.trains.entity.CarriageContraptionEntity; + +import de.mrjulsen.crn.data.train.TrainListener; +import net.minecraft.core.BlockPos; + +@Mixin(ControlsHandler.class) +public class ControlsHandlerMixin { + + @Accessor("entityRef") + private static WeakReference crn$entityRef() { + throw new AssertionError(); + } + + @Inject(method = "startControlling", remap = false, at = @At(value = "HEAD")) + private static void onStartControlling(AbstractContraptionEntity entity, BlockPos controllerLocalPos, CallbackInfo ci) { + if (entity.getContraption() instanceof CarriageContraption carriage && carriage.entity instanceof CarriageContraptionEntity trainEntity) { + if (TrainListener.data.containsKey(trainEntity.trainId)) { + TrainListener.data.get(trainEntity.trainId).isManualControlled = true; + } + } + } + @Inject(method = "stopControlling", remap = false, at = @At(value = "HEAD")) + private static void onStopControlling(CallbackInfo ci) { + if (crn$entityRef().get() != null && crn$entityRef().get().getContraption() instanceof CarriageContraption carriage && carriage.entity instanceof CarriageContraptionEntity trainEntity) { + if (TrainListener.data.containsKey(trainEntity.trainId)) { + TrainListener.data.get(trainEntity.trainId).isManualControlled = false; + } + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/GlobalTrainDisplayDataMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/GlobalTrainDisplayDataMixin.java new file mode 100644 index 00000000..d65a502e --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/GlobalTrainDisplayDataMixin.java @@ -0,0 +1,34 @@ +package de.mrjulsen.crn.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData; + +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPost; +import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPre; + +@Mixin(GlobalTrainDisplayData.class) +public class GlobalTrainDisplayDataMixin { + + /* + * Called every ~5 seconds, when create refreshes its display data. + */ + + @Inject(method = "refresh", remap = false, at = @At(value = "HEAD")) + private static void onRefreshPre(CallbackInfo ci) { + if (CRNEventsManager.isRegistered(GlobalTrainDisplayDataRefreshEventPre.class)) { + CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPre.class).run(); + } + } + + @Inject(method = "refresh", remap = false, at = @At(value = "TAIL")) + private static void onRefreshPost(CallbackInfo ci) { + if (CRNEventsManager.isRegistered(GlobalTrainDisplayDataRefreshEventPost.class)) { + CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPost.class).run(); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ModularGuiLineBuilderAccessor.java b/common/src/main/java/de/mrjulsen/crn/mixin/ModularGuiLineBuilderAccessor.java new file mode 100644 index 00000000..f2528a48 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ModularGuiLineBuilderAccessor.java @@ -0,0 +1,21 @@ +package de.mrjulsen.crn.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import com.simibubi.create.foundation.gui.ModularGuiLine; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; + +import net.minecraft.client.gui.Font; + +@Mixin(ModularGuiLineBuilder.class) +public interface ModularGuiLineBuilderAccessor { + @Accessor("target") + ModularGuiLine crn$getTarget(); + @Accessor("font") + Font crn$getFont(); + @Accessor("x") + int crn$getX(); + @Accessor("y") + int crn$getY(); +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/MountedStorageManagerMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/MountedStorageManagerMixin.java index 126dc6fa..5f741be5 100644 --- a/common/src/main/java/de/mrjulsen/crn/mixin/MountedStorageManagerMixin.java +++ b/common/src/main/java/de/mrjulsen/crn/mixin/MountedStorageManagerMixin.java @@ -12,14 +12,14 @@ import com.simibubi.create.content.contraptions.MountedStorageManager; import com.simibubi.create.content.trains.entity.CarriageContraption; -import de.mrjulsen.crn.block.be.IContraptionBlockEntity; +import de.mrjulsen.crn.block.blockentity.IContraptionBlockEntity; import net.minecraft.world.level.block.entity.BlockEntity; @Mixin(MountedStorageManager.class) public class MountedStorageManagerMixin { @Inject(method = "entityTick", remap = false, at = @At(value = "HEAD")) - public void tick$inject(AbstractContraptionEntity entity, CallbackInfo ci) { + public void onEntityTick(AbstractContraptionEntity entity, CallbackInfo ci) { if (entity.getContraption() instanceof CarriageContraption carriage) { Set beList = new LinkedHashSet<>(); beList.addAll(entity.getContraption().maybeInstancedBlockEntities); diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java new file mode 100644 index 00000000..ea5e6652 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java @@ -0,0 +1,36 @@ +package de.mrjulsen.crn.mixin; + +import java.util.ArrayList; +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.crn.web.WebsitePreparableReloadListener; +import net.minecraft.commands.Commands; +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.ReloadableServerResources; +import net.minecraft.server.packs.resources.PreparableReloadListener; + +@Mixin(ReloadableServerResources.class) +public class ReloadableServerResourcesMixin { + + public WebsitePreparableReloadListener websitemanager; + + @Inject(method = "", at = @At(value = "TAIL")) + public void onInit(RegistryAccess.Frozen frozen, Commands.CommandSelection commandSelection, int i, CallbackInfo ci) { + this.websitemanager = new WebsitePreparableReloadListener(); + ModUtils.setWebsiteResourceManager(websitemanager); + } + + @Inject(method = "listeners", at = @At("RETURN"), cancellable = true) + private void addListener(CallbackInfoReturnable> cir) { + List listeners = new ArrayList<>(cir.getReturnValue()); + listeners.add(websitemanager); + cir.setReturnValue(listeners); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleDataAccessor.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeAccessor.java similarity index 50% rename from common/src/main/java/de/mrjulsen/crn/mixin/ScheduleDataAccessor.java rename to common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeAccessor.java index 551f963e..dc741255 100644 --- a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleDataAccessor.java +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeAccessor.java @@ -4,15 +4,30 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; +import com.simibubi.create.content.trains.entity.Train; import com.simibubi.create.content.trains.schedule.ScheduleRuntime; import net.minecraft.nbt.CompoundTag; @Mixin(ScheduleRuntime.class) -public interface ScheduleDataAccessor { +public interface ScheduleRuntimeAccessor { + @Accessor("train") + Train crn$getTrain(); + + @Accessor("ticksInTransit") + int crn$getTicksInTransit(); + + @Invoker("estimateStayDuration") + int crn$runEstimateStayDuration(int index); + @Accessor("conditionProgress") List crn$conditionProgress(); + @Accessor("conditionContext") List crn$conditionContext(); + + @Accessor("predictionTicks") + List crn$predictionTicks(); } diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeMixin.java new file mode 100644 index 00000000..cb8a0a8a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleRuntimeMixin.java @@ -0,0 +1,153 @@ +package de.mrjulsen.crn.mixin; + +import java.util.Collection; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.graph.DiscoveredPath; +import com.simibubi.create.content.trains.schedule.Schedule; +import com.simibubi.create.content.trains.schedule.ScheduleEntry; +import com.simibubi.create.content.trains.schedule.ScheduleRuntime; +import com.simibubi.create.content.trains.schedule.condition.ScheduleWaitCondition; +import com.simibubi.create.content.trains.schedule.condition.ScheduledDelay; +import com.simibubi.create.content.trains.schedule.destination.DestinationInstruction; +import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction; +import com.simibubi.create.content.trains.station.GlobalStation; + +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.events.CreateTrainPredictionEvent; +import de.mrjulsen.crn.event.events.ScheduleResetEvent; +import de.mrjulsen.crn.event.events.SubmitTrainPredictionsEvent; +import de.mrjulsen.crn.event.events.TrainDestinationChangedEvent; +import dev.architectury.injectables.annotations.PlatformOnly; +import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; +import de.mrjulsen.crn.data.schedule.instruction.ICustomSuggestionsInstruction; +import de.mrjulsen.crn.data.schedule.instruction.IPredictableInstruction; +import de.mrjulsen.crn.data.schedule.instruction.IStationPredictableInstruction; + +@Mixin(ScheduleRuntime.class) +public class ScheduleRuntimeMixin { + + public final Map, IStationPredictableInstruction> customData = new LinkedHashMap<>(); + + public ScheduleRuntime self() { + return (ScheduleRuntime)(Object)this; + } + + public ScheduleRuntimeAccessor accessor() { + return (ScheduleRuntimeAccessor)(Object)this; + } + + + @Inject(method = "submitPredictions", remap = false, at = @At(value = "RETURN"), locals = LocalCapture.CAPTURE_FAILHARD) + public void onSubmitPredictions(CallbackInfoReturnable> cir, Collection predictions, int entryCount, int accumulatedTime, int current) { + if (CRNEventsManager.isRegistered(SubmitTrainPredictionsEvent.class)) { + CRNEventsManager.getEvent(SubmitTrainPredictionsEvent.class).run(accessor().crn$getTrain(), predictions, entryCount, accumulatedTime, current); + } + } + + @Inject(method = "", remap = false, at = @At(value = "TAIL")) + public void onResetWhileInit(CallbackInfo ci) { + if (CRNEventsManager.isRegistered(ScheduleResetEvent.class)) { + CRNEventsManager.getEvent(ScheduleResetEvent.class).run(accessor().crn$getTrain(), true); + } + } + + @Inject(method = {"setSchedule", "discardSchedule"}, remap = false, at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/trains/schedule/ScheduleRuntime;reset()V")) + public void onReset(CallbackInfo ci) { + if (CRNEventsManager.isRegistered(ScheduleResetEvent.class)) { + CRNEventsManager.getEvent(ScheduleResetEvent.class).run(accessor().crn$getTrain(), false); + } + } + + @PlatformOnly(value = "FORGE") + @Inject(method = "startCurrentInstruction", remap = false, at = @At(value = "RETURN"), locals = LocalCapture.CAPTURE_FAILHARD) + public void onStartCurrentInstructionRetForge(CallbackInfoReturnable cir, ScheduleEntry entry, ScheduleInstruction instruction) { + if (CRNEventsManager.isRegistered(TrainDestinationChangedEvent.class) && cir.getReturnValue() != null && instruction instanceof DestinationInstruction) { + CRNEventsManager.getEvent(TrainDestinationChangedEvent.class).run(accessor().crn$getTrain(), accessor().crn$getTrain().getCurrentStation(), cir.getReturnValue(), self().currentEntry); + } + } + + @PlatformOnly(value = "FABRIC") + @Inject(method = "startCurrentInstruction", remap = false, at = @At(value = "RETURN"), locals = LocalCapture.CAPTURE_FAILHARD) + public void onStartCurrentInstructionRetFabric(CallbackInfoReturnable cir, ScheduleEntry entry, ScheduleInstruction instruction) { + if (CRNEventsManager.isRegistered(TrainDestinationChangedEvent.class) && cir.getReturnValue() != null && instruction instanceof DestinationInstruction) { + CRNEventsManager.getEvent(TrainDestinationChangedEvent.class).run(accessor().crn$getTrain(), accessor().crn$getTrain().getCurrentStation(), cir.getReturnValue().destination, self().currentEntry); + } + } + + @Inject(method = "startCurrentInstruction", remap = false, at = @At(value = "TAIL"), cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD) + public void onStartCurrentInstructionPost(CallbackInfoReturnable cir, ScheduleEntry entry, ScheduleInstruction instruction) { + if (instruction instanceof ICustomSuggestionsInstruction custom) { + if (TrainListener.data.containsKey(accessor().crn$getTrain().id)) { + custom.run(self(), TrainListener.data.get(accessor().crn$getTrain().id), accessor().crn$getTrain(), self().currentEntry); + } + + if (instruction instanceof IStationPredictableInstruction predictable) { + customData.put(predictable.getClass(), predictable::predictForStation); + } + + self().state = ScheduleRuntime.State.PRE_TRANSIT; + self().currentEntry++; + } + cir.setReturnValue((GlobalStation)null); + } + + @Inject(method = "predictForEntry", remap = false, at = @At(value = "HEAD")) + public void onPredictForEntryPre(int index, String currentTitle, int accumulatedTime, Collection predictions, CallbackInfoReturnable cir) { + ScheduleInstruction instruction = self().getSchedule().entries.get(index).instruction; + if (instruction instanceof IStationPredictableInstruction predictable) { + customData.put(predictable.getClass(), predictable::predictForStation); + } + if (instruction instanceof IPredictableInstruction predictable) { + predictable.predict(TrainListener.data.get(accessor().crn$getTrain().id), accessor().crn$getTrain().runtime, index, accessor().crn$getTrain()); + } + } + + @Inject(method = "createPrediction", remap = false, at = @At(value = "RETURN")) + public void onCreatePrediction(int index, String destination, String currentTitle, int time, CallbackInfoReturnable cir) { + if (CRNEventsManager.isRegistered(CreateTrainPredictionEvent.class) && cir.getReturnValue() != null) { + int stayDuration = accessor().crn$runEstimateStayDuration(index); + int minStayDuration = estimateMinStayDuration(accessor().crn$getTrain(), index); + CRNEventsManager.getEvent(CreateTrainPredictionEvent.class).run(accessor().crn$getTrain(), self(), new LinkedHashMap<>(customData), index, stayDuration, minStayDuration, cir.getReturnValue()); + } + } + + + + private static int estimateMinStayDuration(Train train, int index) { + Schedule schedule = train.runtime.getSchedule(); + if (index >= schedule.entries.size()) { + if (!schedule.cyclic) + return -1; + index = 0; + } + + ScheduleEntry scheduleEntry = schedule.entries.get(index); + Columns: for (List list : scheduleEntry.conditions) { + int total = 0; + for (ScheduleWaitCondition condition : list) { + if (condition instanceof DynamicDelayCondition wait) + total += wait.minWaitTicks(); + else if (condition instanceof ScheduledDelay wait) + total += wait.totalWaitTicks(); + else + continue Columns; + } + return total; + } + + return -1; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenAccessor.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenAccessor.java new file mode 100644 index 00000000..66607a42 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenAccessor.java @@ -0,0 +1,37 @@ +package de.mrjulsen.crn.mixin; + +import java.util.List; +import java.util.function.Consumer; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import com.simibubi.create.content.trains.schedule.DestinationSuggestions; +import com.simibubi.create.content.trains.schedule.IScheduleInput; +import com.simibubi.create.content.trains.schedule.ScheduleScreen; +import com.simibubi.create.foundation.gui.ModularGuiLine; +import com.simibubi.create.foundation.utility.IntAttached; + +@Mixin(ScheduleScreen.class) +public interface ScheduleScreenAccessor { + + @Accessor("editorSubWidgets") + ModularGuiLine crn$getEditorSubWidgets(); + + @Accessor("destinationSuggestions") + DestinationSuggestions crn$getDestinationSuggestions(); + + @Accessor("destinationSuggestions") + void crn$setDestinationSuggestions(DestinationSuggestions suggestions); + + @Accessor("onEditorClose") + Consumer crn$getOnEditorClose(); + + + @Invoker("onDestinationEdited") + void crn$onDestinationEdited(String text); + + @Invoker("getViableStations") + List> crn$getViableStations(IScheduleInput field); +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java new file mode 100644 index 00000000..3acb5b6a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java @@ -0,0 +1,71 @@ +package de.mrjulsen.crn.mixin; + +import java.util.ArrayList; +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import com.simibubi.create.content.trains.schedule.DestinationSuggestions; +import com.simibubi.create.content.trains.schedule.IScheduleInput; +import com.simibubi.create.content.trains.schedule.ScheduleScreen; +import com.simibubi.create.foundation.utility.IntAttached; + +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.schedule.instruction.ICustomSuggestionsInstruction; +import de.mrjulsen.crn.data.schedule.instruction.IStationTagInstruction; +import de.mrjulsen.crn.data.schedule.instruction.ITrainNameInstruction; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; + +@Mixin(ScheduleScreen.class) +public class ScheduleScreenMixin { + + public ScheduleScreenAccessor accessor() { + return (ScheduleScreenAccessor)(Object)this; + } + + public ScheduleScreen self() { + return (ScheduleScreen)(Object)this; + } + + public int getTopPos() { + return ((AbstractContainerScreen)(Object)this).topPos; + } + + @SuppressWarnings("resource") + @Inject(method = "updateEditorSubwidgets", remap = false, at = @At(value = "INVOKE", shift = Shift.AFTER, target = "Lcom/simibubi/create/foundation/gui/ModularGuiLine;loadValues(Lnet/minecraft/nbt/CompoundTag;Ljava/util/function/Consumer;Ljava/util/function/Consumer;)V"), cancellable = true) + public void onUpdateEditorSubwidgets(IScheduleInput field, CallbackInfo ci) { + if (field instanceof ICustomSuggestionsInstruction) { + accessor().crn$getEditorSubWidgets().forEach(e -> { + if (!(e instanceof EditBox destinationBox)) + return; + accessor().crn$setDestinationSuggestions(new DestinationSuggestions(Minecraft.getInstance(), self(), destinationBox, Minecraft.getInstance().font, onGetViableStations(field), getTopPos() + 33)); + accessor().crn$getDestinationSuggestions().setAllowSuggestions(true); + accessor().crn$getDestinationSuggestions().updateCommandInfo(); + destinationBox.setResponder(accessor()::crn$onDestinationEdited); + }); + } + + } + + + public List> onGetViableStations(IScheduleInput field) { + if (field instanceof IStationTagInstruction) { + List stations = GlobalSettings.getInstance().getAllStationTags(); + List> result = new ArrayList<>(); + for (int i = 0; i < stations.size(); i++) { + result.add(IntAttached.with(i, stations.get(i).getTagName().get())); + } + return result; + } else if (field instanceof ITrainNameInstruction) { + return null; /* TODO */ + //return ClientTrainStationSnapshot.getInstance().getAllTrainStations().stream().map(station -> IntAttached.with(0, station)).toList(); + } + return null; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java new file mode 100644 index 00000000..d7852c88 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java @@ -0,0 +1,35 @@ +package de.mrjulsen.crn.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.station.GlobalStation; + +import de.mrjulsen.crn.event.CRNEventsManager; +import de.mrjulsen.crn.event.events.TrainArrivalAndDepartureEvent; + +@Mixin(Train.class) +public class TrainMixin { + + public Train self() { + return (Train)(Object)this; + } + + @Inject(method = "arriveAt", remap = false, at = @At(value = "TAIL")) + public void onArriveAt(GlobalStation station, CallbackInfo ci) { + if (CRNEventsManager.isRegistered(TrainArrivalAndDepartureEvent.class)) { + CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).run(self(), station, true); + } + } + + @Inject(method = "leaveStation", remap = false, at = @At(value = "TAIL"), locals = LocalCapture.CAPTURE_FAILHARD) + public void onLeaveStation(CallbackInfo ci, GlobalStation currentStation) { + if (CRNEventsManager.isRegistered(TrainArrivalAndDepartureEvent.class)) { + CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).run(self(), currentStation, false); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/TrainStatusAccessor.java b/common/src/main/java/de/mrjulsen/crn/mixin/TrainStatusAccessor.java new file mode 100644 index 00000000..0360d011 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/TrainStatusAccessor.java @@ -0,0 +1,16 @@ +package de.mrjulsen.crn.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import com.simibubi.create.content.trains.entity.TrainStatus; + +@Mixin(TrainStatus.class) +public interface TrainStatusAccessor { + @Accessor("navigation") + boolean crn$navigation(); + @Accessor("track") + boolean crn$track(); + @Accessor("conductor") + boolean crn$conductor(); +} diff --git a/common/src/main/java/de/mrjulsen/crn/network/InstanceManager.java b/common/src/main/java/de/mrjulsen/crn/network/InstanceManager.java index 31c17995..a3ed9952 100644 --- a/common/src/main/java/de/mrjulsen/crn/network/InstanceManager.java +++ b/common/src/main/java/de/mrjulsen/crn/network/InstanceManager.java @@ -1,142 +1,10 @@ package de.mrjulsen.crn.network; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import de.mrjulsen.crn.data.NearestTrackStationResult; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.SimpleTrainConnection; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.network.packets.cts.TrainDataRequestPacket.TrainData; -import de.mrjulsen.crn.network.packets.stc.NavigationResponsePacket.NavigationResponseData; import de.mrjulsen.mcdragonlib.client.OverlayManager; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - public class InstanceManager { - - private static final Map CLIENT_RESPONSE_RECEIVED_ACTION = new HashMap<>(); - private static final Map, NavigationResponseData>> CLIENT_NAVIGATION_RESPONSE_ACTION = new HashMap<>(); - private static final Map> CLIENT_NEAREST_STATION_RESPONSE_ACTION = new HashMap<>(); - private static final Map, Long>> CLIENT_REALTIME_RESPONSE_ACTION = new HashMap<>(); - private static final Map, Long>> CLIENT_NEXT_CONNECTIONS_RESPONSE_ACTION = new HashMap<>(); - private static final Map> CLIENT_NEXT_TRAIN_DATA_RESPONSE_ACTION = new HashMap<>(); - private static long currentRouteOverlayId; - public static String getInstancesCountString() { - return String.format("[%s, %s, %s, %s, %s, %s]", - CLIENT_RESPONSE_RECEIVED_ACTION.size(), - CLIENT_NAVIGATION_RESPONSE_ACTION.size(), - CLIENT_NEAREST_STATION_RESPONSE_ACTION.size(), - CLIENT_REALTIME_RESPONSE_ACTION.size(), - CLIENT_NEXT_CONNECTIONS_RESPONSE_ACTION.size(), - CLIENT_NEXT_TRAIN_DATA_RESPONSE_ACTION.size() - ); - } - - public static void clearAll() { - CLIENT_RESPONSE_RECEIVED_ACTION.clear(); - CLIENT_NAVIGATION_RESPONSE_ACTION.clear(); - CLIENT_NEAREST_STATION_RESPONSE_ACTION.clear(); - CLIENT_REALTIME_RESPONSE_ACTION.clear(); - CLIENT_NEXT_CONNECTIONS_RESPONSE_ACTION.clear(); - CLIENT_NEXT_TRAIN_DATA_RESPONSE_ACTION.clear(); - } - - public static long registerClientResponseReceievedAction(Runnable runnable) { - long id = System.nanoTime(); - CLIENT_RESPONSE_RECEIVED_ACTION.put(id, runnable); - return id; - } - - public static void runClientResponseReceievedAction(long id) { - if (CLIENT_RESPONSE_RECEIVED_ACTION.containsKey(id)) { - Runnable run = CLIENT_RESPONSE_RECEIVED_ACTION.remove(id); - if (run != null) - run.run(); - } - } - - public static long registerClientNavigationResponseAction(BiConsumer, NavigationResponseData> consumer) { - long id = System.nanoTime(); - CLIENT_NAVIGATION_RESPONSE_ACTION.put(id, consumer); - return id; - } - - public static void runClientNavigationResponseAction(long id, List routes, NavigationResponseData data) { - if (CLIENT_NAVIGATION_RESPONSE_ACTION.containsKey(id)) { - BiConsumer, NavigationResponseData> action = CLIENT_NAVIGATION_RESPONSE_ACTION.remove(id); - if (action != null) - action.accept(routes, data); - } - } - - - - public static long registerClientNearestStationResponseAction(Consumer consumer) { - long id = System.nanoTime(); - CLIENT_NEAREST_STATION_RESPONSE_ACTION.put(id, consumer); - return id; - } - - public static void runClientNearestStationResponseAction(long id, NearestTrackStationResult result) { - if (CLIENT_NEAREST_STATION_RESPONSE_ACTION.containsKey(id)) { - Consumer action = CLIENT_NEAREST_STATION_RESPONSE_ACTION.remove(id); - if (action != null) - action.accept(result); - } - } - - - public static long registerClientRealtimeResponseAction(BiConsumer, Long> consumer) { - long id = System.nanoTime(); - CLIENT_REALTIME_RESPONSE_ACTION.put(id, consumer); - return id; - } - - public static void runClientRealtimeResponseAction(long id, Collection result, long time) { - if (CLIENT_REALTIME_RESPONSE_ACTION.containsKey(id)) { - BiConsumer, Long> action = CLIENT_REALTIME_RESPONSE_ACTION.remove(id); - if (action != null) - action.accept(result, time); - } - } - - - public static long registerClientNextConnectionsResponseAction(BiConsumer, Long> consumer) { - long id = System.nanoTime(); - CLIENT_NEXT_CONNECTIONS_RESPONSE_ACTION.put(id, consumer); - return id; - } - - public static void runClientNextConnectionsResponseAction(long id, Collection result, long time) { - if (CLIENT_NEXT_CONNECTIONS_RESPONSE_ACTION.containsKey(id)) { - BiConsumer, Long> action = CLIENT_NEXT_CONNECTIONS_RESPONSE_ACTION.remove(id); - if (action != null) - action.accept(result, time); - } - } - - - - public static long registerClientTrainDataResponseAction(BiConsumer consumer) { - long id = System.nanoTime(); - CLIENT_NEXT_TRAIN_DATA_RESPONSE_ACTION.put(id, consumer); - return id; - } - - public static void runClientTrainDataResponseAction(long id, TrainData data, long time) { - if (CLIENT_NEXT_TRAIN_DATA_RESPONSE_ACTION.containsKey(id)) { - BiConsumer action = CLIENT_NEXT_TRAIN_DATA_RESPONSE_ACTION.remove(id); - if (action != null) - action.accept(data, time); - } - } - public static void setRouteOverlay(long id) { removeRouteOverlay(); currentRouteOverlayId = id; diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java index ea031060..8cbc8a46 100644 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java +++ b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java @@ -3,10 +3,9 @@ import java.util.function.Supplier; import de.mrjulsen.crn.block.AbstractAdvancedSidedDisplayBlock; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; -import de.mrjulsen.crn.data.EDisplayInfo; -import de.mrjulsen.crn.data.EDisplayType; -import de.mrjulsen.crn.data.ESide; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.properties.ESide; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; import de.mrjulsen.mcdragonlib.net.IPacketBase; import dev.architectury.networking.NetworkManager.PacketContext; import net.minecraft.core.BlockPos; @@ -17,51 +16,45 @@ public class AdvancedDisplayUpdatePacket implements IPacketBase { private BlockPos pos; - private EDisplayType type; - private EDisplayInfo info; + private DisplayTypeResourceKey key; private boolean doubleSided; public AdvancedDisplayUpdatePacket() {} - public AdvancedDisplayUpdatePacket(Level level, BlockPos pos, EDisplayType type, EDisplayInfo info, boolean doubleSided) { + public AdvancedDisplayUpdatePacket(Level level, BlockPos pos, DisplayTypeResourceKey key, boolean doubleSided) { this.pos = pos; - this.info = info; - this.type = type; + this.key = key; this.doubleSided = doubleSided; apply(level, this); } - protected AdvancedDisplayUpdatePacket(BlockPos pos, EDisplayType type, EDisplayInfo info, boolean doubleSided) { + protected AdvancedDisplayUpdatePacket(BlockPos pos, DisplayTypeResourceKey key, boolean doubleSided) { this.pos = pos; - this.info = info; - this.type = type; + this.key = key; this.doubleSided = doubleSided; } @Override public void encode(AdvancedDisplayUpdatePacket packet, FriendlyByteBuf buffer) { buffer.writeBlockPos(packet.pos); - buffer.writeInt(packet.info.getId()); - buffer.writeInt(packet.type.getId()); + buffer.writeNbt(packet.key.toNbt()); buffer.writeBoolean(packet.doubleSided); } @Override public AdvancedDisplayUpdatePacket decode(FriendlyByteBuf buffer) { BlockPos pos = buffer.readBlockPos(); - EDisplayInfo info = EDisplayInfo.getTypeById(buffer.readInt()); - EDisplayType type = EDisplayType.getTypeById(buffer.readInt()); + DisplayTypeResourceKey key = DisplayTypeResourceKey.fromNbt(buffer.readNbt()); boolean doubleSided = buffer.readBoolean(); - return new AdvancedDisplayUpdatePacket(pos, type, info, doubleSided); + return new AdvancedDisplayUpdatePacket(pos, key, doubleSided); } private void apply(Level level, AdvancedDisplayUpdatePacket packet) { if (level.isLoaded(packet.pos)) { if (level.getBlockEntity(packet.pos) instanceof AdvancedDisplayBlockEntity blockEntity) { blockEntity.applyToAll(be -> { - be.setDisplayType(packet.type); - be.setInfoType(packet.info); + be.setDisplayTypeKey(packet.key); if (level.getBlockState(be.getBlockPos()).getBlock() instanceof AbstractAdvancedSidedDisplayBlock) { BlockState state = level.getBlockState(be.getBlockPos()); state = state.setValue(AbstractAdvancedSidedDisplayBlock.SIDE, packet.doubleSided ? ESide.BOTH : ESide.FRONT); diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsRequestPacket.java deleted file mode 100644 index d884d59d..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsRequestPacket.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.network.packets.stc.GlobalSettingsResponsePacket; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; - -public class GlobalSettingsRequestPacket implements IPacketBase { - - public long id; - - public GlobalSettingsRequestPacket() { } - - public GlobalSettingsRequestPacket(long id) { - this.id = id; - } - - @Override - public void encode(GlobalSettingsRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - } - - @Override - public GlobalSettingsRequestPacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - return new GlobalSettingsRequestPacket(id); - } - - @Override - public void handle(GlobalSettingsRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new GlobalSettingsResponsePacket(packet.id, GlobalSettingsManager.getInstance().getSettingsData())); - }); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsUpdatePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsUpdatePacket.java deleted file mode 100644 index c6875803..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/GlobalSettingsUpdatePacket.java +++ /dev/null @@ -1,152 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.AliasName; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.TrainGroup; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.stc.GlobalSettingsResponsePacket; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; - -public class GlobalSettingsUpdatePacket implements IPacketBase { - private static final String NBT_STRING = "Val"; - private static final String NBT_COMPOUND_TAG = "Tag"; - - public long id; - public CompoundTag data; - public EGlobalSettingsAction action; - - public GlobalSettingsUpdatePacket() { } - - public GlobalSettingsUpdatePacket(long id, CompoundTag nbt, EGlobalSettingsAction action) { - this.id = id; - this.data = nbt; - this.action = action; - } - - public static void send(Object data, EGlobalSettingsAction action, Runnable then) { - CompoundTag nbt = new CompoundTag(); - switch (action) { - case ADD_TO_BLACKLIST: - case REMOVE_FROM_BLACKLIST: - case ADD_TRAIN_TO_BLACKLIST: - case REMOVE_TRAIN_FROM_BLACKLIST: - case UNREGISTER_ALIAS_STRING: - case UNREGISTER_TRAIN_GROUP_TRAIN: - nbt.putString(NBT_STRING, (String)data); - break; - case UNREGISTER_ALIAS: - case REGISTER_ALIAS: - nbt = ((TrainStationAlias)data).toNbt(); - break; - case UNREGISTER_TRAIN_GROUP: - case REGISTER_TRAIN_GROUP: - nbt = ((TrainGroup)data).toNbt(); - break; - case UPDATE_ALIAS: - Object[] dataArr = (Object[])data; - nbt.putString(NBT_STRING, (String)dataArr[0]); - nbt.put(NBT_COMPOUND_TAG, ((TrainStationAlias)dataArr[1]).toNbt()); - break; - case UPDATE_TRAIN_GROUP: - Object[] dataArr1 = (Object[])data; - nbt.putString(NBT_STRING, (String)dataArr1[0]); - nbt.put(NBT_COMPOUND_TAG, ((TrainGroup)dataArr1[1]).toNbt()); - break; - default: - return; - } - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new GlobalSettingsUpdatePacket(InstanceManager.registerClientResponseReceievedAction(then), nbt, action)); - } - - @Override - public void encode(GlobalSettingsUpdatePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeNbt(packet.data); - buffer.writeEnum(packet.action); - } - - @Override - public GlobalSettingsUpdatePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - CompoundTag data = buffer.readNbt(); - EGlobalSettingsAction action = buffer.readEnum(EGlobalSettingsAction.class); - GlobalSettingsUpdatePacket instance = new GlobalSettingsUpdatePacket(id, data, action); - return instance; - } - - @Override - public void handle(GlobalSettingsUpdatePacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - if (GlobalSettingsManager.getInstance().getSettingsData() == null) { - CreateRailwaysNavigator.LOGGER.error("Failed to handle GlobalSettingsUpdatePacket! The settings instance of the global settings manager is null."); - return; - } - - switch (packet.action) { - case ADD_TO_BLACKLIST: - GlobalSettingsManager.getInstance().getSettingsData().addToBlacklistServer(packet.data.getString(NBT_STRING)); - break; - case REMOVE_FROM_BLACKLIST: - GlobalSettingsManager.getInstance().getSettingsData().removeFromBlacklistServer(packet.data.getString(NBT_STRING)); - break; - case ADD_TRAIN_TO_BLACKLIST: - GlobalSettingsManager.getInstance().getSettingsData().addTrainToBlacklistServer(packet.data.getString(NBT_STRING)); - break; - case REMOVE_TRAIN_FROM_BLACKLIST: - GlobalSettingsManager.getInstance().getSettingsData().removeTrainFromBlacklistServer(packet.data.getString(NBT_STRING)); - break; - case UNREGISTER_ALIAS_STRING: - GlobalSettingsManager.getInstance().getSettingsData().unregisterAliasServer(packet.data.getString(NBT_STRING)); - break; - case UNREGISTER_ALIAS: - GlobalSettingsManager.getInstance().getSettingsData().unregisterAliasServer(TrainStationAlias.fromNbt(packet.data)); - break; - case REGISTER_ALIAS: - GlobalSettingsManager.getInstance().getSettingsData().registerAliasServer(TrainStationAlias.fromNbt(packet.data)); - break; - case UPDATE_ALIAS: - GlobalSettingsManager.getInstance().getSettingsData().updateAliasServer(AliasName.of(packet.data.getString(NBT_STRING)), TrainStationAlias.fromNbt(packet.data.getCompound(NBT_COMPOUND_TAG))); - break; - case UNREGISTER_TRAIN_GROUP_TRAIN: - GlobalSettingsManager.getInstance().getSettingsData().unregisterTrainGroupServer(packet.data.getString(NBT_STRING)); - break; - case UNREGISTER_TRAIN_GROUP: - GlobalSettingsManager.getInstance().getSettingsData().unregisterTrainGroupServer(TrainGroup.fromNbt(packet.data).getGroupName()); - break; - case REGISTER_TRAIN_GROUP: - GlobalSettingsManager.getInstance().getSettingsData().registerTrainGroupServer(TrainGroup.fromNbt(packet.data)); - break; - case UPDATE_TRAIN_GROUP: - GlobalSettingsManager.getInstance().getSettingsData().updateTrainGroupServer(packet.data.getString(NBT_STRING), TrainGroup.fromNbt(packet.data.getCompound(NBT_COMPOUND_TAG))); - break; - default: - return; - } - GlobalSettingsManager.getInstance().setDirty(); - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new GlobalSettingsResponsePacket(packet.id, GlobalSettingsManager.getInstance().getSettingsData())); - }); - } - - public static enum EGlobalSettingsAction { - REGISTER_ALIAS, - UNREGISTER_ALIAS_STRING, - UNREGISTER_ALIAS, - UPDATE_ALIAS, - REGISTER_TRAIN_GROUP, - UNREGISTER_TRAIN_GROUP_TRAIN, - UNREGISTER_TRAIN_GROUP, - UPDATE_TRAIN_GROUP, - ADD_TO_BLACKLIST, - REMOVE_FROM_BLACKLIST, - ADD_TRAIN_TO_BLACKLIST, - REMOVE_TRAIN_FROM_BLACKLIST; - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NavigationRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NavigationRequestPacket.java deleted file mode 100644 index 5e3b7d4a..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NavigationRequestPacket.java +++ /dev/null @@ -1,92 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.List; -import java.util.ArrayList; -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.core.navigation.Graph; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.Route; -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.crn.data.UserSettings; -import de.mrjulsen.crn.network.packets.stc.NavigationResponsePacket; -import de.mrjulsen.crn.network.packets.stc.ServerErrorPacket; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; - -public class NavigationRequestPacket implements IPacketBase { - - public long id; - public String start; - public String end; - public UserSettings filterSettings; - - public NavigationRequestPacket() { } - - public NavigationRequestPacket(long id, String start, String end) { - this(id, start, end, new UserSettings()); - } - - private NavigationRequestPacket(long id, String start, String end, UserSettings settings) { - this.id = id; - this.start = start; - this.end = end; - this.filterSettings = settings; - } - - @Override - public void encode(NavigationRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeUtf(packet.start); - buffer.writeUtf(packet.end); - buffer.writeNbt(packet.filterSettings.toNbt()); - } - - @Override - public NavigationRequestPacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - String start = buffer.readUtf(); - String end = buffer.readUtf(); - UserSettings filterSettings = UserSettings.fromNbt(buffer.readNbt()); - return new NavigationRequestPacket(id, start, end, filterSettings); - } - - @Override - public void handle(NavigationRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - Thread navigationThread = new Thread(() -> { - List routes = new ArrayList<>(); - final long updateTime = contextSupplier.get().getPlayer().level.getDayTime(); - final long startTime = System.currentTimeMillis(); - - try { - TrainStationAlias startAlias = GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(packet.start); - TrainStationAlias endAlias = GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(packet.end); - - if (startAlias == null || endAlias == null) { - return; - } - - Graph graph = new Graph(contextSupplier.get().getPlayer().getLevel(), packet.filterSettings); - routes.addAll(graph.navigate(startAlias, endAlias, true)); - } catch (Exception e) { - CreateRailwaysNavigator.LOGGER.error("Navigation error: ", e); - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new ServerErrorPacket(e.getMessage())); - } finally { - final long estimatedTime = System.currentTimeMillis() - startTime; - CreateRailwaysNavigator.LOGGER.info(String.format("Route calculated. Took %sms.", - estimatedTime - )); - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new NavigationResponsePacket(packet.id, new ArrayList<>(routes.stream().filter(x -> !x.isEmpty()).map(x -> new SimpleRoute(x)).toList()), estimatedTime, updateTime)); - } - }); - navigationThread.setPriority(Thread.MIN_PRIORITY); - navigationThread.setName("Navigator"); - navigationThread.start(); - }); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NearestStationRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NearestStationRequestPacket.java deleted file mode 100644 index 56c9b980..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NearestStationRequestPacket.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.NearestTrackStationResult; -import de.mrjulsen.crn.network.packets.stc.NearestStationResponsePacket; -import de.mrjulsen.crn.network.packets.stc.ServerErrorPacket; -import de.mrjulsen.crn.util.TrainUtils; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.core.BlockPos; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.phys.Vec3; - -public class NearestStationRequestPacket implements IPacketBase { - - public long id; - public BlockPos pos; - - public NearestStationRequestPacket() { } - - public NearestStationRequestPacket(long id, Vec3 pos) { - this(id, new BlockPos(pos)); - } - - public NearestStationRequestPacket(long id, BlockPos pos) { - this.id = id; - this.pos = pos; - } - - @Override - public void encode(NearestStationRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeBlockPos(packet.pos); - } - - @Override - public NearestStationRequestPacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - BlockPos pos = buffer.readBlockPos(); - return new NearestStationRequestPacket(id, pos); - } - - @Override - public void handle(NearestStationRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - Thread navigationThread = new Thread(() -> { - NearestTrackStationResult result = NearestTrackStationResult.empty(); - try { - result = TrainUtils.getNearestTrackStation(contextSupplier.get().getPlayer().getLevel(), packet.pos); - } catch (Exception e) { - CreateRailwaysNavigator.LOGGER.error("Error while trying to find nearest track station ", e); - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new ServerErrorPacket(e.getMessage())); - } finally { - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new NearestStationResponsePacket(packet.id, result)); - } - }); - navigationThread.setPriority(Thread.MIN_PRIORITY); - navigationThread.setName("Station Location Calculator"); - navigationThread.start(); - }); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NextConnectionsRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NextConnectionsRequestPacket.java deleted file mode 100644 index 9199d9ea..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/NextConnectionsRequestPacket.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.UUID; -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.network.packets.stc.NextConnectionsResponsePacket; -import de.mrjulsen.crn.util.TrainUtils; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; - -public class NextConnectionsRequestPacket implements IPacketBase { - - public long requestId; - public UUID trainId; - public long ticksToNextStop; - public String currentStationName; - - public NextConnectionsRequestPacket() { } - - public NextConnectionsRequestPacket(long requestId, UUID trainId, String currentStationName, long ticksToNextStop) { - this.requestId = requestId; - this.trainId = trainId; - this.ticksToNextStop = ticksToNextStop; - this.currentStationName = currentStationName; - } - - @Override - public void encode(NextConnectionsRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.requestId); - buffer.writeUUID(packet.trainId); - buffer.writeLong(packet.ticksToNextStop); - buffer.writeUtf(packet.currentStationName); - } - - @Override - public NextConnectionsRequestPacket decode(FriendlyByteBuf buffer) { - long requestId = buffer.readLong(); - UUID trainId = buffer.readUUID(); - long ticksToNextStop = buffer.readLong(); - String currentStationName = buffer.readUtf(); - - return new NextConnectionsRequestPacket(requestId, trainId, currentStationName, ticksToNextStop); - } - - @Override - public void handle(NextConnectionsRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - new Thread(() -> { - final long updateTime = contextSupplier.get().getPlayer().getLevel().getDayTime(); - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new NextConnectionsResponsePacket(packet.requestId, TrainUtils.getConnectionsAt(packet.currentStationName, packet.trainId, (int)packet.ticksToNextStop), updateTime)); - }, "Connections Loader").run(); - }); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/RealtimeRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/RealtimeRequestPacket.java deleted file mode 100644 index f624853a..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/RealtimeRequestPacket.java +++ /dev/null @@ -1,72 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.UUID; -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.network.packets.stc.RealtimeResponsePacket; -import de.mrjulsen.crn.util.TrainUtils; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.Level; - -public class RealtimeRequestPacket implements IPacketBase { - - public long requestId; - public Collection ids; - - public RealtimeRequestPacket() { } - - public RealtimeRequestPacket(long requestId, Collection ids) { - this.requestId = requestId; - this.ids = ids; - } - - @Override - public void encode(RealtimeRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.requestId); - buffer.writeInt(packet.ids.size()); - for (UUID u : packet.ids) { - buffer.writeUUID(u); - } - } - - @Override - public RealtimeRequestPacket decode(FriendlyByteBuf buffer) { - long requestId = buffer.readLong(); - int count = buffer.readInt(); - Collection uuids = new ArrayList<>(); - for (int i = 0; i < count; i++) { - uuids.add(buffer.readUUID()); - } - return new RealtimeRequestPacket(requestId, uuids); - } - - @Override - public void handle(RealtimeRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - final Level level = contextSupplier.get().getPlayer().getLevel(); - new Thread(() -> { - final long updateTime = level.getDayTime(); - Collection predictions = new ArrayList<>(); - packet.ids.forEach(x -> { - if (!TrainUtils.isTrainIdValid(x)) { - return; - } - - predictions.addAll(TrainUtils.getTrainDeparturePredictions(x, contextSupplier.get().getPlayer().getLevel()).stream().map(a -> a.simplify()).filter(a -> !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(a.stationName())).sorted(Comparator.comparingInt(a -> a.departureTicks())).toList()); - }); - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), (new RealtimeResponsePacket(packet.requestId, predictions, updateTime))); - }, "Realtime Provider").run(); - }); - } - - public static record StationData(Collection stationName, Collection indices, UUID trainId) {} -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrackStationsRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrackStationsRequestPacket.java deleted file mode 100644 index e8fc3e84..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrackStationsRequestPacket.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.event.listeners.TrainListener; -import de.mrjulsen.crn.network.packets.stc.TrackStationResponsePacket; -import de.mrjulsen.crn.util.TrainUtils; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; - -public class TrackStationsRequestPacket implements IPacketBase { - - public long id; - - public TrackStationsRequestPacket() { } - - public TrackStationsRequestPacket(long id) { - this.id = id; - } - - @Override - public void encode(TrackStationsRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - } - - @Override - public TrackStationsRequestPacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - return new TrackStationsRequestPacket(id); - } - - @Override - public void handle(TrackStationsRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new TrackStationResponsePacket( - packet.id, - TrainUtils.getAllStations().stream().map(x -> x.name).toList(), - TrainUtils.getAllTrains().stream().map(x -> x.name.getString()).toList(), - TrainListener.getInstance().getListeningTrainCount(), - TrainListener.getInstance().getTotalTrainCount() - )); - }); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrainDataRequestPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrainDataRequestPacket.java deleted file mode 100644 index 4c04489c..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/TrainDataRequestPacket.java +++ /dev/null @@ -1,196 +0,0 @@ -package de.mrjulsen.crn.network.packets.cts; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Supplier; - -import com.simibubi.create.content.trains.entity.Train; - -import de.mrjulsen.crn.Constants; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.lang.ELanguage; -import de.mrjulsen.crn.data.DeparturePrediction.TrainExitSide; -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.SimpleTrainSchedule; -import de.mrjulsen.crn.data.TrainStop; -import de.mrjulsen.crn.network.packets.stc.TrainDataResponsePacket; -import de.mrjulsen.crn.util.TrainUtils; -import de.mrjulsen.mcdragonlib.data.Cache; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.Level; - -public class TrainDataRequestPacket implements IPacketBase { - - public long requestId; - public UUID trainId; - public boolean getPredictions; - - public TrainDataRequestPacket() { } - - public TrainDataRequestPacket(long requestId, UUID trainId, boolean getPredictions) { - this.requestId = requestId; - this.trainId = trainId; - this.getPredictions = getPredictions; - } - - @Override - public void encode(TrainDataRequestPacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.requestId); - buffer.writeUUID(packet.trainId); - buffer.writeBoolean(packet.getPredictions); - } - - @Override - public TrainDataRequestPacket decode(FriendlyByteBuf buffer) { - long requestId = buffer.readLong(); - UUID trainId = buffer.readUUID(); - boolean getPredictions = buffer.readBoolean(); - - return new TrainDataRequestPacket(requestId, trainId, getPredictions); - } - - @Override - public void handle(TrainDataRequestPacket packet, Supplier contextSupplier) { - contextSupplier.get().queue(() -> { - final Level level = contextSupplier.get().getPlayer().getLevel(); - final long updateTime = level.getDayTime(); - Train train = TrainUtils.getTrain(packet.trainId); - List departurePredictions = new ArrayList<>(); - if (packet.getPredictions && train != null) { - Collection stops = new ArrayList<>(TrainUtils.getTrainStopsSorted(packet.trainId, level).stream().filter(x -> !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(x.getPrediction().getStationName())).toList()); - if (stops != null) { - departurePredictions.addAll(SimpleTrainSchedule.of(stops).makeScheduleUntilNextRepeat().getAllStops().stream().map(x -> x.getPrediction().simplify()).toList()); - } - } - - CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)contextSupplier.get().getPlayer(), new TrainDataResponsePacket(packet.requestId, new TrainData( - packet.trainId, - train.name.getString(), - departurePredictions, - train.speed, - train.navigation.ticksWaitingForSignal, - train.currentlyBackwards, - true - ), updateTime)); - }); - } - - public static class TrainData { - private static final String NBT_TRAIN_ID = "Id"; - private static final String NBT_SPEED = "Speed"; - private static final String NBT_WAITING_FOR_SIGNAL = "WaitingForSignal"; - private static final String NBT_NAME = "Name"; - private static final String NBT_PREDICTIONS = "Predictions"; - private static final String NBT_TRAIN_DIRECTION = "Direction"; - private static final String NBT_ON_TRAIN = "OnTrain"; - - private final UUID trainId; - private final String trainName; - private final List predictions; - private final double speed; - private final int ticksWaitingForSignal; - private final boolean oppositeDirection; - - private final boolean onTrain; - - private final Cache> stopovers = new Cache<>(() -> { - List s = new ArrayList<>(); - if (predictions().size() >= 2) { - for (int i = 1; i < predictions().size() - 1; i++) { - s.add(predictions().get(i)); - } - } - return s; - }); - - public TrainData(UUID trainId, String trainName, List predictions, double speed, int ticksWaitingForSignal, boolean oppositeDirection, boolean onTrain) { - this.trainId = trainId; - this.trainName = trainName; - this.predictions = predictions; - this.speed = speed; - this.ticksWaitingForSignal = ticksWaitingForSignal; - this.oppositeDirection = oppositeDirection; - this.onTrain = onTrain; - } - - public UUID trainId() { - return trainId; - } - - public String trainName() { - return trainName; - } - - public List predictions() { - return predictions; - } - - public double speed() { - return speed; - } - - public int ticksWaitingForSignal() { - return ticksWaitingForSignal; - } - - public List stopovers() { - return stopovers.get(); - } - - public boolean isOppositeDirection() { - return oppositeDirection; - } - - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putUUID(NBT_TRAIN_ID, trainId); - nbt.putDouble(NBT_SPEED, speed); - nbt.putInt(NBT_WAITING_FOR_SIGNAL, ticksWaitingForSignal); - nbt.putString(NBT_NAME, trainName); - nbt.putBoolean(NBT_TRAIN_DIRECTION, oppositeDirection); - nbt.putBoolean(NBT_ON_TRAIN, onTrain); - - ListTag list = new ListTag(); - list.addAll(predictions().stream().map(x -> x.toNbt()).toList()); - nbt.put(NBT_PREDICTIONS, list); - return nbt; - } - - public static TrainData fromNbt(CompoundTag nbt) { - return new TrainData( - nbt.getUUID(NBT_TRAIN_ID), - nbt.getString(NBT_NAME), - nbt.getList(NBT_PREDICTIONS, Tag.TAG_COMPOUND).stream().map(x -> SimpleDeparturePrediction.fromNbt(((CompoundTag)x))).toList(), - nbt.getDouble(NBT_SPEED), - nbt.getInt(NBT_WAITING_FOR_SIGNAL), - nbt.getBoolean(NBT_TRAIN_DIRECTION), - nbt.getBoolean(NBT_ON_TRAIN) - ); - } - - public Optional getNextStop() { - return predictions().size() > 0 ? Optional.of(predictions().get(0)) : Optional.empty(); - } - - public Optional getLastStop() { - return predictions().size() > 0 ? Optional.of(predictions().get(predictions().size() - 1)) : Optional.empty(); - } - - public static TrainData empty(boolean onTrain) { - MutableComponent text = onTrain ? ELanguage.translate("block.createrailwaysnavigator.advanced_display.ber.shunting_trip") : ELanguage.translate("block.createrailwaysnavigator.advanced_display.ber.not_in_service"); - return new TrainData(Constants.ZERO_UUID, "", List.of(new SimpleDeparturePrediction("", "", 0, text.getString(), "", Constants.ZERO_UUID, null, TrainExitSide.UNKNOWN)), 0, 0, false, onTrain); - } - - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/GlobalSettingsResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/GlobalSettingsResponsePacket.java deleted file mode 100644 index 2f97790d..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/GlobalSettingsResponsePacket.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.data.GlobalSettings; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.Env; -import dev.architectury.utils.EnvExecutor; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.FriendlyByteBuf; - -public class GlobalSettingsResponsePacket implements IPacketBase { - public long id; - public GlobalSettings settings; - - public GlobalSettingsResponsePacket() { } - - public GlobalSettingsResponsePacket(long id, GlobalSettings settings) { - this.id = id; - this.settings = settings; - } - - @Override - public void encode(GlobalSettingsResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeNbt(packet.settings.toNbt(new CompoundTag())); - } - - @Override - public GlobalSettingsResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - GlobalSettings settings = GlobalSettings.fromNbt(buffer.readNbt()); - return new GlobalSettingsResponsePacket(id, settings); - } - - @Override - public void handle(GlobalSettingsResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - GlobalSettingsManager.createClientInstance().updateSettingsData(packet.settings); - InstanceManager.runClientResponseReceievedAction(packet.id); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NavigationResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NavigationResponsePacket.java deleted file mode 100644 index 043ea74e..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NavigationResponsePacket.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.List; -import java.util.ArrayList; -import java.util.function.Supplier; - -import de.mrjulsen.crn.data.SimpleRoute; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class NavigationResponsePacket implements IPacketBase { - public long id; - public List routes; - public long duration; - public long lastUpdated; - - public NavigationResponsePacket() { } - - public NavigationResponsePacket(long id, List routes, long duration, long lastUpdated) { - this.id = id; - this.routes = routes; - this.duration = duration; - this.lastUpdated = lastUpdated; - } - - @Override - public void encode(NavigationResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeLong(packet.duration); - buffer.writeLong(packet.lastUpdated); - buffer.writeInt(packet.routes.size()); - for (SimpleRoute route : packet.routes) { - buffer.writeNbt(route.toNbt()); - } - } - - @Override - public NavigationResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - long duration = buffer.readLong(); - long lastUpdated = buffer.readLong(); - int routesCount = buffer.readInt(); - List routes = new ArrayList<>(routesCount); - for (int i = 0; i < routesCount; i++) { - routes.add(SimpleRoute.fromNbt(buffer.readNbt())); - } - return new NavigationResponsePacket(id, routes, duration, lastUpdated); - } - - @Override - public void handle(NavigationResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - InstanceManager.runClientNavigationResponseAction(packet.id, packet.routes, new NavigationResponseData(packet.lastUpdated, packet.duration)); - }); - }); - } - - public static record NavigationResponseData(long lastUpdated, long calculationTime) {} -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NearestStationResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NearestStationResponsePacket.java deleted file mode 100644 index cd865cab..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NearestStationResponsePacket.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.data.NearestTrackStationResult; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class NearestStationResponsePacket implements IPacketBase { - public long id; - public NearestTrackStationResult result; - - public NearestStationResponsePacket() { } - - public NearestStationResponsePacket(long id, NearestTrackStationResult result) { - this.id = id; - this.result = result; - } - - @Override - public void encode(NearestStationResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - packet.result.serialize(buffer); - } - - @Override - public NearestStationResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - NearestTrackStationResult result = NearestTrackStationResult.deserialize(buffer); - return new NearestStationResponsePacket(id, result); - } - - @Override - public void handle(NearestStationResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - InstanceManager.runClientNearestStationResponseAction(packet.id, packet.result); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NextConnectionsResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NextConnectionsResponsePacket.java deleted file mode 100644 index 50a14eb1..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/NextConnectionsResponsePacket.java +++ /dev/null @@ -1,60 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Supplier; - -import de.mrjulsen.crn.data.SimpleTrainConnection; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class NextConnectionsResponsePacket implements IPacketBase { - public long id; - public Collection connections; - public long time; - - public NextConnectionsResponsePacket() { } - - public NextConnectionsResponsePacket(long id, Collection connections, long time) { - this.id = id; - this.connections = connections; - this.time = time; - } - - @Override - public void encode(NextConnectionsResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeLong(packet.time); - buffer.writeInt(packet.connections.size()); - for (SimpleTrainConnection s : packet.connections) { - buffer.writeNbt(s.toNbt()); - } - } - - @Override - public NextConnectionsResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - long time = buffer.readLong(); - int count = buffer.readInt(); - List connections = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - connections.add(SimpleTrainConnection.fromNbt(buffer.readNbt())); - } - return new NextConnectionsResponsePacket(id, connections, time); - } - - @Override - public void handle(NextConnectionsResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - InstanceManager.runClientNextConnectionsResponseAction(packet.id, packet.connections, packet.time); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/RealtimeResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/RealtimeResponsePacket.java deleted file mode 100644 index a8eed92d..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/RealtimeResponsePacket.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Supplier; - -import de.mrjulsen.crn.data.DeparturePrediction.SimpleDeparturePrediction; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class RealtimeResponsePacket implements IPacketBase { - public long id; - public Collection departure; - public long time; - - public RealtimeResponsePacket() { } - - public RealtimeResponsePacket(long id, Collection departure, long time) { - this.id = id; - this.departure = departure; - this.time = time; - } - - @Override - public void encode(RealtimeResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeLong(packet.time); - buffer.writeInt(packet.departure.size()); - for (SimpleDeparturePrediction s : packet.departure) { - buffer.writeNbt(s.toNbt()); - } - } - - @Override - public RealtimeResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - long time = buffer.readLong(); - int count = buffer.readInt(); - List departure = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - departure.add(SimpleDeparturePrediction.fromNbt(buffer.readNbt())); - } - return new RealtimeResponsePacket(id, departure, time); - } - - @Override - public void handle(RealtimeResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - new Thread(() -> { - InstanceManager.runClientRealtimeResponseAction(packet.id, packet.departure, packet.time); - }, "Realtime Processor").run(); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/ServerErrorPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/ServerErrorPacket.java index 20974106..5f78907d 100644 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/ServerErrorPacket.java +++ b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/ServerErrorPacket.java @@ -6,7 +6,7 @@ import de.mrjulsen.mcdragonlib.net.IPacketBase; import dev.architectury.networking.NetworkManager.PacketContext; import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; +import net.fabricmc.api.EnvType; import net.minecraft.network.FriendlyByteBuf; public class ServerErrorPacket implements IPacketBase { @@ -30,7 +30,7 @@ public ServerErrorPacket decode(FriendlyByteBuf buffer) { @Override public void handle(ServerErrorPacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { + EnvExecutor.runInEnv(EnvType.CLIENT, () -> () -> { contextSupplier.get().queue(() -> { ClientWrapper.handleErrorMessagePacket(packet, contextSupplier); }); diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TimeCorrectionPacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TimeCorrectionPacket.java deleted file mode 100644 index 20d2454c..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TimeCorrectionPacket.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.event.listeners.JourneyListenerManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class TimeCorrectionPacket implements IPacketBase { - public int amount; - - public TimeCorrectionPacket() { } - - public TimeCorrectionPacket(int amount) { - this.amount = amount; - } - - @Override - public void encode(TimeCorrectionPacket packet, FriendlyByteBuf buffer) { - buffer.writeInt(packet.amount); - } - - @Override - public TimeCorrectionPacket decode(FriendlyByteBuf buffer) { - return new TimeCorrectionPacket(buffer.readInt()); - } - - @Override - public void handle(TimeCorrectionPacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - JourneyListenerManager.getInstance().getAllListeners().forEach(x -> x.getListeningRoute().shiftTime(packet.amount)); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrackStationResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrackStationResponsePacket.java deleted file mode 100644 index b99f9c07..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrackStationResponsePacket.java +++ /dev/null @@ -1,83 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Supplier; - -import de.mrjulsen.crn.data.ClientTrainStationSnapshot; -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class TrackStationResponsePacket implements IPacketBase { - public long id; - public Collection stationNames; - public Collection trainNames; - public int listingTrainCount; - public int totalTrainCount; - - public TrackStationResponsePacket() { } - - public TrackStationResponsePacket(long id, Collection stationNames, Collection trainNames, int listingTrainCount, int totalTrainCount) { - this.id = id; - this.stationNames = stationNames; - this.trainNames = trainNames; - this.listingTrainCount = listingTrainCount; - this.totalTrainCount = totalTrainCount; - } - - @Override - public void encode(TrackStationResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeInt(packet.listingTrainCount); - buffer.writeInt(packet.totalTrainCount); - buffer.writeInt(packet.stationNames.size()); - for (String s : packet.stationNames) { - buffer.writeUtf(s); - } - - buffer.writeInt(packet.trainNames.size()); - for (String s : packet.trainNames) { - buffer.writeUtf(s); - } - } - - @Override - public TrackStationResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - int listingTrainCount = buffer.readInt(); - int totalTrainCount = buffer.readInt(); - int count = buffer.readInt(); - List stationNames = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - stationNames.add(buffer.readUtf()); - } - - count = buffer.readInt(); - List trainNames = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - trainNames.add(buffer.readUtf()); - } - return new TrackStationResponsePacket(id, stationNames, trainNames, listingTrainCount, totalTrainCount); - } - - @Override - public void handle(TrackStationResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - ClientTrainStationSnapshot.makeNew( - packet.stationNames == null || packet.stationNames.isEmpty() ? new ArrayList<>() : new ArrayList<>(packet.stationNames), - packet.trainNames == null || packet.trainNames.isEmpty() ? new ArrayList<>() : new ArrayList<>(packet.trainNames), - packet.listingTrainCount, - packet.totalTrainCount - ); - InstanceManager.runClientResponseReceievedAction(packet.id); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrainDataResponsePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrainDataResponsePacket.java deleted file mode 100644 index 99208745..00000000 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/stc/TrainDataResponsePacket.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.mrjulsen.crn.network.packets.stc; - -import java.util.function.Supplier; - -import de.mrjulsen.crn.network.InstanceManager; -import de.mrjulsen.crn.network.packets.cts.TrainDataRequestPacket.TrainData; -import de.mrjulsen.mcdragonlib.net.IPacketBase; -import dev.architectury.networking.NetworkManager.PacketContext; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; -import net.minecraft.network.FriendlyByteBuf; - -public class TrainDataResponsePacket implements IPacketBase { - public long id; - public TrainData departure; - public long time; - - public TrainDataResponsePacket() { } - - public TrainDataResponsePacket(long id, TrainData departure, long time) { - this.id = id; - this.departure = departure; - this.time = time; - } - - @Override - public void encode(TrainDataResponsePacket packet, FriendlyByteBuf buffer) { - buffer.writeLong(packet.id); - buffer.writeLong(packet.time); - buffer.writeNbt(packet.departure.toNbt()); - } - - @Override - public TrainDataResponsePacket decode(FriendlyByteBuf buffer) { - long id = buffer.readLong(); - long time = buffer.readLong(); - TrainData departure = TrainData.fromNbt(buffer.readNbt()); - - return new TrainDataResponsePacket(id, departure, time); - } - @Override - public void handle(TrainDataResponsePacket packet, Supplier contextSupplier) { - EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { - contextSupplier.get().queue(() -> { - InstanceManager.runClientTrainDataResponseAction(packet.id, packet.departure, packet.time); - }); - }); - } -} - diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java b/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java new file mode 100644 index 00000000..17d2146a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java @@ -0,0 +1,932 @@ +package de.mrjulsen.crn.registry; + +import java.util.UUID; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; + +import com.simibubi.create.content.trains.entity.Train; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.ArrayList; +import java.util.Optional; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.data.NearestTrackStationResult; +import de.mrjulsen.crn.data.StationTag; +import de.mrjulsen.crn.data.TagName; +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.crn.data.UserSettings; +import de.mrjulsen.crn.data.StationTag.StationInfo; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient.AddStationTagEntryData; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient.RemoveStationTagEntryData; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient.UpdateStationTagNameData; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient.UpdateTrainGroupColorData; +import de.mrjulsen.crn.data.storage.GlobalSettingsClient.UpdateTrainLineColorData; +import de.mrjulsen.crn.data.train.ClientTrainStop; +import de.mrjulsen.crn.data.train.TrainData; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainPrediction; +import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.crn.data.train.TrainTravelSection; +import de.mrjulsen.crn.data.train.TrainUtils; +import de.mrjulsen.crn.data.train.ClientTrainStop.TrainStopRealTimeData; +import de.mrjulsen.crn.data.train.portable.NextConnectionsDisplayData; +import de.mrjulsen.crn.data.train.portable.TrainDisplayData; +import de.mrjulsen.crn.debug.TrainDebugData; +import de.mrjulsen.crn.data.navigation.ClientRoute; +import de.mrjulsen.crn.data.navigation.NavigatableGraph; +import de.mrjulsen.crn.data.navigation.Route; +import de.mrjulsen.crn.data.navigation.RoutePart; +import de.mrjulsen.crn.data.navigation.ClientRoutePart.TrainRealTimeData; +import de.mrjulsen.mcdragonlib.data.Pair; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import de.mrjulsen.mcdragonlib.util.accessor.BasicDataAccessorPacket.IChunkReceiver; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessorType; +import de.mrjulsen.crn.network.packets.stc.ServerErrorPacket; +import de.mrjulsen.crn.registry.data.NextConnectionsRequestData; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +public final class ModAccessorTypes { + +//#region STATION TAGS + + public static final DataAccessorType GET_STATION_TAG = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_station_tag"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, GlobalSettings.getInstance().getOrCreateStationTagFor(in).toNbt()); + return false; + }, (hasMore, previousData, iteration, nbt) -> { + return StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null); + } + )); + + public static final DataAccessorType GET_STATION_TAG_BY_TAG_NAME = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_station_tag_by_tag_name"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in.get()); + }, (nbt) -> { + return TagName.of(nbt.getString(DataAccessorType.DEFAULT_NBT_DATA)); + }, (player, in, temp, nbt, iteration) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, GlobalSettings.getInstance().getOrCreateStationTagFor(in).toNbt()); + return false; + }, (hasMore, previousData, iteration, nbt) -> { + return StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null); + } + )); + + public static final DataAccessorType CREATE_STATION_TAG = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "create_station_tag"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, GlobalSettings.getInstance().createOrGetStationTag(in).toNbt()); + return false; + }, (hasMore, data, iteration, nbt) -> { + return StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null); + } + )); + + public static final DataAccessorType REGISTER_STATION_TAG = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "register_station_tag"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, in.toNbt()); + }, (nbt) -> { + return StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null); + }, (player, in, temp, nbt, iteration) -> { + in.updateLastEdited(player.getGameProfile().getName()); + GlobalSettings.getInstance().registerStationTag(in); + return false; + } + )); + + public static final DataAccessorType DELETE_STATION_TAG = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "delete_station_tag"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getUUID(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().removeStationTag(in); + return false; + } + )); + + public static final DataAccessorType UPDATE_STATION_TAG_NAME = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "update_station_tag_name"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putUUID("Id", in.tagId()); + nbt.putString("Name", in.name()); + }, (nbt) -> { + return new UpdateStationTagNameData(nbt.getUUID("Id"), nbt.getString("Name")); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().getStationTag(in.tagId()).ifPresent(x -> { + x.updateLastEdited(player.getGameProfile().getName()); + x.setName(TagName.of(in.name())); + }); + return false; + } + )); + + public static final DataAccessorType, Optional> ADD_STATION_TAG_ENTRY = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "add_station_tag_entry"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID("Id", in.tagId()); + nbt.putString("Name", in.station()); + nbt.put("Info", in.info().toNbt()); + }, (nbt) -> { + return new AddStationTagEntryData(nbt.getUUID("Id"), nbt.getString("Name"), StationInfo.fromNbt(nbt.getCompound("Info"))); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().getStationTag(in.tagId()).ifPresent(x -> { + x.add(in.station(), in.info()); + x.updateLastEdited(player.getGameProfile().getName()); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, x.toNbt()); + }); + return false; + }, (hasMore, data, iteration, nbt) -> { + return Optional.ofNullable(nbt.contains(DataAccessorType.DEFAULT_NBT_DATA) ? StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null) : null); + } + )); + + public static final DataAccessorType, Optional> UPDATE_STATION_TAG_ENTRY = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "update_station_tag_entry"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID("Id", in.tagId()); + nbt.putString("Name", in.station()); + nbt.put("Info", in.info().toNbt()); + }, (nbt) -> { + return new AddStationTagEntryData(nbt.getUUID("Id"), nbt.getString("Name"), StationInfo.fromNbt(nbt.getCompound("Info"))); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().getStationTag(in.tagId()).ifPresent(x -> { + x.updateInfoForStation(in.station(), in.info()); + x.updateLastEdited(player.getGameProfile().getName()); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, x.toNbt()); + }); + return false; + }, (hasMore, data, iteration, nbt) -> { + return Optional.ofNullable(nbt.contains(DataAccessorType.DEFAULT_NBT_DATA) ? StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null) : null); + } + )); + + public static final DataAccessorType, Optional> REMOVE_STATION_TAG_ENTRY = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "remove_station_tag_entry"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID("Id", in.tagId()); + nbt.putString("Name", in.station()); + }, (nbt) -> { + return new RemoveStationTagEntryData(nbt.getUUID("Id"), nbt.getString("Name")); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().getStationTag(in.tagId()).ifPresent(x -> { + x.remove(in.station()); + x.updateLastEdited(player.getGameProfile().getName()); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, x.toNbt()); + }); + return false; + }, (hasMore, data, iteration, nbt) -> { + return Optional.ofNullable(nbt.contains(DataAccessorType.DEFAULT_NBT_DATA) ? StationTag.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), null) : null); + } + )); + + public static final DataAccessorType, Collection> GET_ALL_STATION_TAGS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_station_tags"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllStationTags().stream().sorted((a, b) -> a.getTagName().get().compareToIgnoreCase(b.getTagName().get())).toList())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + if (tags.isEmpty()) { + return false; + } + for (int i = 0; i < 64 && !tags.isEmpty(); i++) { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll().toNbt()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(StationTag.fromNbt(nbt.getCompound(x), null))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + /** Input: true = exclude blacklisted */ + public static final DataAccessorType, Collection> GET_ALL_STATIONS_AS_TAGS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_stations_as_tags"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putBoolean(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getBoolean(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(TrainUtils.getAllStations().stream().filter(x -> !in || !GlobalSettings.getInstance().isStationBlacklisted(x)).map(x -> GlobalSettings.getInstance().getOrCreateStationTagFor(x)).distinct().sorted((a, b) -> a.getTagName().get().compareToIgnoreCase(b.getTagName().get())).toList())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + if (tags.isEmpty()) { + return false; + } + for (int i = 0; i < 64 && !tags.isEmpty(); i++) { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll().toNbt()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(StationTag.fromNbt(nbt.getCompound(x), null))); + return l; + }, (chunks) -> { + return chunks; + } + )); + +//#endregion +//#region TRAIN GROUPS + + public static final DataAccessorType, List> GET_ALL_TRAIN_GROUPS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_train_groups"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllTrainGroups())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + if (tags.isEmpty()) { + return false; + } + for (int i = 0; i < 64 && !tags.isEmpty(); i++) { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll().toNbt()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final List l = list; + nbt.getAllKeys().forEach(x -> l.add(TrainGroup.fromNbt(nbt.getCompound(x)))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType DELETE_TRAIN_GROUP = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "delete_train_group"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().removeTrainGroup(in); + return false; + } + )); + + public static final DataAccessorType UPDATE_TRAIN_GROUP_COLOR = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "update_train_group_color"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putString("Id", in.name()); + nbt.putInt("Color", in.color()); + }, (nbt) -> { + return new UpdateTrainGroupColorData(nbt.getString("Id"), nbt.getInt("Color")); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().getTrainGroup(in.name()).ifPresent(x -> { + x.setColor(in.color()); + }); + return false; + } + )); + + public static final DataAccessorType CREATE_TRAIN_GROUP = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "create_train_group"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + TrainGroup group = GlobalSettings.getInstance().createOrGetTrainGroup(in); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, group.toNbt()); + return false; + }, (hasMore, data, iteration, nbt) -> { + return TrainGroup.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + } + )); + +//#endregion +//#region STATION BLACKLIST + + public static final DataAccessorType, Collection> ADD_STATION_TO_BLACKLIST = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "add_station_to_blacklist"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + GlobalSettings.getInstance().blacklistStation(in); + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllBlacklistedStations())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType, Collection> REMOVE_STATION_FROM_BLACKLIST = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "remove_station_from_blacklist"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + GlobalSettings.getInstance().removeStationFromBlacklist(in); + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllBlacklistedStations())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType, List> GET_BLACKLISTED_STATIONS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_blacklisted_stations"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllBlacklistedStations())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final List l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + +//#endregion +//#region TRAIN BLACKLIST + + public static final DataAccessorType, Collection> ADD_TRAIN_TO_BLACKLIST = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "add_train_to_blacklist"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + GlobalSettings.getInstance().blacklistTrain(in); + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllBlacklistedTrains())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType, Collection> REMOVE_TRAIN_FROM_BLACKLIST = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "remove_train_from_blacklist"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + GlobalSettings.getInstance().removeTrainFromBlacklist(in); + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllBlacklistedTrains())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType, List> GET_BLACKLISTED_TRAINS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_blacklisted_trains"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllBlacklistedTrains())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final List l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + + + public static final DataAccessorType UPDATE_REALTIME = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "update_realtime"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getUUID(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (!TrainListener.data.containsKey(in)) { + return false; + } + + TrainData data = TrainListener.data.get(in); + Map values = data.getPredictions().stream().map(a -> new TrainStopRealTimeData( + a.getStationTag().getClientTag(a.getStationName()), + a.getEntryIndex(), + a.getScheduledArrivalTime(), + a.getScheduledDepartureTime(), + a.getRealTimeArrivalTime(), + a.getRealTimeDepartureTime(), + a.getArrivalTimeDeviation(), + a.getDepartureTimeDeviation(), + a.getRealTimeArrivalTicks(), + a.getCurrentCycle() + )).collect(Collectors.toMap(a -> a.entryIndex(), a -> a)); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, new TrainRealTimeData(data.getSessionId(), values, data.getStatus(), data.isCancelled()).toNbt()); + return false; + }, (hasMore, previousData, iteration, nbt) -> { + return nbt.contains(DataAccessorType.DEFAULT_NBT_DATA) ? TrainRealTimeData.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)) : null; + } + )); + + + + public static final DataAccessorType GET_USER_SETTINGS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_user_settings"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getUUID(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, UserSettings.getSettingsFor(in, false).toNbt()); + nbt.putUUID("Id", in); + return false; + }, (hasMore, list, iteration, nbt) -> { + return UserSettings.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), nbt.getUUID("Id"), false); + } + )); + + public static final DataAccessorType SAVE_USER_SETTINGS = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "save_user_settings"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, in.toNbt()); + nbt.putUUID("Id", in.getOwnerId()); + }, (nbt) -> { + return UserSettings.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), nbt.getUUID("Id"), false); + }, (player, in, temp, nbt, iteration) -> { + in.save(); + return false; + } + )); + + public static final DataAccessorType GET_NEAREST_STATION = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_nearest_station"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putInt("x", in.getX()); + nbt.putInt("y", in.getY()); + nbt.putInt("z", in.getZ()); + }, (nbt) -> { + return new BlockPos(nbt.getInt("x"), nbt.getInt("y"), nbt.getInt("z")); + }, (player, in, temp, nbt, iteration) -> { + NearestTrackStationResult result = NearestTrackStationResult.empty(); + try { + result = TrainUtils.getNearestTrackStation(player.level, in); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Error while trying to find nearest track station.", e); + CreateRailwaysNavigator.net().CHANNEL.sendToPlayer((ServerPlayer)player, new ServerErrorPacket(e.getMessage())); + } + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, result.toNbt()); + return false; + }, (hasMore, list, iteration, nbt) -> { + return NearestTrackStationResult.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + } + )); + + public static final DataAccessorType GET_TRAIN_DISPLAY_DATA = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_train_display_data"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getUUID(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + Optional trainOpt = TrainUtils.getTrain(in); + if (!trainOpt.isPresent() || !TrainUtils.isTrainUsable(trainOpt.get()) || GlobalSettings.getInstance().isTrainBlacklisted(trainOpt.get())) { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, TrainDisplayData.empty().toNbt()); + return false; + } + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, TrainDisplayData.of(trainOpt.get()).toNbt()); + return false; + }, (hasMore, list, iteration, nbt) -> { + return TrainDisplayData.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + } + )); + + public static final DataAccessorType GET_NEXT_CONNECTIONS_DISPLAY_DATA = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_next_connections_display_data"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, in.toNbt()); + }, (nbt) -> { + return NextConnectionsRequestData.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + }, (player, in, temp, nbt, iteration) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, NextConnectionsDisplayData.at(in.stationName(), in.selfTrainId()).toNbt()); + return false; + }, (hasMore, list, iteration, nbt) -> { + return NextConnectionsDisplayData.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + } + )); + + + public static final DataAccessorType, Collection> GET_ALL_TRAIN_NAMES = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_train_names"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(TrainUtils.getTrains(false).stream().map(x -> x.name.getString()).toList())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType, Collection> GET_ALL_STATION_NAMES = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_station_names"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(TrainUtils.getAllStations().stream().map(x -> x.name).toList())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final Collection l = list; + nbt.getAllKeys().forEach(x -> l.add(nbt.getString(x))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + //#region TRAIN LINES + + public static final DataAccessorType, List> GET_ALL_TRAIN_LINES = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_train_lines"), DataAccessorType.Builder.createNoInputChunked( + (player, in, temp, nbt, iteration) -> { + if (temp.getFirst() == null) { + temp.setFirst(new ConcurrentLinkedQueue<>(GlobalSettings.getInstance().getAllTrainLines())); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + if (tags.isEmpty()) { + return false; + } + + for (int i = 0; i < 256 && !tags.isEmpty(); i++) { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA + i, tags.poll().toNbt()); + } + return !tags.isEmpty(); + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (list == null) { + list = new ArrayList(); + } + final List l = list; + nbt.getAllKeys().forEach(x -> l.add(TrainLine.fromNbt(nbt.getCompound(x)))); + return l; + }, (chunks) -> { + return chunks; + } + )); + + public static final DataAccessorType DELETE_TRAIN_LINE = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "delete_train_line"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().removeTrainLine(in); + return false; + } + )); + + public static final DataAccessorType UPDATE_TRAIN_LINE_COLOR = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "update_train_line_color"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putString("Id", in.name()); + nbt.putInt("Color", in.color()); + }, (nbt) -> { + return new UpdateTrainLineColorData(nbt.getString("Id"), nbt.getInt("Color")); + }, (player, in, temp, nbt, iteration) -> { + GlobalSettings.getInstance().getTrainLine(in.name()).ifPresent(x -> { + x.setColor(in.color()); + }); + return false; + } + )); + + public static final DataAccessorType CREATE_TRAIN_LINE = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "create_train_line"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + TrainLine group = GlobalSettings.getInstance().createOrGetTrainLine(in); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, group.toNbt()); + return false; + }, (hasMore, data, iteration, nbt) -> { + return TrainLine.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + } + )); + + //#endregion + + public static record NavigationData(String start, String end, UUID player) {} + public static final DataAccessorType, List> NAVIGATE = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "navigate"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putString("Start", in.start()); + nbt.putString("End", in.end()); + nbt.putUUID("Player", in.player()); + }, (nbt) -> { + return new NavigationData(nbt.getString("Start"), nbt.getString("End"), nbt.getUUID("Player")); + }, (player, in, temp, nbt, iteration) -> { + try { + if (temp.getFirst() == null) { + GlobalSettings settings = GlobalSettings.getInstance(); + List routes = NavigatableGraph.searchRoutes( + settings.getTagByName(TagName.of(in.start())).orElse(settings.getOrCreateStationTagFor(in.start())), + settings.getTagByName(TagName.of(in.end())).orElse(settings.getOrCreateStationTagFor(in.end())), + in.player(), + true + ); + temp.setFirst(new ConcurrentLinkedQueue<>(routes)); + } + @SuppressWarnings("unchecked") + Queue tags = (Queue)((MutableSingle)temp).getFirst(); + if (tags.isEmpty()) { + return false; + } + Route r = tags.poll(); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, r.toNbt()); + return !tags.isEmpty(); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Navigation error.", e); + } + return false; + }, (IChunkReceiver>)(hasMore, list, iteration, nbt) -> { + if (!nbt.contains(DataAccessorType.DEFAULT_NBT_DATA)) { + return List.of(); + } + if (list == null) { + list = new ArrayList(); + } + list.add(ClientRoute.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), true)); + return list; + }, (chunks) -> { + return chunks; + } + )); + + public static record DepartureRoutesData(String stationTagName, UUID player) {} + public static final DataAccessorType>, List>> GET_DEPARTURE_AND_ARRIVAL_ROUTES_AT = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_departure_and_arrival_routes_at"), DataAccessorType.Builder.createChunked( + (in, nbt) -> { + nbt.putString("Station", in.stationTagName()); + nbt.putUUID("Player", in.player()); + }, (nbt) -> { + return new DepartureRoutesData(nbt.getString("Station"), nbt.getUUID("Player")); + }, (player, in, temp, nbt, iteration) -> { + try { + if (temp.getFirst() == null) { + UserSettings settings = UserSettings.getSettingsFor(in.player(), true); + StationTag station = GlobalSettings.getInstance().getOrCreateStationTagFor(TagName.of(in.stationTagName())); + Set trains = TrainUtils.getDepartingTrainsAt(station).stream().filter(x -> + TrainUtils.isTrainUsable(x) && + !GlobalSettings.getInstance().isTrainBlacklisted(x) && + TrainListener.data.containsKey(x.id) + ).collect(Collectors.toSet()); + + List> routesL = new ArrayList<>(); + for (Train train : trains) { + TrainData data = TrainListener.data.get(train.id); + List matchingPredictions = data.getPredictionsChronologically().stream().filter(x -> x.getStationTag().equals(station)).toList(); + + for (TrainPrediction prediction : matchingPredictions) { + TrainTravelSection section = prediction.getSection(); + if ((!section.isUsable() && !(section.isFirstStop(prediction) && section.previousSection().isUsable() && section.previousSection().shouldIncludeNextStationOfNextSection())) || (section.getTrainGroup() != null && settings.searchExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))) { + continue; + } + + TrainTravelSection previousSection = section.previousSection(); + boolean isStart = section.isFirstStop(prediction); + boolean isStartAndFinal = isStart && previousSection.isUsable() && previousSection.shouldIncludeNextStationOfNextSection() && (previousSection.getTrainGroup() == null || !settings.searchExcludedTrainGroups.getValue().contains(previousSection.getTrainGroup().getGroupName())); + + TrainStop stop = new TrainStop(prediction); + stop.simulateTicks(settings.searchDepartureInTicks.getValue()); + TrainPrediction fromPrediction = section.getFirstStop().get(); + TrainStop from = new TrainStop(fromPrediction); + + Route route = new Route(List.of(new RoutePart(data.getSessionId(), train.id, List.of(stop /* current/target */, from /* from */), section.getAllStops(settings.searchDepartureInTicks.getValue(), prediction.getEntryIndex()))), false); + + if ((!isStart || isStartAndFinal) && (section.getTrainGroup() == null || !settings.searchExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))) { + + Route selectedRoute = route; + if (isStartAndFinal) { + TrainPrediction frPred = previousSection.getFirstStop().get(); + TrainStop fr = new TrainStop(frPred); + selectedRoute = new Route(List.of(new RoutePart(data.getSessionId(), train.id, List.of(stop /* current/target */, fr /* from */), previousSection.getAllStops(settings.searchDepartureInTicks.getValue(), prediction.getEntryIndex()))), false); + } + routesL.add(Pair.of(true, selectedRoute)); // Arrival + } + if ((section.isUsable()) && (section.getTrainGroup() == null || !settings.searchExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))) { + routesL.add(Pair.of(false, route)); // Departure + } + } + } + + Collections.sort(routesL, (a, b) -> { + long val1 = a.getFirst() ? a.getSecond().getStart().getScheduledArrivalTime() : a.getSecond().getStart().getScheduledDepartureTime(); + long val2 = b.getFirst() ? b.getSecond().getStart().getScheduledArrivalTime() : b.getSecond().getStart().getScheduledDepartureTime(); + return Long.compare(val1, val2); + }); + temp.setFirst(new ConcurrentLinkedQueue<>(routesL)); + } + + @SuppressWarnings("unchecked") + Queue> tags = (Queue>)((MutableSingle)temp).getFirst(); + if (tags.isEmpty()) { + return false; + } + Pair r = tags.poll(); + nbt.putBoolean("IsArrival", r.getFirst()); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, r.getSecond().toNbt()); + return !tags.isEmpty(); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Schedule board generation error.", e); + } + return false; + }, (IChunkReceiver>>)(hasMore, list, iteration, nbt) -> { + if (!nbt.contains(DataAccessorType.DEFAULT_NBT_DATA)) { + return List.of(); + } + if (list == null) { + list = new ArrayList>(); + } + list.add(Pair.of(nbt.getBoolean("IsArrival"), ClientRoute.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA), false))); + return list; + }, (chunks) -> { + return chunks; + } + )); + + + public static record DeparturesData(UUID stationTagId, UUID trainId) {} + public static final DataAccessorType, List> GET_DEPARTURES_AT = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_departures_at"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putUUID("Tag", in.stationTagId()); + nbt.putUUID("Train", in.trainId()); + }, (nbt) -> { + return new DeparturesData(nbt.getUUID("Tag"), nbt.getUUID("Train")); + }, (player, in, temp, nbt, iteration) -> { + try { + StationTag tag = GlobalSettings.getInstance().getStationTag(in.stationTagId()).get(); + ListTag list = new ListTag(); + list.addAll(TrainUtils.getDeparturesAt(tag, in.trainId()).stream().map(x -> x.toNbt(true)).toList()); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, list); + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("Next connections error.", e); + } + return false; + }, (hasMore, data, iteration, nbt) -> { + return nbt.getList(DataAccessorType.DEFAULT_NBT_DATA, Tag.TAG_COMPOUND).stream().map(x -> (ClientTrainStop)ClientTrainStop.fromNbt((CompoundTag)x)).toList(); + } + )); + + public static final DataAccessorType ALL_TRAINS_INITIALIZED = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "all_trains_initialized"), DataAccessorType.Builder.createNoInput( + (player, in, temp, nbt, iteration) -> { + nbt.putBoolean(DataAccessorType.DEFAULT_NBT_DATA, TrainListener.allTrainsInitialized()); + return false; + }, (hasMore, data, iteration, nbt) -> { + return nbt.getBoolean(DataAccessorType.DEFAULT_NBT_DATA); + } + )); + + public static final DataAccessorType, List> GET_ALL_TRAINS_DEBUG_DATA = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_trains_debug_data"), DataAccessorType.Builder.createNoInput( + (player, in, temp, nbt, iteration) -> { + ListTag list = new ListTag(); + list.addAll(TrainListener.data.values().stream().map(x -> TrainDebugData.fromTrain(x).toNbt()).toList()); + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, list); + return false; + }, (hasMore, data, iteration, nbt) -> { + return nbt.getList(DataAccessorType.DEFAULT_NBT_DATA, Tag.TAG_COMPOUND).stream().map(x -> TrainDebugData.fromNbt((CompoundTag)x)).toList(); + } + )); + + public static final DataAccessorType SHOW_TRAIN_DEBUG_SCREEN = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "show_train_debug_screen"), DataAccessorType.Builder.createNoIO( + (player, in, temp, nbt, iteration) -> { + ClientWrapper.showTrainDebugScreen(); + return false; + } + )); + + public static final DataAccessorType TRAIN_SOFT_RESET = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "train_soft_reset"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getUUID(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (TrainListener.data.containsKey(in)) { + TrainListener.data.get(in).resetPredictions(); + } + return false; + } + )); + + public static final DataAccessorType TRAIN_HARD_RESET = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "train_hard_reset"), DataAccessorType.Builder.createEmptyResponse( + (in, nbt) -> { + nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getUUID(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + if (TrainListener.data.containsKey(in)) { + TrainListener.data.get(in).hardResetPredictions(); + } + return false; + } + )); + + public static void init() {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModBlockEntities.java b/common/src/main/java/de/mrjulsen/crn/registry/ModBlockEntities.java index 70bdd6e3..30e8cb91 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModBlockEntities.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModBlockEntities.java @@ -3,8 +3,8 @@ import com.tterrag.registrate.util.entry.BlockEntityEntry; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.block.be.AdvancedDisplayBlockEntity; -import de.mrjulsen.crn.block.be.TrainStationClockBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.TrainStationClockBlockEntity; import de.mrjulsen.mcdragonlib.client.ber.StaticBlockEntityRenderer; public class ModBlockEntities { @@ -17,7 +17,8 @@ public class ModBlockEntities { ModBlocks.ADVANCED_DISPLAY_PANEL, ModBlocks.ADVANCED_DISPLAY_HALF_PANEL, ModBlocks.ADVANCED_DISPLAY_SMALL, - ModBlocks.ADVANCED_DISPLAY_SLOPED + ModBlocks.ADVANCED_DISPLAY_SLOPED, + ModBlocks.ADVANCED_DISPLAY_SLAB ) .renderer(() -> StaticBlockEntityRenderer::new) .register(); @@ -30,6 +31,6 @@ public class ModBlockEntities { .renderer(() -> StaticBlockEntityRenderer::new) .register(); - public static void register() { + public static void init() { } } diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java b/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java index 287d7ef1..89b8ab72 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java @@ -14,6 +14,7 @@ import de.mrjulsen.crn.block.AdvancedDisplayBoardBlock; import de.mrjulsen.crn.block.AdvancedDisplayHalfPanelBlock; import de.mrjulsen.crn.block.AdvancedDisplayPanelBlock; +import de.mrjulsen.crn.block.AdvancedDisplaySlabBlock; import de.mrjulsen.crn.block.AdvancedDisplaySlopedBlock; import de.mrjulsen.crn.block.AdvancedDisplaySmallBlock; import de.mrjulsen.crn.block.TrainStationClockBlock; @@ -21,7 +22,7 @@ import de.mrjulsen.crn.block.connected.AdvancedDisplaySmallCTBehaviour; import de.mrjulsen.crn.block.display.AdvancedDisplayTarget; import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.Env; +import net.fabricmc.api.EnvType; import net.minecraft.client.renderer.RenderType; import net.minecraft.world.level.block.Block; @@ -38,6 +39,16 @@ public class ModBlocks { .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) .item() + .tab(() -> ModCreativeModeTab.MAIN) + .build() + .register(); + public static final BlockEntry ADVANCED_DISPLAY_SLAB = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_slab", AdvancedDisplaySlabBlock::new) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .addLayer(() -> RenderType::cutout) + .initialProperties(SharedProperties::softMetal) + .transform(TagGen.pickaxeOnly()) + .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) + .item() .build() .register(); @@ -105,9 +116,9 @@ public static NonNullConsumer connectedTextures( } protected static void onClient(Supplier toRun) { - EnvExecutor.runInEnv(Env.CLIENT, toRun); + EnvExecutor.runInEnv(EnvType.CLIENT, toRun); } - public static void register() { + public static void init() { } } diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTags.java b/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTags.java index c77385ae..a534dde7 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTags.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTags.java @@ -12,6 +12,7 @@ public static void register() { .add(ModBlocks.ADVANCED_DISPLAY_HALF_PANEL) .add(ModBlocks.ADVANCED_DISPLAY_SMALL) .add(ModBlocks.ADVANCED_DISPLAY_SLOPED) + .add(ModBlocks.ADVANCED_DISPLAY_SLAB) ; } } diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java b/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java new file mode 100644 index 00000000..95edd1e1 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java @@ -0,0 +1,81 @@ +package de.mrjulsen.crn.registry; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.properties.EDisplayInfo; +import de.mrjulsen.crn.block.properties.EDisplayType; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeInfo; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; +import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoInformative; +import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoSimple; +import de.mrjulsen.crn.client.ber.variants.BERPlatformDetailed; +import de.mrjulsen.crn.client.ber.variants.BERPlatformInformative; +import de.mrjulsen.crn.client.ber.variants.BERPlatformSimple; +import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationDetailed; +import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationInformative; +import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationSimple; +import net.minecraft.resources.ResourceLocation; + +public final class ModDisplayTypes { + + public static final DisplayTypeResourceKey PASSENGER_INFORMATION_RUNNING_TEXT = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.PASSENGER_INFORMATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "running_text"), + BERPassengerInfoSimple::new, new DisplayTypeInfo(true, null)); + + public static final DisplayTypeResourceKey PASSENGER_INFORMATION_OVERVIEW = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.PASSENGER_INFORMATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "detailed_with_schedule"), + BERPassengerInfoInformative::new, new DisplayTypeInfo(false, null)); + + public static final DisplayTypeResourceKey TRAIN_DESTINATION_SIMPLE = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.TRAIN_DESTINATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "simple"), + BERTrainDestinationSimple::new, new DisplayTypeInfo(true, null)); + + public static final DisplayTypeResourceKey TRAIN_DESTINATION_DETAILED = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.TRAIN_DESTINATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "extended"), + BERTrainDestinationDetailed::new, new DisplayTypeInfo(true, null)); + + public static final DisplayTypeResourceKey TRAIN_DESTINATION_OVERVIEW = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.TRAIN_DESTINATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "detailed"), + BERTrainDestinationInformative::new, new DisplayTypeInfo(true, null)); + + public static final DisplayTypeResourceKey PLATFORM_RUNNING_TEXT = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.PLATFORM, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "running_text"), + BERPlatformSimple::new, new DisplayTypeInfo(true, be -> 32)); + + public static final DisplayTypeResourceKey PLATFORM_TABLE = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.PLATFORM, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "table"), + BERPlatformDetailed::new, new DisplayTypeInfo(false, be -> be.getYSize() * 3 - 1)); + + public static final DisplayTypeResourceKey PLATFORM_FOCUS = AdvancedDisplaysRegistry.registerDisplayType( + EDisplayType.PLATFORM, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "focus"), + BERPlatformInformative::new, new DisplayTypeInfo(false, be -> be.getYSize() * 3 - 2)); + + @Deprecated + public static DisplayTypeResourceKey legacy_getKeyForType(EDisplayType type, EDisplayInfo info) { + switch (type) { + case PASSENGER_INFORMATION -> { + switch (info) { + case INFORMATIVE -> { return PASSENGER_INFORMATION_OVERVIEW; } + default -> { return PASSENGER_INFORMATION_RUNNING_TEXT; } + } + } + case TRAIN_DESTINATION -> { + switch (info) { + case DETAILED -> { return TRAIN_DESTINATION_DETAILED; } + case INFORMATIVE -> { return TRAIN_DESTINATION_OVERVIEW; } + default -> { return TRAIN_DESTINATION_SIMPLE; } + } + } + case PLATFORM -> { + switch (info) { + case DETAILED -> { return PLATFORM_TABLE; } + case INFORMATIVE -> { return PLATFORM_FOCUS; } + default -> { return PLATFORM_RUNNING_TEXT; } + } + } + default -> { return TRAIN_DESTINATION_SIMPLE; } + } + } + + public static void init() {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModExtras.java b/common/src/main/java/de/mrjulsen/crn/registry/ModExtras.java index 2f4b5653..2e2d4c93 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModExtras.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModExtras.java @@ -14,7 +14,7 @@ public class ModExtras { public static boolean registeredTrackStationSource = false; - public static void register() { + public static void init() { Block maybeRegistered = null; try { diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModItems.java b/common/src/main/java/de/mrjulsen/crn/registry/ModItems.java index def3fb7a..8fc5a2c7 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModItems.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModItems.java @@ -15,6 +15,6 @@ public class ModItems { .properties(p -> p.stacksTo(1)) .register(); - public static void register() { + public static void init() { } } diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java b/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java new file mode 100644 index 00000000..83a424dd --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java @@ -0,0 +1,34 @@ +package de.mrjulsen.crn.registry; + +import java.util.function.Supplier; + +import com.simibubi.create.content.trains.schedule.Schedule; +import com.simibubi.create.content.trains.schedule.condition.ScheduleWaitCondition; +import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction; +import com.simibubi.create.foundation.utility.Pair; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; +import de.mrjulsen.crn.data.schedule.instruction.ResetTimingsInstruction; +import de.mrjulsen.crn.data.schedule.instruction.TravelSectionInstruction; +import net.minecraft.resources.ResourceLocation; + +public class ModSchedule { + + static { + registerInstruction("travel_section", TravelSectionInstruction::new); + registerInstruction("reset_timings", ResetTimingsInstruction::new); + + registerCondition("dynamic_delay", DynamicDelayCondition::new); + } + + private static void registerInstruction(String name, Supplier factory) { + Schedule.INSTRUCTION_TYPES.add(Pair.of(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, name), factory)); + } + + private static void registerCondition(String name, Supplier factory) { + Schedule.CONDITION_TYPES.add(Pair.of(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, name), factory)); + } + + public static void init() {} +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java b/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java new file mode 100644 index 00000000..e9f23df3 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.registry; + +import java.util.Optional; + +import com.simibubi.create.content.trains.entity.Train; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainStatus; +import de.mrjulsen.crn.data.train.TrainStatus.Registry; +import de.mrjulsen.crn.data.train.TrainStatus.TrainStatusCategory; +import de.mrjulsen.crn.data.train.TrainStatus.TrainStatusType; +import de.mrjulsen.crn.mixin.TrainStatusAccessor; + +public final class ModTrainStatusInfos { + + public static final Registry REGISTRY = Registry.create(CreateRailwaysNavigator.MOD_ID); + + // Custom + public static final TrainStatus RED_SIGNAL = REGISTRY.register("red_signal", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.red_signal"), (data) -> { + return data.isCurrentSectionDelayed() && + data.getTrain().navigation.waitingForSignal != null && + data.getTrain().navigation.waitingForSignal.getSecond() && + data.occupyingTrains.isEmpty() + ; + })); + + public static final TrainStatus PRIORITY_OTHER_TRAIN = REGISTRY.register("priority_other_train", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.priority_other_train"), (data) -> { + if (!data.isCurrentSectionDelayed() || data.getTrain().navigation.waitingForSignal == null) { + return false; + } + + Optional occupyingTrain = data.occupyingTrains.stream().findFirst(); + if (!occupyingTrain.isPresent()) { + return false; + } + + return TrainListener.data.containsKey(occupyingTrain.get().id) ? !TrainListener.data.get(occupyingTrain.get().id).isDelayed() : false; + })); + + public static final TrainStatus PERVIOUS_TRAIN_DELAYED = REGISTRY.register("previous_train_delayed", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.delay_other_train"), (data) -> { + if (!data.isCurrentSectionDelayed() || data.getTrain().navigation.waitingForSignal == null) { + return false; + } + + Optional occupyingTrain = data.occupyingTrains.stream().findFirst(); + if (!occupyingTrain.isPresent()) { + return false; + } + + return TrainListener.data.containsKey(occupyingTrain.get().id) ? TrainListener.data.get(occupyingTrain.get().id).isDelayed() : false; + })); + + public static final TrainStatus TRACK_CLOSED = REGISTRY.register("track_closed", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.track_closed"), (data) -> { + return data.isCurrentSectionDelayed() && ((TrainStatusAccessor)data.getTrain().status).crn$track(); + })); + + public static final TrainStatus STAFF_SHORTAGE = REGISTRY.register("staff_shortage", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.staff_shortage"), (data) -> { + return data.isCurrentSectionDelayed() && ((TrainStatusAccessor)data.getTrain().status).crn$conductor(); + })); + + public static final TrainStatus OPERATIONAL_DISRUPTION = REGISTRY.register("operational_disruption", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.operational_disruption"), (data) -> { + return data.isCurrentSectionDelayed() && ((TrainStatusAccessor)data.getTrain().status).crn$navigation(); + })); + + public static final TrainStatus SPECIAL_JOURNEY = REGISTRY.register("special_journey", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.special_trip"), (data) -> { + return false; + })); + + public static final TrainStatus OUT_OF_SERVICE = REGISTRY.register("out_of_service", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service"), (data) -> { + return data.isCurrentSectionDelayed() && data.getTrain().runtime.paused; + })); + + public static void init() {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/registry/data/NextConnectionsRequestData.java b/common/src/main/java/de/mrjulsen/crn/registry/data/NextConnectionsRequestData.java new file mode 100644 index 00000000..0ef29c3f --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/registry/data/NextConnectionsRequestData.java @@ -0,0 +1,24 @@ +package de.mrjulsen.crn.registry.data; + +import java.util.UUID; + +import net.minecraft.nbt.CompoundTag; + +public record NextConnectionsRequestData(String stationName, UUID selfTrainId) { + public static final String NBT_STATION_NAME = "StationName"; + public static final String NBT_TRAIN_ID = "TrainId"; + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putString(NBT_STATION_NAME, stationName()); + nbt.putUUID(NBT_TRAIN_ID, selfTrainId()); + return nbt; + } + + public static NextConnectionsRequestData fromNbt(CompoundTag nbt) { + return new NextConnectionsRequestData( + nbt.getString(NBT_STATION_NAME), + nbt.getUUID(NBT_TRAIN_ID) + ); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/util/BERUtils.java b/common/src/main/java/de/mrjulsen/crn/util/BERUtils.java deleted file mode 100644 index ad0725a1..00000000 --- a/common/src/main/java/de/mrjulsen/crn/util/BERUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.mrjulsen.crn.util; - -import com.mojang.blaze3d.platform.GlStateManager; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; - -import de.mrjulsen.crn.client.ModGuiUtils; -import de.mrjulsen.mcdragonlib.util.ColorUtils; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.texture.OverlayTexture; -import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockState; - -public class BERUtils { - - public void initRenderEngine() { - RenderSystem.enableBlend(); - RenderSystem.enableDepthTest(); - RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); - } - - public void setTint(int r, int g, int b, int a) { - RenderSystem.setShaderColor(r, g, b, a); - } - - public void renderTexture(ResourceLocation texture, MultiBufferSource bufferSource, BlockEntity blockEntity, PoseStack poseStack, float x, float y, float z, float w, float h, float u0, float v0, float u1, float v1, Direction facing, int tint, int light) { - VertexConsumer vertexconsumer = bufferSource.getBuffer(RenderType.text(texture)); - short[] color = ColorUtils.decodeARGB(tint); - addQuadSide(blockEntity, blockEntity.getBlockState(), facing, vertexconsumer, poseStack, - x, y, z, - x + w, y + h, z, - u0, v0, - u1, v1, - (float)color[1] / 255.0F, (float)color[2] / 255.0F, (float)color[3] / 255.0F, (float)color[0] / 255.0F, - light - ); - } - - public static void addVert(VertexConsumer builder, PoseStack pPoseStack, float x, float y, float z, float u, float v, float r, float g, float b, float a, int lu, int lv) { - builder.vertex(pPoseStack.last().pose(), x, y, z).color(r, g, b, a).uv(u, v).uv2(lu, lv).overlayCoords(OverlayTexture.NO_OVERLAY).normal(pPoseStack.last().normal(), 0, 0, 1).endVertex(); - } - - private static void renderWithoutAO(VertexConsumer builder, PoseStack pPoseStack, float x0, float y0, float z0, float x1, float y1, float z1, float u0, float v0, float u1, float v1, float r, float g, float b, float a, int packedLight) { - addVert(builder, pPoseStack, x0, y0, z0, u0, v0, r, g, b, a, packedLight & 0xFFFF, (packedLight >> 16) & 0xFFFF); - addVert(builder, pPoseStack, x0, y1, z0, u0, v1, r, g, b, a, packedLight & 0xFFFF, (packedLight >> 16) & 0xFFFF); - addVert(builder, pPoseStack, x1, y1, z1, u1, v1, r, g, b, a, packedLight & 0xFFFF, (packedLight >> 16) & 0xFFFF); - addVert(builder, pPoseStack, x1, y0, z1, u1, v0, r, g, b, a, packedLight & 0xFFFF, (packedLight >> 16) & 0xFFFF); - } - - @SuppressWarnings("resources") - public static void addQuadSide(BlockEntity be, BlockState state, Direction direction, VertexConsumer builder, PoseStack pPoseStack, float x0, float y0, float z0, float x1, float y1, float z1, float u0, float v0, float u1, float v1, float r, float g, float b, float a, int packedLight) { - renderWithoutAO(builder, pPoseStack, x0, y0, z0, x1, y1, z1, u0, v0, u1, v1, r, g, b, a, packedLight); - } - - public void fillColor(MultiBufferSource pBufferSource, BlockEntity blockEntity, int color, PoseStack poseStack, float x, float y, float z, float w, float h, Direction facing, int light) { - renderTexture(ModGuiUtils.getBlankTexture(), pBufferSource, blockEntity, poseStack, x, y, z, w, h, 0, 0, 1, 1, facing, color, light); - } - - - - -} diff --git a/common/src/main/java/de/mrjulsen/crn/util/DLListUtils.java b/common/src/main/java/de/mrjulsen/crn/util/DLListUtils.java new file mode 100644 index 00000000..3285a590 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/util/DLListUtils.java @@ -0,0 +1,48 @@ +package de.mrjulsen.crn.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; + +public final class DLListUtils { + public static void iterateLooped(List list, int startIndex, BiConsumer action) { + for (int i = 0; i < list.size(); i++) { + int j = (i + startIndex) % list.size(); + action.accept(j, list.get(j)); + } + } + + public static List getNextN(List list, int startIndex, int count) { + if (count > list.size()) { + throw new IndexOutOfBoundsException("The number of elements to be obtained is greater than the list."); + } + List elements = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int j = (i + startIndex) % list.size(); + elements.add(list.get(j)); + } + return elements; + } + + public static Optional getNext(List list, int startIndex, BiPredicate predicate) { + for (int i = 0; i < list.size(); i++) { + int j = (i + startIndex) % list.size(); + if (predicate.test(j, list.get(j))) { + return Optional.of(list.get(j)); + } + } + return Optional.empty(); + } + + public static Optional getPrevious(List list, int startIndex, BiPredicate predicate) { + for (int i = 0; i < list.size(); i++) { + int j = (i + startIndex) % list.size(); + if (predicate.test(j, list.get(j))) { + return Optional.of(list.get(j)); + } + } + return Optional.empty(); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/util/IListenable.java b/common/src/main/java/de/mrjulsen/crn/util/IListenable.java new file mode 100644 index 00000000..09ca49b2 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/util/IListenable.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.util; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.Consumer; + +public interface IListenable { + + Map>> getListeners(); + + default void createEvent(String name) { + if (getListeners().containsKey(name)) { + throw new IllegalArgumentException("An event with this ID has already been created: " + name); + } + getListeners().put(name, new IdentityHashMap<>()); + } + + default void deleteEvent(String name) { + if (getListeners().containsKey(name)) { + getListeners().remove(name); + } + } + + default void clearEvents() { + getListeners().clear(); + } + + default int eventsCount() { + return getListeners().size(); + } + + default int listenersCount(String name) { + if (!hasEvent(name)) { + throw new IllegalArgumentException("This listener event does not exist: " + name); + } + + return getListeners().get(name).size(); + } + + default boolean hasEvent(String name) { + return getListeners().containsKey(name); + } + + default void listen(String name, Object listenerObject, Consumer listener) { + if (!hasEvent(name)) { + throw new IllegalArgumentException("This listener event does not exist: " + name); + } + + getListeners().get(name).put(listenerObject, listener); + } + + default void stopListening(String name, Object listenerObject) { + if (!hasEvent(name)) { + throw new IllegalArgumentException("This listener event does not exist: " + name); + } + + getListeners().get(name).remove(listenerObject); + } + + default void stopListeningAll(Object listenerObject) { + getListeners().values().stream().forEach(x -> { + if (x.containsKey(listenerObject)) { + x.remove(listenerObject); + } + }); + } + + default void notifyListeners(String name, T data) { + if (!hasEvent(name)) { + throw new IllegalArgumentException("This listener event does not exist: " + name); + } + + new ArrayList<>(getListeners().get(name).values()).stream().forEach(x -> x.accept(data)); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/util/LockedList.java b/common/src/main/java/de/mrjulsen/crn/util/LockedList.java new file mode 100644 index 00000000..65b19771 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/util/LockedList.java @@ -0,0 +1,73 @@ +package de.mrjulsen.crn.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Stream; + +public class LockedList extends ArrayList { + + private boolean locked; + + private void lock() { + while (locked) {} + locked = true; + } + + private void unlock() { + locked = false; + } + + @Override + public boolean add(T e) { + lock(); + boolean b = super.add(e); + unlock(); + return b; + } + + @Override + public void add(int index, T element) { + lock(); + super.add(index, element); + unlock(); + } + + @Override + public boolean remove(Object o) { + lock(); + boolean b = super.remove(o); + unlock(); + return b; + } + + @Override + public void clear() { + lock(); + super.clear(); + unlock(); + } + + @Override + public T get(int index) { + lock(); + T t = super.get(index); + unlock(); + return t; + } + + @Override + public boolean retainAll(Collection c) { + lock(); + boolean b = super.retainAll(c); + unlock(); + return b; + } + + @Override + public Stream stream() { + lock(); + Stream s = super.stream(); + unlock(); + return s; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java b/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java index 69bcbbcb..84673a5d 100644 --- a/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java +++ b/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java @@ -1,17 +1,28 @@ package de.mrjulsen.crn.util; -import java.util.Map; -import java.util.Set; - +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.function.Predicate; import com.simibubi.create.foundation.utility.Components; import com.simibubi.create.foundation.utility.Lang; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.web.WebsitePreparableReloadListener; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import dev.architectury.platform.Platform; +import dev.architectury.utils.Env; import net.minecraft.network.chat.MutableComponent; import net.minecraft.util.Mth; public class ModUtils { + + private static WebsitePreparableReloadListener websitemanager; + public static float clockHandDegrees(long time, int divisor) { return 360.0F / divisor * (time % divisor); } @@ -22,14 +33,38 @@ public static double calcSpeed(double metersPerTick, ESpeedUnit unit) { public static MutableComponent calcSpeedString(double metersPerTick, ESpeedUnit unit) { return TextUtils.text((int) Math.abs(Math.round(calcSpeed(metersPerTick, unit))) + " " + unit.getUnit()); + } + + public static int calculateMedian(Queue history, int smoothingThreshold, Predicate filter) { + if (history.isEmpty()) { + return 0; + } + + List values = new LinkedList<>(); + for (int i : history) { + if (!filter.test(i)) + continue; + + values.add(i); + } + + Collections.sort(values); + int median = 0; + if (values.size() % 2 == 0) { + median = (int)(((double)values.get(values.size() / 2) + (double)values.get(values.size() / 2 + 1)) / 2D); + } + median = values.get(values.size() / 2); + + final int med = median; + return (int)history.stream().mapToInt(x -> x).filter(x -> Math.abs(med - x) <= smoothingThreshold).average().orElse(0); } - public static String timeRemainingString(int ticks) { + public static String timeRemainingString(long ticks) { StringBuilder sb = new StringBuilder(); final String unpredictable = " ~ "; final String whitespace = " "; - if (ticks == -1 || ticks >= 12000 - 15 * 20) { + if (ticks == -1 || ticks >= 120000 - 15 * 20) { sb.append(whitespace); sb.append(unpredictable); @@ -37,8 +72,8 @@ public static String timeRemainingString(int ticks) { sb.append(Lang.translateDirect("display_source.station_summary.now").getString()); } else { - int min = ticks / 1200; - int sec = (ticks / 20) % 60; + long min = ticks / 1200; + long sec = (ticks / 20) % 60; sec = Mth.ceil(sec / 15f) * 15; if (sec == 60) { min++; @@ -51,17 +86,30 @@ public static String timeRemainingString(int ticks) { return sb.toString(); } - public static boolean areEqual(Set> set1, Set> set2) { - if (set1.size() != set2.size()) { - return false; - } + public static long generateId(Predicate exists) { + long id; + do { + id = DragonLib.RANDOM.nextLong(); + } while (exists.test(id)); + return id; + } - for (Map.Entry entry : set1) { - if(!set2.contains(entry)) { - return false; - } - } + public static void setWebsiteResourceManager(WebsitePreparableReloadListener manager) { + websitemanager = manager; + } - return true; + public static WebsitePreparableReloadListener getWebsiteResourceManager() { + return websitemanager; + } + + /** Client-side only! */ + public static String formatTime(long time, boolean asETA) throws RuntimeSideException { + if (Platform.getEnvironment() != Env.CLIENT) { + throw new RuntimeSideException(true); + } + if (asETA) { + return timeRemainingString(time - DragonLib.getCurrentWorldTime()); + } + return TimeUtils.parseTime((time + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY, ModClientConfig.TIME_FORMAT.get()); } } diff --git a/common/src/main/java/de/mrjulsen/crn/util/TrainUtils.java b/common/src/main/java/de/mrjulsen/crn/util/TrainUtils.java deleted file mode 100644 index a984b399..00000000 --- a/common/src/main/java/de/mrjulsen/crn/util/TrainUtils.java +++ /dev/null @@ -1,297 +0,0 @@ -package de.mrjulsen.crn.util; - -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import com.simibubi.create.Create; -import com.simibubi.create.content.decoration.slidingDoor.DoorControlBehaviour; -import com.simibubi.create.content.trains.GlobalRailwayManager; -import com.simibubi.create.content.trains.display.GlobalTrainDisplayData; -import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; -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.signal.TrackEdgePoint; -import com.simibubi.create.content.trains.station.GlobalStation; -import com.simibubi.create.content.trains.station.StationBlockEntity; -import de.mrjulsen.crn.data.DeparturePrediction; -import de.mrjulsen.crn.data.GlobalSettingsManager; -import de.mrjulsen.crn.data.NearestTrackStationResult; -import de.mrjulsen.crn.data.SimpleTrainConnection; -import de.mrjulsen.crn.data.SimpleTrainSchedule; -import de.mrjulsen.crn.data.SimulatedTrainSchedule; -import de.mrjulsen.crn.data.TrainStationAlias; -import de.mrjulsen.crn.data.TrainStop; -import de.mrjulsen.crn.data.DeparturePrediction.TrainExitSide; -import de.mrjulsen.mcdragonlib.util.MathUtils; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.Vec3i; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.Vec3; - -public class TrainUtils { - - public static final GlobalRailwayManager RAILWAY_MANAGER = Create.RAILWAYS; - - /** - * Get data about all trains and when they arrive where. - * @return a Map where the key is the station name and the value is a list of data from all trains that will arrive at this stations. - */ - public static Map> Gott() { - return new HashMap<>(GlobalTrainDisplayData.statusByDestination); - } - - public static Map> getMappedDeparturePredictions() { - Map> statusData = Gott(); - Map> map = new HashMap<>(); - GlobalSettingsManager.getInstance().getSettingsData().getAliasList().forEach(alias -> { - map.put(alias, statusData.entrySet().stream().filter(x -> alias.contains(x.getKey())).flatMap(x -> x.getValue().stream()).map(x -> new DeparturePrediction(x)).toList()); - }); - return map; - } - - public static void getMappedDeparturePredictions(Map> globalPredictions, Map> trainPredictions) { - Map> statusData = Gott(); - statusData.entrySet().forEach((e) -> { - String key = e.getKey(); - Collection value = e.getValue(); - TrainStationAlias alias = GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(key); - if (!globalPredictions.containsKey(alias.getAliasName().get())) { - globalPredictions.put(alias.getAliasName().get(), new ArrayList<>()); - } - globalPredictions.get(alias.getAliasName().get()).addAll(value.stream().map(x -> new DeparturePrediction(x)).toList()); - value.forEach(x -> { - if (!trainPredictions.containsKey(x.train.id)) { - trainPredictions.put(x.train.id, new ArrayList<>()); - } - trainPredictions.get(x.train.id).add(new DeparturePrediction(x)); - }); - }); - } - - public static Collection getTrainDeparturePredictions(UUID trainId, Level level) { - final Map> edges = level != null ? getAllEdgesMapped() : new HashMap<>(); - final Map> stationsByName = level != null ? getAllStations().stream().collect(Collectors.groupingBy(x -> x.name, Collectors.toSet())) : new HashMap<>(); - - Collection preds = Gott().values().stream().flatMap(x -> x.stream()).filter(x -> x.train.id.equals(trainId)).map(x -> { - DeparturePrediction prediction = new DeparturePrediction(x); - if (stationsByName.containsKey(prediction.getStationName())) { - Set exitSides = stationsByName.get(prediction.getStationName()).stream().filter(a -> edges.containsKey(a.id)).map(a -> getTrainStationExit(a, Direction.fromYRot(angleOn(a, edges.get(a.id).stream().findFirst().get())), level)).collect(Collectors.toSet()); - if (exitSides.size() == 1) { - prediction.setExit(exitSides.stream().findFirst().get()); - } - } - return prediction; - }).toList(); - return preds; - } - - public static Collection getTrainStopsSorted(UUID trainId, Level level) { - return getTrainDeparturePredictions(trainId, level).stream().map(x -> new TrainStop(x.getNextStop(), x)).filter(x -> !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(x.getPrediction().getStationName())).sorted(Comparator.comparingInt(x -> x.getPrediction().getTicks())).toList(); - } - - public static List getConnectionsAt(String stationName, UUID currentTrainId, int ticksToNextStop) { - TrainStationAlias alias = GlobalSettingsManager.getInstance().getSettingsData().getAliasFor(stationName); - SimpleTrainSchedule ownSchedule = SimpleTrainSchedule.of(getTrainStopsSorted(currentTrainId, null)); - GlobalTrainDisplayData.refresh(); - - List excludedSchedules = new ArrayList<>(); - Map scheduleByPrediction = new HashMap<>(); - Map simulatedScheduleByPrediction = new HashMap<>(); - - return Gott().entrySet().stream().filter(x -> alias.contains(x.getKey())).map(x -> x.getValue()) - .flatMap(x -> x.parallelStream().map(y -> new DeparturePrediction(y))) - .peek(x -> { - SimpleTrainSchedule schedule = SimpleTrainSchedule.of(getTrainStopsSorted(x.getTrain().id, null)); - scheduleByPrediction.put(x, schedule); - simulatedScheduleByPrediction.put(x, schedule.simulate(x.getTrain(), ticksToNextStop, alias)); - }) - .sorted(Comparator.comparingInt(x -> simulatedScheduleByPrediction.get(x).getSimulationData().simulationCorrection())) - .filter(x -> { - SimpleTrainSchedule schedule = scheduleByPrediction.get(x); - SimulatedTrainSchedule directionalSchedule = simulatedScheduleByPrediction.get(x); - - if (excludedSchedules.stream().anyMatch(y -> y.exactEquals(directionalSchedule))) { - return false; - } - - boolean b = !x.getTrain().id.equals(currentTrainId) && - TrainUtils.isTrainValid(x.getTrain()) && - !GlobalSettingsManager.getInstance().getSettingsData().isTrainBlacklisted(x.getTrain()) && - !schedule.equals(ownSchedule); - - if (b) { - excludedSchedules.add(directionalSchedule); - } - - return b; - }).map(x -> { - SimulatedTrainSchedule sched = simulatedScheduleByPrediction.get(x); - Optional firstStop = sched.getFirstStopOf(x.getNextStop()); - return new SimpleTrainConnection( - x.getTrain().name.getString(), - x.getTrain().id, - x.getTrain().icon.getId(), - sched.getSimulationData().simulationTime() + sched.getSimulationData().simulationCorrection(), - firstStop.isPresent() ? firstStop.get().getPrediction().getScheduleTitle() : x.getScheduleTitle(), - firstStop.isPresent() ? firstStop.get().getStationAlias().getInfoForStation(firstStop.get().getPrediction().getStationName()) : x.getInfo() - ); - }).sorted(Comparator.comparingInt(x -> x.ticks())).toList(); - - } - - public static boolean GottKnows(String station) { - return Gott().keySet().stream().anyMatch(x -> { - String regex = x.isBlank() ? x : "\\Q" + x.replace("*", "\\E.*\\Q") + "\\E"; - return station.matches(regex); - }); - } - - /** - * A list of all stations in the world. - * @return a list containing all track stations. - */ - public static Collection getAllStations() { - final Collection stations = new ArrayList<>(); - RAILWAY_MANAGER.trackNetworks.forEach((uuid, graph) -> { - Collection foundStations = graph.getPoints(EdgePointType.STATION); - stations.addAll(foundStations); - }); - return stations; - } - - public static Collection getAllEdges() { - final Set edges = new HashSet<>(); - RAILWAY_MANAGER.trackNetworks.forEach((uuid, graph) -> { - edges.addAll(graph.getNodes().stream().map(x -> graph.locateNode(x)).flatMap(x -> graph.getConnectionsFrom(x).values().stream()).collect(Collectors.toSet())); - }); - return edges; - } - - public static Map> getAllEdgesMapped() { - final Map> edges = new HashMap<>(); - RAILWAY_MANAGER.trackNetworks.forEach((uuid, graph) -> { - edges.putAll(graph.getNodes().stream() - .map(x -> graph.locateNode(x)) - .flatMap(x -> graph.getConnectionsFrom(x).values().stream()) - .distinct() - .flatMap(edge -> edge.getEdgeData().getPoints().stream().map(point -> new AbstractMap.SimpleEntry<>(point.id, edge))) - .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toSet()))) - ); - }); - return edges; - } - - public static Optional getEdgeForStation(GlobalStation station) { - return getAllEdges().stream().filter(x -> x.getEdgeData().getPoints().stream().anyMatch(y -> y.equals(station))).findFirst(); - } - - public static NearestTrackStationResult getNearestTrackStation(Level level, Vec3i pos) { - Optional station = getAllStations().stream().filter(x -> - GottKnows(x.name) && - x.getBlockEntityDimension().equals(level.dimension()) && - !GlobalSettingsManager.getInstance().getSettingsData().isBlacklisted(x.name) - ).min((a, b) -> Double.compare(a.getBlockEntityPos().distSqr(pos), b.getBlockEntityPos().distSqr(pos))); - - double distance = station.isPresent() ? station.get().getBlockEntityPos().distSqr(pos) : 0; - return new NearestTrackStationResult(station, distance); - } - - public static double getStationAngle(GlobalStation station) { - return angleOn(station, getEdgeForStation(station).get()); - } - - public static Direction getStationDirection(GlobalStation station) { - return Direction.fromYRot(getStationAngle(station)); - } - - public static double angleOn(TrackEdgePoint point, TrackEdge edge) { - double basePos = point.isPrimary(edge.node1) ? edge.getLength() - point.position : point.position; - Vec3 vec = edge.getDirectionAt(basePos); - return point.isPrimary(edge.node1) ? MathUtils.getVectorAngle(vec) : MathUtils.getVectorAngle(vec.reverse()); - } - - public static DoorControlBehaviour getTrainStationDoorControl(GlobalStation station, Level level) { - BlockPos stationPos = station.getBlockEntityPos(); - if (level == null || !level.isLoaded(stationPos)) { - return null; - } - if (level.getBlockEntity(stationPos) instanceof StationBlockEntity be) { - return be.doorControls; - } - return null; - } - - /** - * - * @param station - * @param server - * @return 1 = right, -1 = left, 0 = unknown - */ - public static TrainExitSide getTrainStationExitDirection(GlobalStation station, Level level) { - DoorControlBehaviour dcb = getTrainStationDoorControl(station, level); - if (dcb == null) { - return TrainExitSide.UNKNOWN; - } - Direction stationDirection = getStationDirection(station); - - if (dcb.mode.matches(stationDirection.getClockWise())) { - return TrainExitSide.RIGHT; - } else if (dcb.mode.matches(stationDirection.getCounterClockWise())) { - return TrainExitSide.LEFT; - } - return TrainExitSide.UNKNOWN; - } - - public static TrainExitSide getTrainStationExit(GlobalStation station, Direction stationDirection, Level level) { - DoorControlBehaviour dcb = getTrainStationDoorControl(station, level); - if (dcb == null) { - return TrainExitSide.UNKNOWN; - } - - if (dcb.mode.matches(stationDirection.getClockWise())) { - return TrainExitSide.RIGHT; - } else if (dcb.mode.matches(stationDirection.getCounterClockWise())) { - return TrainExitSide.LEFT; - } - return TrainExitSide.UNKNOWN; - } - - - - /** - * A list of all trains in the world. - * @return a list containing all trains. - */ - public static Collection getAllTrains() { - return RAILWAY_MANAGER.trains.values(); - } - - public static Train getTrain(UUID trainId) { - return RAILWAY_MANAGER.trains.get(trainId); - } - - public static boolean isTrainValid(Train train) { - return !train.derailed && - !train.invalid && - !train.runtime.paused && - train.runtime.getSchedule() != null && - train.graph != null - ; - } - - public static boolean isTrainIdValid(UUID trainId) { - return isTrainValid(getTrain(trainId)); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java b/common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java new file mode 100644 index 00000000..82812505 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java @@ -0,0 +1,274 @@ +package de.mrjulsen.crn.web; + +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.event.ModCommonEvents; +import de.mrjulsen.crn.data.navigation.NavigatableGraph; +import de.mrjulsen.crn.data.navigation.Route; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsServer; +import com.google.common.net.MediaType; +import com.sun.net.httpserver.HttpExchange; + +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; + +public class SimpleWebServer { + + static HttpServer server; + + public static void start() throws Exception { + /* + server = HttpServer.create(new InetSocketAddress(80), 0); + server.createContext("/hello", new HelloWorldHandler()); + server.createContext("/api/" + CreateRailwaysNavigator.MOD_ID + "/navigate", new V1NavigationHandler()); + server.createContext("/" + CreateRailwaysNavigator.MOD_ID, new WebsiteHandler(CreateRailwaysNavigator.MOD_ID)); + server.createContext("/" + CreateRailwaysNavigator.SHORT_MOD_ID, new WebsiteHandler(CreateRailwaysNavigator.SHORT_MOD_ID)); + server.createContext("/status", new TestHandler()); + server.setExecutor(null); // creates a default executor + server.start(); + */ + } + + public static void stop() { + DLUtils.doIfNotNull(server, x -> { + x.stop(0); + }); + } + + public static Map> parseQueryParameters(HttpExchange ex, Charset charset) { + String queryString = ex.getRequestURI().getRawQuery(); + if (queryString == null || queryString.isEmpty()) { + return Collections.emptyMap(); + } + Map> parsedParams = new TreeMap>(); + for (String param : queryString.split("&")) { + String[] parts = param.split("=", 2); + String key = parts[0]; + String value = parts.length == 2 ? parts[1] : ""; + try { + key = URLDecoder.decode(key, charset.name()); + value = URLDecoder.decode(value, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + List values = parsedParams.get(key); + if (values == null) { + values = new LinkedList(); + parsedParams.put(key, values); + } + values.add(value); + } + + for (Map.Entry> me : parsedParams.entrySet()) { + me.setValue(Collections.unmodifiableList(me.getValue())); + } + return Collections.unmodifiableMap(parsedParams); + } + + private static void startResponse(HttpExchange ex, int code, MediaType contentType, boolean hasBody) throws IOException { + if (contentType != null) { + ex.getResponseHeaders().set("Content-Type", contentType.type()); + } + if (!hasBody) { // No body. Required for HEAD requests + ex.sendResponseHeaders(code, -1); + } else { // Chuncked encoding + ex.sendResponseHeaders(code, 0); + } + } + + private static void sendError(HttpExchange ex, int code, String msg) { + CreateRailwaysNavigator.LOGGER.warn(msg); + try { + respond(ex, code, MediaType.PLAIN_TEXT_UTF_8, msg.getBytes()); + } catch (IOException e) { + CreateRailwaysNavigator.LOGGER.error("Unable to send error response.", e); + } + } + + private static void respond(HttpExchange ex, int code, MediaType contentType, byte response[]) throws IOException { + startResponse(ex, code, contentType, response != null); + if (response != null) { + OutputStream responseBody = ex.getResponseBody(); + responseBody.write(response); + responseBody.flush(); + responseBody.close(); + } + ex.close(); + } + + public static void sendRedirect(HttpExchange ex, URI location) throws IOException { + ex.getResponseHeaders().set("Location", location.toString()); + respond(ex, HttpURLConnection.HTTP_SEE_OTHER, null, null); + } + + public static URI getRequestUri(HttpExchange ex) { + String host = ex.getRequestHeaders().getFirst("Host"); + if (host == null) { // Client must be using HTTP/1.0 + CreateRailwaysNavigator.LOGGER.warn("Request did not provide Host header, using 'localhost' as hostname"); + int port = ex.getHttpContext().getServer().getAddress().getPort(); + host = "localhost:" + port; + } + String protocol = (ex.getHttpContext().getServer() instanceof HttpsServer) ? "https" : "http"; + URI base; + try { + base = new URI(protocol, host, "/", null, null); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + URI requestedUri = ex.getRequestURI(); + requestedUri = base.resolve(requestedUri); + return requestedUri; + } + + public static void redirectTo(HttpExchange ex, String redirect) { + URI base = getRequestUri(ex); + URI path; + try { + path = new URI(redirect); + sendRedirect(ex, base.resolve(path)); + } catch (URISyntaxException | IOException e) { + CreateRailwaysNavigator.LOGGER.error("Could not construct URI.", e); + } + } + + + + static class HelloWorldHandler implements HttpHandler { + + @Override + public void handle(HttpExchange t) throws IOException { + String response = "Hello World! " + CreateRailwaysNavigator.MOD_ID; + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + + static class WebsiteHandler implements HttpHandler { + + private final String subUrl; + private final int subUrlLength; + + public WebsiteHandler(String subUrl) { + this.subUrl = subUrl; + this.subUrlLength = subUrl.length() + 1; + } + + @Override + public void handle(HttpExchange t) throws IOException { + + String requestedPath = t.getRequestURI().getPath(); + if (requestedPath.startsWith("/" + subUrl) && requestedPath.length() >= subUrlLength) { + requestedPath = requestedPath.substring(subUrlLength); + } else { + sendError(t, HttpURLConnection.HTTP_BAD_REQUEST, "The requested URL is invalid: " + requestedPath); + } + + if (requestedPath.isBlank()) { + redirectTo(t, "/" + subUrl + "/"); + return; + } + + if (requestedPath.equals("/")) { + requestedPath = "/index.html"; // Set default page to index.html + } + + CreateRailwaysNavigator.LOGGER.info("A web service requested a resource: " + requestedPath); + Optional fileData = ModUtils.getWebsiteResourceManager().getFileBytesFor(requestedPath); + + if (!fileData.isPresent()) { + sendError(t, HttpURLConnection.HTTP_NOT_FOUND, "The requested resource does not exist: " + requestedPath); + /* + String response = "404 (Not Found)\n" + requestedPath + " does not exist."; + t.sendResponseHeaders(404, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + */ + } else { + respond(t, HttpURLConnection.HTTP_OK, null, fileData.get()); + /* + byte[] fileBytes = fileData.get(); + System.out.println("BYTES: " + fileBytes.length); + t.sendResponseHeaders(200, 0); + OutputStream os = t.getResponseBody(); + os.write(fileBytes); + os.close(); + */ + } + } + } + + static class TestHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = "PACKS"; + for (String str : ModCommonEvents.getCurrentServer().get().getPackRepository().getAvailableIds()) { + response += "\n - " + str; + } + response += "\nCRN PACK"; + for (ResourceLocation str : ModCommonEvents.getCurrentServer().get().getPackRepository().getPack("mod:" + CreateRailwaysNavigator.MOD_ID).open().getResources(PackType.SERVER_DATA, CreateRailwaysNavigator.MOD_ID, "", (str) -> true)) { + response += "\n - " + str; + } + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + + static class V1NavigationHandler implements HttpHandler { + + private static final String KEY_START = "start"; + private static final String KEY_DESTINATION = "destination"; + + @Override + public void handle(HttpExchange t) throws IOException { + Map> params = parseQueryParameters(t, StandardCharsets.UTF_8); + + if (!params.containsKey(KEY_START) || !params.containsKey(KEY_DESTINATION)) { + sendError(t, HttpURLConnection.HTTP_BAD_REQUEST, "Wrong parameters."); + } + + try { + + GlobalSettings settings = GlobalSettings.getInstance(); + List routes = NavigatableGraph.searchRoutes(settings.getOrCreateStationTagFor(params.get(KEY_START).get(0)), settings.getOrCreateStationTagFor(params.get(KEY_DESTINATION).get(0)), null, true); + String response = DragonLib.GSON.toJson(routes); + + t.sendResponseHeaders(200, 0); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + + } catch (Exception e) { + CreateRailwaysNavigator.LOGGER.error("DEAD", e); + } + + + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java b/common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java new file mode 100644 index 00000000..cd2b0086 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java @@ -0,0 +1,57 @@ +package de.mrjulsen.crn.web; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; + +public class WebsitePreparableReloadListener extends SimplePreparableReloadListener>> { + + public static final String RESOURCE_PATH = "crn_website"; + + private Map> data; + + @Override + protected void apply(Map> object, ResourceManager resourceManager, ProfilerFiller profiler) { + data = object; + } + + @Override + protected Map> prepare(ResourceManager resourceManager, ProfilerFiller profiler) { + + Map> map = new HashMap<>(); + Collection locations = resourceManager.listResources(RESOURCE_PATH, x -> true).keySet(); + + for (ResourceLocation loc : locations) { + final ResourceLocation localLoc = loc; + String path = localLoc.getPath().replace(RESOURCE_PATH, ""); + map.put(path, () -> { + try { + Resource resource = resourceManager.getResourceOrThrow(localLoc); + try (InputStream inputstream = resource.open()) { + return inputstream.readAllBytes(); + } + } catch (IOException e) { + e.printStackTrace(); + return null; + } + }); + } + + return map; + } + + public Optional getFileBytesFor(String path) { + return Optional.ofNullable(data.containsKey(path) ? data.get(path).get() : null); + } + +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/blockstates/advanced_display_slab.json b/common/src/main/resources/assets/createrailwaysnavigator/blockstates/advanced_display_slab.json new file mode 100644 index 00000000..ca37c364 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/blockstates/advanced_display_slab.json @@ -0,0 +1,101 @@ +{ + "variants": { + "facing=north,side=front,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_neg" + }, + "facing=east,side=front,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_neg", + "y": 90 + }, + "facing=south,side=front,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_neg", + "y": 180 + }, + "facing=west,side=front,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_neg", + "y": 270 + }, + + "facing=north,side=both,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_neg" + }, + "facing=east,side=both,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_neg", + "y": 90 + }, + "facing=south,side=both,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_neg", + "y": 180 + }, + "facing=west,side=both,y_alignment=negative": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_neg", + "y": 270 + }, + + + "facing=north,side=front,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_cen" + }, + "facing=east,side=front,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_cen", + "y": 90 + }, + "facing=south,side=front,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_cen", + "y": 180 + }, + "facing=west,side=front,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_cen", + "y": 270 + }, + + "facing=north,side=both,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_cen" + }, + "facing=east,side=both,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_cen", + "y": 90 + }, + "facing=south,side=both,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_cen", + "y": 180 + }, + "facing=west,side=both,y_alignment=center": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_cen", + "y": 270 + }, + + + "facing=north,side=front,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_pos" + }, + "facing=east,side=front,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_pos", + "y": 90 + }, + "facing=south,side=front,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_pos", + "y": 180 + }, + "facing=west,side=front,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_pos", + "y": 270 + }, + + "facing=north,side=both,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_pos" + }, + "facing=east,side=both,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_pos", + "y": 90 + }, + "facing=south,side=both,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_pos", + "y": 180 + }, + "facing=west,side=both,y_alignment=positive": { + "model": "createrailwaysnavigator:block/advanced_display_slab_double_pos", + "y": 270 + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json index 595ba832..9d9aea12 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json @@ -35,8 +35,7 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", - "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Außa Betrieb", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Betriebsfahrt", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Betriebsfahrt", "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Routenanzeig Einstäiunga", @@ -108,16 +107,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "Sie hom %s eareicht. Mia bedankn uns fia Ihre Reise und Servus.", "gui.createrailwaysnavigator.route_overview.next_connections": "Naxte Oschlüsse", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Umstieg", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Zuag foid aus", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Zuag foid aus", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informadion zua %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Foid aus", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Foid aus", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Oschluss gfährdet", "gui.createrailwaysnavigator.route_overview.connection_missed": "Oschluss vapasst", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Zuag foid aus", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Zuag foid aus", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Ihre Reise noch %s konn ned fortgsetzt wern.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Aufgrund oana Zuagvaspätung hom Sie Ihrn Oschluss vapasst. Suchn Sie noch oana Oidanative im Navigatoa. Mia entschuidign uns fia de Unannehmlichkeitn.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Informadion zua %s: Dea Zuag foid heid aus! Mia entschuidign uns fia de Unannehmlichkeitn. Suchn Sie noch oana Oidanative im Navigatoa.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Zuag foid heid aus", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informadion zua %s: Dea Zuag foid heid aus! Mia entschuidign uns fia de Unannehmlichkeitn. Suchn Sie noch oana Oidanative im Navigatoa.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Zuag foid heid aus", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Ihre Reise noch %s konn ned fortgsetzt wern. Suchn Sie noch oana Oidanative im Navigatoa.", "gui.createrailwaysnavigator.route_overview.options": "Drugge %s fia Optiona.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Ihre Reise noch %s ofangt!", @@ -127,8 +126,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ihr Zuag in %s fahrd heid vo Gleis %s ob.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Okunft %s vaspätet.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s stod %s in %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Zuag foid aus", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s noch %s foid heid aus.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Zuag foid aus", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s noch %s foid heid aus.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Ihr Umstieg städ bevoa", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Steign Sie in %s → %s auf Gleis %s um", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Steign Sie in %s → %s um", @@ -151,16 +150,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Zuag Blacklist", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Schliaße Züg aus, z.B. Güterzüg, Sonderzüg, etc., damit de ned in den Vorschlägn auftaan.", - "gui.createrailwaysnavigator.alias_settings.title": "Bohhof Dogs Einstäiunga", - "gui.createrailwaysnavigator.alias_settings.summary": "Enthält %s Stationa", - "gui.createrailwaysnavigator.alias_settings.editor": "Zualetzt vo %s am %s bearbadet", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Nein Eintrog eastäin", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Dog löschn", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Station entferna", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Station hizufügn", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Bohhofsnama", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Gleisbezeichnung", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Nama eingem", + "gui.createrailwaysnavigator.station_tags.title": "Bohhof Dogs Einstäiunga", + "gui.createrailwaysnavigator.station_tags.summary": "Enthält %s Stationa", + "gui.createrailwaysnavigator.station_tags.editor": "Zualetzt vo %s am %s bearbadet", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Nein Eintrog eastäin", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Dog löschn", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Station entferna", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Station hizufügn", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Bohhofsnama", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Gleisbezeichnung", + "gui.createrailwaysnavigator.station_tags.enter_name": "Nama eingem", "gui.createrailwaysnavigator.train_group_settings.title": "Zuagkategorin Einstäiunga", "gui.createrailwaysnavigator.train_group_settings.summary": "Enthält %s Ziag", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json index 340f81a5..e690d6a3 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json @@ -34,9 +34,12 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Benutze den Block auf _Zügen_ als _Zugzielanzeigen_ oder _Fahrgastinformationsanzeigen_ oder an _Bahnhöfen_ als verbesserte _Bahnsteiganzeige_, die auch mehr Informationen anzeigen als normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "R-Klick mit einem Schraubenschlüssel", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne ein _Menü_, um die Anzeige zu _konfigurieren_.", + "block.createrailwaysnavigator.advanced_display_slab": "Verbesserte Anzeige-Stufe", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Benutze den Block auf _Zügen_ als _Zugzielanzeigen_ oder _Fahrgastinformationsanzeigen_ oder an _Bahnhöfen_ als verbesserte _Bahnsteiganzeige_, die auch mehr Informationen anzeigen als normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "R-Klick mit einem Schraubenschlüssel", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Öffne ein _Menü_, um die Anzeige zu _konfigurieren_.", - "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Außer Betrieb", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Betriebsfahrt", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Betriebsfahrt", "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Routenanzeige Einstellungen", @@ -73,7 +76,9 @@ "gui.createrailwaysnavigator.common.search": "Suchen", "gui.createrailwaysnavigator.common.auto": "Autom.", "gui.createrailwaysnavigator.common.server_error": "Fehler beim Ausführen der Aufgabe. Schaue in die Serverkonsole.", - "gui.createrailwaysnavigator.via": "über", + "gui.createrailwaysnavigator.common.delete": "Löschen", + "gui.createrailwaysnavigator.common.add": "Hinzufügen", + "gui.createrailwaysnavigator.common.help": "Hilfe erhalten", "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "Keine Verbindungen gefunden.", @@ -89,15 +94,15 @@ "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Sucheinstellungen", "gui.createrailwaysnavigator.navigator.search.tooltip": "Suchen", "gui.createrailwaysnavigator.navigator.location.tooltip": "Nächsgelegene Station von aktueller Position", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Aktualisieren", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Eingaben tauschen", "gui.createrailwaysnavigator.route_details.title": "Streckendetails", "gui.createrailwaysnavigator.route_details.departure": "Abfahrt in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Umstieg in", "gui.createrailwaysnavigator.route_details.transfer": "Umstieg", - "gui.createrailwaysnavigator.route_details.save_route": "Route speichern", - "gui.createrailwaysnavigator.route_overview.title": "Streckendetails", + "gui.createrailwaysnavigator.route_overview.title": "Reisebegleitung", "gui.createrailwaysnavigator.route_overview.journey_begins": "Ihre Reise beginnt! %s nach %s, Abfahrt %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Ihre Reise beginnt! %s nach %s, Abfahrt %s von Gleis %s", "gui.createrailwaysnavigator.route_overview.train_details": "%s nach %s", @@ -108,16 +113,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "Sie haben %s erreicht. Wir bedanken uns für Ihre Reise und wünschen einen schönen Tag.", "gui.createrailwaysnavigator.route_overview.next_connections": "Nächste Anschlüsse", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Umstieg", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Zug fällt aus", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Zug fällt aus", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Information zu %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Fällt aus", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Fällt aus", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Anschluss gefährdet", "gui.createrailwaysnavigator.route_overview.connection_missed": "Anschluss verpasst", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Zug fällt aus", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Zug fällt aus", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Ihre Reise nach %s kann nicht fortgesetzt werden.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Aufgrund einer Zugverspätung haben Sie Ihren Anschluss verpasst. Suchen Sie nach einer Alternative im Navigator. Wir entschuldigen uns für die Unannehmlichkeiten.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Information zu %s: Dieser Zug fällt heute aus! Wir entschuldigen uns für die Unannehmlichkeiten. Suchen Sie nach einer Alternative im Navigator.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Zug fällt heute aus", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Information zu %s: Dieser Zug fällt heute aus! Wir entschuldigen uns für die Unannehmlichkeiten. Suchen Sie nach einer Alternative im Navigator.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Zug fällt heute aus", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Ihre Reise nach %s kann nicht fortgesetzt werden. Suchen Sie nach einer Alternative im Navigator.", "gui.createrailwaysnavigator.route_overview.options": "Drücke %s für Optionen.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Ihre Reise nach %s beginnt!", @@ -127,8 +132,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ihr Zug in %s fährt heute von Gleis %s ab.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Ankunft %s verspätet.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s statt %s in %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Zug fällt aus", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s nach %s fällt heute aus.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Zug fällt aus", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s nach %s fällt heute aus.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Ihr Umstieg steht bevor", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Steigen Sie in %s → %s auf Gleis %s um", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Steigen Sie in %s → %s um", @@ -143,7 +148,7 @@ "gui.createrailwaysnavigator.global_settings.title": "Globale Einstellungen", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Zum Bearbeiten klicken", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Bahnhof Tags", - "gui.createrailwaysnavigator.global_settings.option_alias.description": "Erstelle Tags, um mehrere Stationen (z.B. MyStation 1, MyStation 2, ...) als einen Bahnhof (z.B. MyStation) zu behandeln.", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Fasse einzelne Bahnhöfe zu einem einzigen Bahnhof mit mehreren Bahnsteigen und eigenem Namen zusammen, damit der Navigator Umsteigemöglichkeiten etc. vorschlagen kann.", "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Bahnhof Blacklist", "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Schließe Stationen aus, die nicht in den Navigtionsergebnissen erscheinen sollen. Diese Stationen werden bei der Suche ignoriert.", "gui.createrailwaysnavigator.global_settings.train_group.title": "Zugkategorien", @@ -151,16 +156,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Zug Blacklist", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Schließe Züge aus, z.B. Güterzüge, Sonderzüge, etc., damit diese nicht in den Vorschlägen auftauchen.", - "gui.createrailwaysnavigator.alias_settings.title": "Bahnhof Tags Einstellungen", - "gui.createrailwaysnavigator.alias_settings.summary": "Enthält %s Stationen", - "gui.createrailwaysnavigator.alias_settings.editor": "Zuletzt von %s am %s bearbeitet", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Neuen Eintrag erstellen", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Tag löschen", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Station entfernen", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Station hinzufügen", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Bahnhofsname", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Gleisbezeichnung", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Name eingeben", + "gui.createrailwaysnavigator.station_tags.summary": "Enthält %s Stationen", + "gui.createrailwaysnavigator.station_tags.editor": "Zuletzt von %s am %s bearbeitet", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Neuen Eintrag erstellen", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Tag löschen", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Station entfernen", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Zum Ändern klicken", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Station hinzufügen", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Bahnhofsname", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Gleisbezeichnung", + "gui.createrailwaysnavigator.station_tags.enter_name": "Name eingeben", "gui.createrailwaysnavigator.train_group_settings.title": "Zugkategorien Einstellungen", "gui.createrailwaysnavigator.train_group_settings.summary": "Enthält %s Züge", @@ -200,6 +205,7 @@ "gui.createrailwaysnavigator.destination": "Ziel", "gui.createrailwaysnavigator.line": "Linie", "gui.createrailwaysnavigator.following_trains": "Folgezüge:", + "gui.createrailwaysnavigator.via": "über", "gui.createrailwaysnavigator.advanced_display_settings.title": "Verbesserte Anzeige Einstellungen", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Anzeigetyp", @@ -245,6 +251,101 @@ "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "Die Breite der Spalte für den Zugname in Pixel. (Standard: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Gleis Spaltenbreite", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "Die Breite der Spalte für das Gleis in Pixel. (-1 = Autom., Standard: -1)", + + "gui.createrailwaysnavigator.section_settings.title": "Abschnittseinstellungen", + "gui.createrailwaysnavigator.section_settings.train_groups": "Zugkategorie zuweisen", + "gui.createrailwaysnavigator.section_settings.train_lines": "Zuglinie zuweisen", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Start des nächsten Bereichs einbinden", + "gui.createrailwaysnavigator.section_settings.usable": "Navigierbar", + "gui.createrailwaysnavigator.section_settings.none": "(Keine)", + + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Dynamische Wartezeit", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Mindestwartezeit", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Warte: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "oder mindestens %s", + + "createrailwaysnavigator.schedule.instruction.travel_section": "Neuer Fahrplanabschnitt", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Anfang eines neuen Fahrplanabschnitts.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Konfigurieren...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Setze Zugkategorie: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Setze Zuglinie: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Start des nächsten Bereichs einbinden: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Navigierbar: ", + + "createrailwaysnavigator.schedule.instruction.reset_timings": "Zeiten zurücksetzen", + + "display.createrailwaysnavigator.train_destination.simple": "Kompakt", + "display.createrailwaysnavigator.train_destination.extended": "Erweitert", + "display.createrailwaysnavigator.train_destination.detailed": "Detailliert", + "display.createrailwaysnavigator.passenger_information.running_text": "Lauftext", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Detailliert mit Fahrplan", + "display.createrailwaysnavigator.platform.running_text": "Lauftext", + "display.createrailwaysnavigator.platform.table": "Tabelle", + "display.createrailwaysnavigator.platform.focus": "Fokussiert", + + "gui.createrailwaysnavigator.saved_routes.title": "Gespeicherte Reisen", + "gui.createrailwaysnavigator.schedule_board.title": "Abfahrtenliste", + "gui.createrailwaysnavigator.station_tags.title": "Bahnhof-Tags", + "gui.createrailwaysnavigator.journey_info.title": "Reiseinformation", + "gui.createrailwaysnavigator.journey_info.date": "Tag %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) nach %s", + "gui.createrailwaysnavigator.color_picker.custom": "Neu...", + "gui.createrailwaysnavigator.color_picker.no_color": "Keine Farbe", + "gui.createrailwaysnavigator.schedule_board.view_details": "Details ansehen", + "gui.createrailwaysnavigator.schedule_board.train_from": "von %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Abfahrt in", + "gui.createrailwaysnavigator.search_options.train_groups": "Zugkategorien", + "gui.createrailwaysnavigator.search_options.transfer_time": "Umstiegszeit", + "gui.createrailwaysnavigator.search_options.advanced_options": "Erweiterte Optionen", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Alle", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s ausgeschlossen", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Alle", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s gespeichert", + "gui.createrailwaysnavigator.empty_list": "Die Liste ist leer", + "gui.createrailwaysnavigator.new_entry.add": "Neuen Eintrag hinzufügen", + "gui.createrailwaysnavigator.new_entry.new": "Neu:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s gespeichert", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Fahrzeiten geändert!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "Die Fahrzeiten eines gespeicherten Route haben sich geändert. Bitte informieren Sie sich im Navigator über diese Änderungen.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Umstiege", + "gui.createrailwaysnavigator.route_overview.cancelled": "Fällt aus", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Gespeicherte Reise", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Ankunft", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Fällt heute aus!", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Verspätung ca. %s Minuten", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information zu %s: Fällt heute aus!", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information zu %s: Verspätung ca. %s Minuten", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", fällt heute aus. Wir bitten um Entschuldigung.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", heute circa %s Minuten später.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Grund dafür ist eine ", + "gui.createrailwaysnavigator.route_widget.show_details": "Details ansehen", + "gui.createrailwaysnavigator.route_widget.save": "Speichern", + "gui.createrailwaysnavigator.route_widget.remove": "Nicht mehr speichern", + "gui.createrailwaysnavigator.route_widget.share": "Teilen...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "In der Vergangenheit", + "gui.createrailwaysnavigator.saved_routes.today": "Heute", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Morgen", + "gui.createrailwaysnavigator.saved_routes.in_days": "In %s Tagen", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Details ansehen", + "gui.createrailwaysnavigator.saved_route_widget.share": "Teilen...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Benachrichtigunen aktivieren", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Verzögerungen im Betriebsablauf", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Verspätung aus vorheriger Fahrt", + "gui.createrailwaysnavigator.train_status.red_signal": "Haltzeigendes Signal", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Vorfahrt eines anderen Zuges", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Verspätung eines vorausfahrenden Zuges", + "gui.createrailwaysnavigator.train_status.track_closed": "Streckensperrung", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Personalausfall", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Betriebsstörung", + "gui.createrailwaysnavigator.train_status.special_trip": "Sonderfahrt", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Route speichern", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Route nicht mehr speichern", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Popup-Fenster anzeigen", + "gui.createrailwaysnavigator.navigator.my_profile": "Mein Profil", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Manche Routen können unvollständig sein oder überhaupt nicht vorgeschalgen werden, da Create Railways Navigator noch nicht alle Züge initialisiert hat.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Zuglinien", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Farbe auswählen", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Erstelle Linien, um mehrere Züge zu Linienzügen zusammenzufassen, deren Anzeigename zu überschreiben oder um weitere Einstellungen anzupassen. (Kann nur im Fahrplan zugewiesen werden)", "createrailwaysnavigator.moin": "moin" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json index 76210995..c12e9f8c 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json @@ -29,14 +29,17 @@ "block.createrailwaysnavigator.advanced_display_half_panel": "Half Advanced Display Panel", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Use it on _trains_, as a _train destination display_ or _passenger information display_, or at _train stations_ as improved _platform displays_ which also shows more information than regular display boards.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "When R-Clicked using a Wrench", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Open a menu to _configure_ the _display_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Open a menu to _configure_ the _display_.", "block.createrailwaysnavigator.advanced_display_sloped": "Sloped Advanced Display", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Use it on _trains_, as a _train destination display_ or _passenger information display_, or at _train stations_ as improved _platform displays_ which also shows more information than regular display boards.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "When R-Clicked using a Wrench", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Open a menu to _configure_ the _display_.", + "block.createrailwaysnavigator.advanced_display_slab": "Advanced Display Slab", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Use it on _trains_, as a _train destination display_ or _passenger information display_, or at _train stations_ as improved _platform displays_ which also shows more information than regular display boards.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "When R-Clicked using a Wrench", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Open a menu to _configure_ the _display_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Out of service!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "non-passenger ride", "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Show Route Overlay Options", @@ -73,6 +76,9 @@ "gui.createrailwaysnavigator.common.search": "Search", "gui.createrailwaysnavigator.common.auto": "Auto", "gui.createrailwaysnavigator.common.server_error": "Server error while executing task. Look console for details.", + "gui.createrailwaysnavigator.common.delete": "Delete", + "gui.createrailwaysnavigator.common.add": "Add", + "gui.createrailwaysnavigator.common.help": "Get Help", "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "No connections found.", @@ -88,15 +94,15 @@ "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Search Settings", "gui.createrailwaysnavigator.navigator.search.tooltip": "Search", "gui.createrailwaysnavigator.navigator.location.tooltip": "Nearest station from current position", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Refresh", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Switch fields", "gui.createrailwaysnavigator.route_details.title": "Route Details", "gui.createrailwaysnavigator.route_details.departure": "Departure in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transfer in", "gui.createrailwaysnavigator.route_details.transfer": "Transfer", - "gui.createrailwaysnavigator.route_details.save_route": "Save Connection", - "gui.createrailwaysnavigator.route_overview.title": "Route Details", + "gui.createrailwaysnavigator.route_overview.title": "Travel Companion", "gui.createrailwaysnavigator.route_overview.journey_begins": "Your journey begins! %s to %s, departure %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Your journey begins! %s to %s, departure %s on platform %s", "gui.createrailwaysnavigator.route_overview.train_details": "%s to %s", @@ -107,16 +113,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "You have reached %s. Thank you for traveling and have a nice day.", "gui.createrailwaysnavigator.route_overview.next_connections": "Next connections", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Transfer", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Train Canceled", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Train Cancelled", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Information about %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Canceled", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Cancelled", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Connection endangered", "gui.createrailwaysnavigator.route_overview.connection_missed": "Connection missed", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Train canceled", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Train cancelled", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Your journey to %s cannot be continued.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Due to a train delay you missed your connecting train. Search for an alternative in the navigator. We apologize for the inconvenience.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Information about %s: This train is canceled today! We apologize for the inconvenience. Search for an alternative in the navigator.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Train has been canceled", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Information about %s: This train is cancelled today! We apologize for the inconvenience. Search for an alternative in the navigator.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Train has been cancelled", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Your journey to %s cannot be continued. Search for an alternative in the navigator.", "gui.createrailwaysnavigator.route_overview.options": "Press %s for options.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Your journey to %s begins!", @@ -126,10 +132,10 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Your train in %s leaves from platform %s today.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Arrival %s delayed.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s instead of %s in %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Train canceled", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s to %s is canceled today.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Train cancelled", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s to %s is cancelled today.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Transfer is coming", - "gui.createrailwaysnavigator.route_overview.notification.transfer": "Change to %s → %s on platform %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Change to %s → %s", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Change to %s → %s on platform %s", "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Your connection is endangered!", "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "You probably won't be able to reach %s to %s.", @@ -142,24 +148,24 @@ "gui.createrailwaysnavigator.global_settings.title": "Global Settings", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Click to edit", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Train Station Tags", - "gui.createrailwaysnavigator.global_settings.option_alias.description": "Define station tags to threaten multi-platform stations (e.g. MyStation 1, MyStation 2, ...) as a single station (e.g. MyStation) with custom names.", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Combine individual train stations into a single multi-platform station with its own name so that the navigator can suggest transfers and more.", "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Train Station Blacklist", - "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Exclude Track Stations which should not appear in the navigation results. This stations will be ignored when generating routes.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Exclude Train Stations which should not appear in the navigation results. These stations will be ignored when generating routes.", "gui.createrailwaysnavigator.global_settings.train_group.title": "Train Groups", - "gui.createrailwaysnavigator.global_settings.train_group.description": "Create train groups to organize all trains (e.g. regional services, long-distance services, ...). Users can decide which groups they want to use.", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Create train groups to organize all trains (e.g. regional services, long-distance services, ...). Users can decide which train groups they want to use.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Train Blacklist", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Exclude trains, e.g. freight trains, special trains, etc., so that they are not used in the route suggestions.", - "gui.createrailwaysnavigator.alias_settings.title": "Train Station Tag Settings", - "gui.createrailwaysnavigator.alias_settings.summary": "Contains %s Track Stations", - "gui.createrailwaysnavigator.alias_settings.editor": "Last edited by %s on %s", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Create new Entry", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Delete Tag", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Remove Station", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Add Station", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Track Station Name", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Platform", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Enter name here", + "gui.createrailwaysnavigator.station_tags.summary": "Contains %s Train Stations", + "gui.createrailwaysnavigator.station_tags.editor": "Last edited by %s on %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Create new Entry", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Delete Tag", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Remove Station", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Click to change", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Add Station", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Train Station Name", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Platform Label", + "gui.createrailwaysnavigator.station_tags.enter_name": "Enter name here", "gui.createrailwaysnavigator.train_group_settings.title": "Train Group Settings", "gui.createrailwaysnavigator.train_group_settings.summary": "Contains %s Trains", @@ -246,5 +252,100 @@ "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Platform Column Width", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "in block pixels. (Default: Auto)", + "gui.createrailwaysnavigator.section_settings.title": "Section Settings", + "gui.createrailwaysnavigator.section_settings.train_groups": "Assign Train Group", + "gui.createrailwaysnavigator.section_settings.train_lines": "Assign Train Line", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Include start of next section", + "gui.createrailwaysnavigator.section_settings.usable": "Navigable", + "gui.createrailwaysnavigator.section_settings.none": "(None)", + + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Dynamic Delay", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Minimum Duration", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Wait: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "or at least %s", + + "createrailwaysnavigator.schedule.instruction.travel_section": "New Schedule Section", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Beginning of a new schedule section.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Configure...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Set Train Group: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Set Train Line: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Include start of next section: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Navigable: ", + + "createrailwaysnavigator.schedule.instruction.reset_timings": "Reset Timings", + + "display.createrailwaysnavigator.train_destination.simple": "Compact", + "display.createrailwaysnavigator.train_destination.extended": "Extended", + "display.createrailwaysnavigator.train_destination.detailed": "Detailed", + "display.createrailwaysnavigator.passenger_information.running_text": "Scrolling Text", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Detailed with Schedule", + "display.createrailwaysnavigator.platform.running_text": "Scrolling Text", + "display.createrailwaysnavigator.platform.table": "Table", + "display.createrailwaysnavigator.platform.focus": "Focus", + + "gui.createrailwaysnavigator.saved_routes.title": "Saved Routes", + "gui.createrailwaysnavigator.schedule_board.title": "Schedule Board", + "gui.createrailwaysnavigator.station_tags.title": "Station Tags", + "gui.createrailwaysnavigator.journey_info.title": "Journey information", + "gui.createrailwaysnavigator.journey_info.date": "Day %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) to %s", + "gui.createrailwaysnavigator.color_picker.custom": "Custom...", + "gui.createrailwaysnavigator.color_picker.no_color": "No Color", + "gui.createrailwaysnavigator.schedule_board.view_details": "View Details", + "gui.createrailwaysnavigator.schedule_board.train_from": "from %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Departure in", + "gui.createrailwaysnavigator.search_options.train_groups": "Train Groups", + "gui.createrailwaysnavigator.search_options.transfer_time": "Transfer Time", + "gui.createrailwaysnavigator.search_options.advanced_options": "Advanced Options", + "gui.createrailwaysnavigator.search_options.train_groups.all": "All", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s excluded", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "All", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s excluded", + "gui.createrailwaysnavigator.empty_list": "The list is empty", + "gui.createrailwaysnavigator.new_entry.add": "Add new entry", + "gui.createrailwaysnavigator.new_entry.new": "New:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s saved", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Schedule has changed!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "The schedule of your saved route has changed. Please check the navigator for these changes.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Transfers", + "gui.createrailwaysnavigator.route_overview.cancelled": "Cancelled", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Saved Route", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Arrival", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Cancelled", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Delay approx. %s minutes", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information about %s: This train got cancelled", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information about %s: Delay approx. %s minutes", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", is cancelled today. We apologize for the inconvenience.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", today approx. %s minutes delayed.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Reason: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Show details", + "gui.createrailwaysnavigator.route_widget.save": "Save", + "gui.createrailwaysnavigator.route_widget.remove": "Remove", + "gui.createrailwaysnavigator.route_widget.share": "Share...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "In the past", + "gui.createrailwaysnavigator.saved_routes.today": "Today", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Tomorrow", + "gui.createrailwaysnavigator.saved_routes.in_days": "In %s days", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Show details", + "gui.createrailwaysnavigator.saved_route_widget.share": "Share...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Show Notifications", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Delay in operations", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Delay in previous journey", + "gui.createrailwaysnavigator.train_status.red_signal": "Red signal", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Priority of another train", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Delay of another train", + "gui.createrailwaysnavigator.train_status.track_closed": "Track closed", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Staff shortage", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Operational disruption", + "gui.createrailwaysnavigator.train_status.special_trip": "Special trip", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Save route", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Remove route", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Show Popup", + "gui.createrailwaysnavigator.navigator.my_profile": "My Profile", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Some routes may be incomplete or not suggested at all because Create Railways Navigator has not initialized all trains yet.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Train Lines", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Select Color", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Create train lines to group different trains together, overwrite their display name and adjust other settings for each line. (Can only be assigned to the trains in the schedule)", + "createrailwaysnavigator.moin": "moin" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json index bbd4eacf..be3d0836 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json @@ -36,7 +36,6 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Abre un menú para _configurar_ la _pantalla_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "¡Fuera de servicio!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "viaje sin pasajeros", "category.createrailwaysnavigator.crn": "Navegador de ferrocarriles", "key.createrailwaysnavigator.route_overlay_options": "Mostrar opciones de superposición de ruta", @@ -107,16 +106,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "Has llegado a %s. Gracias por viajar y que tengas un buen día.", "gui.createrailwaysnavigator.route_overview.next_connections": "Próximas conexiones", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Transbordo", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Tren cancelado", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Tren cancelado", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Información sobre %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Cancelado", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Cancelado", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Conexión en peligro", "gui.createrailwaysnavigator.route_overview.connection_missed": "Conexión perdida", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Tren cancelado", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Tren cancelado", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Tu viaje a %s no puede continuar.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Debido a un retraso del tren, has perdido la conexión del tren. Busca una alternativa en el navegador. Pedimos disculpas por las molestias.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Información sobre %s: ¡Este tren está cancelado hoy! Pedimos disculpas por las molestias. Busca una alternativa en el navegador.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "El tren ha sido cancelado", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Información sobre %s: ¡Este tren está cancelado hoy! Pedimos disculpas por las molestias. Busca una alternativa en el navegador.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "El tren ha sido cancelado", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Tu viaje a %s no puede continuar. Busca una alternativa en el navegador.", "gui.createrailwaysnavigator.route_overview.options": "Pulsa %s para opciones.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "¡Tu viaje a %s comienza!", @@ -126,8 +125,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Tu tren en %s sale hoy desde el andén %s.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Llegada %s retrasada.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s en lugar de %s en %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Tren cancelado", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s a %s está cancelado hoy.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Tren cancelado", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s a %s está cancelado hoy.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Transbordo próximo", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Cambio a %s → %s en el andén %s", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Cambio a %s → %s en el andén %s", @@ -150,16 +149,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Lista negra de trenes", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Excluir trenes, por ejemplo, trenes de carga, trenes especiales, etc., para que no se utilicen en las sugerencias de rutas.", - "gui.createrailwaysnavigator.alias_settings.title": "Configuración de etiquetas de estaciones de tren", - "gui.createrailwaysnavigator.alias_settings.summary": "Contiene %s estaciones de tren", - "gui.createrailwaysnavigator.alias_settings.editor": "Última edición por %s el %s", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Crear nueva entrada", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Eliminar etiqueta", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Eliminar estación", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Agregar estación", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Nombre de la estación de tren", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Andén", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Introduce el nombre aquí", + "gui.createrailwaysnavigator.station_tags.title": "Configuración de etiquetas de estaciones de tren", + "gui.createrailwaysnavigator.station_tags.summary": "Contiene %s estaciones de tren", + "gui.createrailwaysnavigator.station_tags.editor": "Última edición por %s el %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Crear nueva entrada", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Eliminar etiqueta", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Eliminar estación", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Agregar estación", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nombre de la estación de tren", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Andén", + "gui.createrailwaysnavigator.station_tags.enter_name": "Introduce el nombre aquí", "gui.createrailwaysnavigator.train_group_settings.title": "Configuración de grupos de trenes", "gui.createrailwaysnavigator.train_group_settings.summary": "Contiene %s trenes", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json new file mode 100644 index 00000000..33f6046e --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json @@ -0,0 +1,250 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Merci d'avoir voyagé", + "advancement.createrailwaysnavigator.navigator.description": "Crafter un Navigateur pour trouver une connection entre une station et une autre.", + "advancement.createrailwaysnavigator.advanced_display": "Pas vraiment 4k", + "advancement.createrailwaysnavigator.advanced_display.description": "Améliorer vos tableaux d'affichage pour afficher plus d'information et même les placer dans vos trains.", + + "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", + + "item.createrailwaysnavigator.navigator": "Create Navigateur Ferroviaire", + "item.createrailwaysnavigator.navigator.tooltip.summary": "Le _navigateur_ montre les _correspondances_ possibles avec des informations supplémentaires comme les _escales_, des _informations en temps réel_ et plus encore.", + + "block.createrailwaysnavigator.train_station_clock": "Horloge de Gare", + "block.createrailwaysnavigator.advanced_display_block": "Bloc d'Affichage Avancé", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Quand click-droit en utilisant une clé", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display": "Tableau d'Affichage Avancé", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Quand click-droit en utilisant une clé", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_small": "Petit Tableau d'Affichage Avancé", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Quand click-droit en utilisant une clé", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_panel": "Panneau d'Affichage Avancé", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Quand click-droit en utilisant une clé", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Demi-Panneau d'Affichage Avancé", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Quand click-droit en utilisant une clé", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Affichage Incliné Avancé", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Quand click-droit en utilisant une clé", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Hors service!", + "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "train sans passagers", + + "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "key.createrailwaysnavigator.route_overlay_options": "Afficher les Options de l'Overlay d'Itinéraire", + + "enum.createrailwaysnavigator.overlay_position": "Position de l'Affichage", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Coin Supérieur Gauche", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Coin Supérieur Droit", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Coin Inférieur Gauche", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Coin Inférieur Droit", + + "gui.createrailwaysnavigator.loading.title": "Téléchargement des données du serveur...", + + "gui.createrailwaysnavigator.overlay_settings.title": "Paramètres de l'Overlay d'Itinéraire", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Afficher les détails", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Enlever l'overlay d'itinéraire", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Annonces du narrateur activées.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Annonces du narrateur désactivées.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Notifications activées", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Notifications désactivées", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Taille de l'interface", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Annonces du narrateur", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Le Narrateur annonce des évènements importants pendant votre voyage, ex. le prochain arrêt, des changements, etc.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notifications", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Recevez des notifications pop-up sur les évènements importants de votre voyage, ex. le prochain arrêt, des changements, etc.", + + "gui.createrailwaysnavigator.common.expand": "Afficher les détails", + "gui.createrailwaysnavigator.common.collapse": "Cacher les détails", + "gui.createrailwaysnavigator.common.go_back": "Retour", + "gui.createrailwaysnavigator.common.go_to_top": "Aller en haut", + "gui.createrailwaysnavigator.common.reset_defaults": "Réinitialiser aux valeurs par défaut", + "gui.createrailwaysnavigator.common.count": "Compte", + "gui.createrailwaysnavigator.common.true": "Oui", + "gui.createrailwaysnavigator.common.false": "Non", + "gui.createrailwaysnavigator.common.search": "Rechercher", + "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.server_error": "Erreur serveur lors de l'éxecution de la tâche. Regarder la console pour en savoir plus.", + + "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.navigator.no_connections": "Aucun itinéraire trouvé.", + "gui.createrailwaysnavigator.navigator.not_searched": "Rien n'a encore été recherché.", + "gui.createrailwaysnavigator.navigator.searching": "Recherche d'un itinéraire...", + "gui.createrailwaysnavigator.navigator.error_title": "Impossible de naviguer!", + "gui.createrailwaysnavigator.navigator.start_end_null": "Le départ ou la destination est vide.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Le départ et la destination sont égaux.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Itinéraire dans le passé", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Corresp.", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "à partir de %s", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Paramètres globaux", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Paramètres de recherche", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Recherche", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Gare la plus proche de votre position", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Inverser le point de départ et la destination", + + "gui.createrailwaysnavigator.route_details.title": "Détail de l'itinéraire", + "gui.createrailwaysnavigator.route_details.departure": "Départ dans", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Correspondance dans", + "gui.createrailwaysnavigator.route_details.transfer": "Correspondance", + "gui.createrailwaysnavigator.route_details.save_route": "Enregistrer l'itinéraire'", + + "gui.createrailwaysnavigator.route_overview.title": "Détail de l'itinéraire", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Votre voyage commence! %s à %s, départ %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Votre voyage commence! %s à %s, départ %s sur la voie %s", + "gui.createrailwaysnavigator.route_overview.train_details": "%s à %s", + "gui.createrailwaysnavigator.route_overview.next_stop": "Prochain arrêt: %s", + "gui.createrailwaysnavigator.route_overview.transfer": "Correspondance entre %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Correspondance entre %s → %s sur la voie %s", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Voyage terminé!", + "gui.createrailwaysnavigator.route_overview.after_journey": "Vous êtes arrivés à %s. Merci d'avoir voyagé et passez une bonne journée.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Prochaines correspondances", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Correspondance", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Train Annulé", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Information à propos de %s", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Annulé", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "Correspondance en danger", + "gui.createrailwaysnavigator.route_overview.connection_missed": "Correspondance manqué", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Train annulé", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Votre voyage vers %s ne peut se poursuivre.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "A cause d'un retard, vous avez manqué votre correspondace. Rechercher une alternative dans le Navigateur. Nous nous excusons pour le dérangement.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Information à propos de %s: Ce train a été annulé pour aujourd'hui! Nous nous excusons pour le dérangement. Rechercher une alternative dans le Navigateur.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Le train a été annulé", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Votre voyage vers %s ne peut pas se poursuivre. Rechercher une alternative dans le Navigateur.", + "gui.createrailwaysnavigator.route_overview.options": "Appuyer sur %s pour voir les options.", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Votre voyage vers %s commence!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s vers %s, départ %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s vers %s, départ %s depuis la voie %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Votre voie a changé!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Votre train dans %s part de la voie %s aujourd'hui.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Départ %s retardé.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s au lieu de %s dans %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Train annulé", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s vers %s est annulé aujourd'hui.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Votre correspondance est en approche", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Correspondance entre %s → %s sur la voie %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Correspondance entre %s → %s sur la voie %s", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Votre correspondance est en danger!", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Vous ne pourrez probablement pas atteindre %s vers %s.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Correspondance manquée", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Vous avez manqué votre correspondace %s vers %s.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Vous avez atteint votre destination !", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Merci d'avoir voyagé et passez une bonne journée", + "gui.createrailwaysnavigator.route_overview.date": "Jour %s, %s", + + "gui.createrailwaysnavigator.global_settings.title": "Paramètres globaux", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Cliquez ici pour modifier", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Balises de Gare", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Définissez des balises de gares pour traîter une gare à multiples quais (ex. MaGare 1, MaGare 2) comme une seule gare (e.g. MaGare) avec un nom personnalisé.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Liste Noire de Gare", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Exclure les gares qui ne devrait pas apparaître dans les résultats de navigations. Ces gares seront ignorés lors de la génération d'itinéraire.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Groupes de Train", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Créez des groupes de trains pour les organiser (ex. services régionaux, services longue distance, ...). Les utilisateurs peuvent décider quels groupes ils souhaitent utiliser.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Liste Noire de Train", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Exclure des trains, ex. trains de marchandises, trains spéciaux, etc., de manière à ce qu'ils ne puissent être utilisé dans des itinéraires.", + + "gui.createrailwaysnavigator.station_tags.title": "Paramètres des Balises de Gares", + "gui.createrailwaysnavigator.station_tags.summary": "Contient %s Gares", + "gui.createrailwaysnavigator.station_tags.editor": "Modifié par %s le %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Créer une nouvelle entrée", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Supprimer la Balise", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Retirer une Gare", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Ajouter une Gare", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nom de la Station", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Quai", + "gui.createrailwaysnavigator.station_tags.enter_name": "Entrer un nom ici", + + "gui.createrailwaysnavigator.train_group_settings.title": "Paramètres des Groupes de Trains", + "gui.createrailwaysnavigator.train_group_settings.summary": "Contient %s Trains", + "gui.createrailwaysnavigator.train_group_settings.editor": "Modifié par %s le %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Supprimer le Groupe", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Retirer le Train", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Ajouter un Train", + + "gui.createrailwaysnavigator.blacklist.title": "Liste noire des Gares", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Ajouter à la liste noire", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Retirer de la liste noire", + + "gui.createrailwaysnavigator.train_blacklist.title": "Liste noire des Trains", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Ajouter à la liste noire", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Retirer de la liste noire", + + "gui.createrailwaysnavigator.search_settings.title": "Paramètres de recherche", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Temps de Correspondance Minimum", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Le temps minimum qui devrait être disponible pour changer de train. (1h ~ 50 secondes réelles)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Filtre de Catégorie de Train", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Décidez quels trains et quelles catégories de trains vous souhaitez utiliser.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s catégories séléctionés", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Tous", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Aucun", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Réinitialiser les filtres", + + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Ajouter", + + "gui.createrailwaysnavigator.time": "Temps: %s", + "gui.createrailwaysnavigator.time.now": "maintenant", + "gui.createrailwaysnavigator.time_format.dhm": "%s jours %s h. %s m.", + "gui.createrailwaysnavigator.time_format.hm": "%s h. %s min.", + "gui.createrailwaysnavigator.time_format.m": "%s m.", + + "gui.createrailwaysnavigator.platform": "Voie", + "gui.createrailwaysnavigator.departure": "Départ", + "gui.createrailwaysnavigator.destination": "Destination", + "gui.createrailwaysnavigator.line": "Ligne", + "gui.createrailwaysnavigator.following_trains": "Trains suivants:", + "gui.createrailwaysnavigator.via": "via", + + "gui.createrailwaysnavigator.advanced_display_settings.title": "Paramètres des Affichages Avancés", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Type d'Affichage", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Détermine les informations affichés.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Type d'Information", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Détermine le degré de détail des informations.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Double-face", + + "enum.createrailwaysnavigator.display_info_type": "Type d'Affiche d'Information", + "enum.createrailwaysnavigator.display_info_type.description": "Détermine combien d'informations devraient être affichés sur votre tableau d'affichage.", + "enum.createrailwaysnavigator.display_info_type.simple": "Simple", + "enum.createrailwaysnavigator.display_info_type.info.simple": "L'écran va seulement afficher les informations les plus importantes sans détails supplémentaires.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Détaillé", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "Les informations les plus importantes avec quelques détails seront affichés, comme la vitesse du trains, les escales, etc.", + "enum.createrailwaysnavigator.display_info_type.informative": "Informatif", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Affiche toutes les informations qui pourraient être intéressantes et les affiches dans une disposition agréable. Déconseillé pour les petits petits affichages car le texte peu devenir très petit.", + + "enum.createrailwaysnavigator.display_type": "Type d'Affichage", + "enum.createrailwaysnavigator.display_type.description": "Le type d'affiche en fonction de son objectif.", + "enum.createrailwaysnavigator.display_type.train_destination": "Destination du Train", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Destiné à être utilisé en dehors des trains car il affiche des informations sur le train lui-même, comme le nom, la destinations et (si séléctionés) les escales.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Informations pour les passagers", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Représentes les écrans présents à l'interieur des trains. Ces écrans afficheront les prochains arrêts, la direction de la sortie et (si séléctionés) la vitesse du train et une vue d'ensemble de la route", + "enum.createrailwaysnavigator.display_type.platform": "Affichage de quai", + "enum.createrailwaysnavigator.display_type.info.platform": "Ces écrans doivent être utilisés sur les quais des gares et montre les prochains trains arrivant avec quelques détails en plus si séléctionés. Ne peut pas être utilisés à l'intérieur des trains!", + + "enum.createrailwaysnavigator.side": "Face", + "enum.createrailwaysnavigator.side.description": "La face du bloc où les informations devraient être affichés.", + "enum.createrailwaysnavigator.side.front": "Face avant", + "enum.createrailwaysnavigator.side.info.front": "Les informations seront affichés uniquement sur la face avant du bloc. C'est le comportement par défaut.", + "enum.createrailwaysnavigator.side.both": "Toutes les faces", + "enum.createrailwaysnavigator.side.info.both": "Les informations seront affichés sur toutes les faces.", + + "enum.createrailwaysnavigator.time_display": "Affichage des horaires", + "enum.createrailwaysnavigator.time_display.description": "Détermines comment les horaires devraient être affichés", + "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absolu)", + "enum.createrailwaysnavigator.time_display.eta": "ETA", + "enum.createrailwaysnavigator.time_display.info.eta": "ETA (estimation du temps d'arrivée)", + + "create.display_source.advanced_display": "Affichage Avancé", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Largeur de la colonne du nom du train", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "en pixels bloc. (Defaut: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Largeur de la colonne du quai", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "en pixel bloc. (Defaut: Auto)", + + "createrailwaysnavigator.moin": "moin" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json new file mode 100644 index 00000000..87d4f882 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json @@ -0,0 +1,250 @@ +{ + "advancement.createrailwaysnavigator.navigator": "오늘도 열차를 이용해주셔서 고맙습니다", + "advancement.createrailwaysnavigator.navigator.description": "기차역 사이를 잇는 열차를 검색하기 위한 탐색기를 제작하세요.", + "advancement.createrailwaysnavigator.advanced_display": "4K 화질은 아니로군", + "advancement.createrailwaysnavigator.advanced_display.description": "더 많은 정보를 표시하기 위해 전광판을 업그레이드하고 무려 기차 안에다가도 설치해보세요.", + + "itemGroup.createrailwaysnavigator.tab": "Create 철도 탐색기", + + "item.createrailwaysnavigator.navigator": "Create 철도 탐색기", + "item.createrailwaysnavigator.navigator.tooltip.summary": "_탐색기_는 추가적인 _열차 경로_와 함께 _경유지_, _실시간 데이터_ 등 다양한 정보를 보여줍니다.", + + "block.createrailwaysnavigator.train_station_clock": "승강장 시계", + "block.createrailwaysnavigator.advanced_display_block": "고급 전광판 블록", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display": "고급 전광판", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_small": "소형 고급 전광판", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_panel": "고급 전광판 판넬", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_half_panel": "고급 전광판 반블록", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_sloped": "역경사형 고급 전광판", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "점검 중", + "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "무승객열차", + + "category.createrailwaysnavigator.crn": "Create 철도 탐색기", + "key.createrailwaysnavigator.route_overlay_options": "경로 오버레이 옵션 보이기", + + "enum.createrailwaysnavigator.overlay_position": "안내판 위치", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "좌측 상단", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "우측 상단", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "좌측 하단", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "우측 하단", + + "gui.createrailwaysnavigator.loading.title": "서버에서 데이터를 가져옵니다...", + + "gui.createrailwaysnavigator.overlay_settings.title": "경로 오버레이 설정", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "자세히 보기", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "경로 오버레이 삭제", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "안내방송 활성화", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "안내방송 비활성화", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "알림 활성화", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "알림 비활성화", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "안내창 크기", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "안내방송", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "열차 운행 중 중요한 정보 (도착역, 변경사항 등)에 대한 음성 안내를 받습니다.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "알림", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "열차 운행 중 중요한 정보 (도착역, 변경사항 등)에 대한 알림을 받습니다.", + + "gui.createrailwaysnavigator.common.expand": "자세히 보기", + "gui.createrailwaysnavigator.common.collapse": "숨기기", + "gui.createrailwaysnavigator.common.go_back": "뒤로", + "gui.createrailwaysnavigator.common.go_to_top": "맨 위로", + "gui.createrailwaysnavigator.common.reset_defaults": "초기화", + "gui.createrailwaysnavigator.common.count": "개수", + "gui.createrailwaysnavigator.common.true": "예", + "gui.createrailwaysnavigator.common.false": "아니오", + "gui.createrailwaysnavigator.common.search": "검색", + "gui.createrailwaysnavigator.common.auto": "자동", + "gui.createrailwaysnavigator.common.server_error": "작업을 수행하던 중 서버 오류가 발생했습니다. 자세한 정보는 콘솔을 확인하십시오.", + + "gui.createrailwaysnavigator.navigator.title": "Create 철도 탐색기", + "gui.createrailwaysnavigator.navigator.no_connections": "연결된 열차가 없습니다.", + "gui.createrailwaysnavigator.navigator.not_searched": "아직 검색되지 않았습니다.", + "gui.createrailwaysnavigator.navigator.searching": "검색 중...", + "gui.createrailwaysnavigator.navigator.error_title": "불가능한 경로입니다!", + "gui.createrailwaysnavigator.navigator.start_end_null": "출발지 혹은 도착지가 존재하지 않습니다.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "출발지와 도착지가 동일합니다.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ 갱신되지 않은 연결", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "환승역", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "%s발", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "광역 설정", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "검색 설정", + "gui.createrailwaysnavigator.navigator.search.tooltip": "검색", + "gui.createrailwaysnavigator.navigator.location.tooltip": "현재 위치에서 가장 가까운 열차", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "영역 전환", + + "gui.createrailwaysnavigator.route_details.title": "경로 상세", + "gui.createrailwaysnavigator.route_details.departure": "출발 시각", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "환승 시각", + "gui.createrailwaysnavigator.route_details.transfer": "환승", + "gui.createrailwaysnavigator.route_details.save_route": "연결 저장", + + "gui.createrailwaysnavigator.route_overview.title": "경로 상세", + "gui.createrailwaysnavigator.route_overview.journey_begins": "%s 열차, %s 행, 출발시각 %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "%s 열차, %s 행, 출발시각 %s, 타는 곳 %s", + "gui.createrailwaysnavigator.route_overview.train_details": "%s 열차, %s행", + "gui.createrailwaysnavigator.route_overview.next_stop": "이번 역은 %s 입니다", + "gui.createrailwaysnavigator.route_overview.transfer": "%s 열차의 %s 행으로 환승", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "%s 열차(%s 행, 타는 곳 %s)로 환승", + "gui.createrailwaysnavigator.route_overview.journey_completed": "여정 완료", + "gui.createrailwaysnavigator.route_overview.after_journey": "%s에 도착하셨습니다. 오늘도 열차를 이용해주셔서 고맙습니다. 안녕히 가십시오.", + "gui.createrailwaysnavigator.route_overview.next_connections": "다음 경로", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "환승", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "운행 취소됨", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "%s 열차 정보", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ 운행 취소", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "해당 경로에 긴급상황 발생", + "gui.createrailwaysnavigator.route_overview.connection_missed": "경로 이탈", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "열차가 운행 취소됨", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "%s로 향하는 운행이 단축운행 되었습니다. 불편을 끼쳐드려 대단히 죄송합니다.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "열차 지연으로 인해 다음 열차에 탑승하실 수 없습니다. 새로운 대체편성을 검색해주십시오. 이용에 불편을 끼쳐드려 대단히 죄송합니다.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "금일 %s 열차는 운행이 취소되었습니다. 새로운 대체편성을 검색해주십시오. 이용에 불편을 끼쳐드려 대단히 죄송합니다.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "열차 운행 취소에 대한 사과말씀", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "%s로 향하는 열차가 단축운행 되었습니다. 새로운 대체편성을 검색해주십시오. 이용에 불편을 끼쳐드려 대단히 죄송합니다.", + "gui.createrailwaysnavigator.route_overview.options": "%s 눌러 선택사항 확인", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "%s로 향하는 여정 시작", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s 열차, %s 행, 출발시각 %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s 열차, %s 행, 출발시각 %s, 타는 곳 %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "타는 곳이 변경되었습니다!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "%s의 열차는 오늘 타는 곳 %s으로 들어올 예정입니다.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s의 %s 지연에 관한 안내", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s 도착 예정, 기존 시각 %s (%s)", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s 열차 취소에 관한 안내", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s에서 탑승해야 할 %s행 열차가 취소되었습니다.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "곧 환승해야 합니다.", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "%s 열차(%s행)로 환승", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "%s 열차(%s행, 타는 곳 %s)로 환승", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "긴급상황발생에 따른 열차 운행중단에 관한 안내", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "위와 같은 사유로 %s 열차(%s행)는 이 역에서 운행을 중단하였습니다. 불편을 끼쳐드려 대단히 죄송합니다.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "환승 열차를 놓쳤습니다!", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "%s 열차(%s행)를 탈 수 없습니다.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "운행 완료.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "오늘도 철도를 이용해주셔서 고맙습니다. 가시는 목적지까지 안녕히 가십시오.", + "gui.createrailwaysnavigator.route_overview.date": "%s일, %s", + + "gui.createrailwaysnavigator.global_settings.title": "광역 설정", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "클릭하여 수정하기", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "기차역 태그", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "기차역 태그를 만들어 여러개의 승강장이 있는 경우 (예: OO역 타는 곳 1번, 2번, 3번...) 하나의 기차역으로 표시할 수 있습니다.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "기차역 블랙리스트", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "기차역 블랙리스트에 등록된 기차역은 경로 추천에 사용되지 않습니다.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "기차 그룹", + "gui.createrailwaysnavigator.global_settings.train_group.description": "기차 그룹을 만들어 다양한 목적의 열차(지역열차, 광역열차 등)를 관리합니다. 원하는 그룹을 사용할 수도 있습니다.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "기차 블랙리스트", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "기차 블랙리스트에 등록된 기차(예: 화물열차, 특수열차)는 경로 추천에 사용되지 않습니다.", + + "gui.createrailwaysnavigator.station_tags.title": "기차역 태그 설정", + "gui.createrailwaysnavigator.station_tags.summary": "%s개의 타는 곳 포함", + "gui.createrailwaysnavigator.station_tags.editor": "%s님이 %s에 마지막으로 수정함", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "새 태그 추가", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "태그 삭제", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "타는 곳 삭제", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "타는 곳 추가", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "정거장 이름", + "gui.createrailwaysnavigator.station_tags.hint.platform": "타는 곳", + "gui.createrailwaysnavigator.station_tags.enter_name": "이름 입력", + + "gui.createrailwaysnavigator.train_group_settings.title": "기차 그룹 설정", + "gui.createrailwaysnavigator.train_group_settings.summary": "%s개의 기차 포함", + "gui.createrailwaysnavigator.train_group_settings.editor": "%s이/가 %s에 마지막으로 편집", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "그룹 삭제", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "기차 삭제", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "기차 추가", + + "gui.createrailwaysnavigator.blacklist.title": "기차역 블랙리스트", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "블랙리스트에 추가", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "블랙리스트에서 삭제", + + "gui.createrailwaysnavigator.train_blacklist.title": "기차 블랙리스트", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "블랙리스트에 추가", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "블랙리스트에서 삭제", + + "gui.createrailwaysnavigator.search_settings.title": "검색 설정", + "gui.createrailwaysnavigator.search_settings.transfer_time": "최소 환승 시간", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "환승하는 데 걸릴 최소 시간을 설정합니다. (실제 시간 기준 1시간 ~ 50초)", + "gui.createrailwaysnavigator.search_settings.train_groups": "기차 종류 선택", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "검색할 기차 종류를 선택합니다.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s개 선택됨", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "모두선택", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "선택취소", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "필터 초기화", + + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "추가하기", + + "gui.createrailwaysnavigator.time": "현재시각 %s", + "gui.createrailwaysnavigator.time.now": "지금", + "gui.createrailwaysnavigator.time_format.dhm": "%s일 %s시간 %s분", + "gui.createrailwaysnavigator.time_format.hm": "%s시간 %s분", + "gui.createrailwaysnavigator.time_format.m": "%s분", + + "gui.createrailwaysnavigator.platform": "타는 곳", + "gui.createrailwaysnavigator.departure": "출발시간", + "gui.createrailwaysnavigator.destination": "도착역", + "gui.createrailwaysnavigator.line": "열차종류", + "gui.createrailwaysnavigator.following_trains": "다음 열차", + "gui.createrailwaysnavigator.via": "경유: ", + + "gui.createrailwaysnavigator.advanced_display_settings.title": "고급 전광판 설정", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "전광판 종류", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "어디에 사용할 전광판인지 결정합니다.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "표시 내용", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "내용의 상세 정도를 결정합니다.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "양면 표기", + + "enum.createrailwaysnavigator.display_info_type": "전광판 표시 내용", + "enum.createrailwaysnavigator.display_info_type.description": "전광판에 어느정도 내용까지 표시할지 결정합니다.", + "enum.createrailwaysnavigator.display_info_type.simple": "간단하게", + "enum.createrailwaysnavigator.display_info_type.info.simple": "추가적인 내용 없이 가장 필요한 정보만 표시합니다.", + "enum.createrailwaysnavigator.display_info_type.detailed": "상세하게", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "기차 속도, 경유지와 같은 중요한 정보들도 표시합니다.", + "enum.createrailwaysnavigator.display_info_type.informative": "풍부하게", + "enum.createrailwaysnavigator.display_info_type.info.informative": "흥미로울 수 있는 모든 정보를 멋지게 표시합니다. 작은 전광판에선 글씨가 작아질 수 있습니다.", + + "enum.createrailwaysnavigator.display_type": "전광판 종류", + "enum.createrailwaysnavigator.display_type.description": "각각의 목적에 맞는 전광판을 선택합니다.", + "enum.createrailwaysnavigator.display_type.train_destination": "열차 행선지", + "enum.createrailwaysnavigator.display_type.info.train_destination": "기차 외부에 설치되는 전광판으로, 열차 이름, 도착지, 경유지(선택한 경우) 등을 표시합니다.", + "enum.createrailwaysnavigator.display_type.passenger_information": "승객 안내용", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "기차 내부에 설치되는 전광판으로, 다음 역, 내릴 문, 기차 속도(선택한 경우), 기차 경로 정보 등을 표시합니다.", + "enum.createrailwaysnavigator.display_type.platform": "승강장 전광판", + "enum.createrailwaysnavigator.display_type.info.platform": "승강장에 설치되는 전광판으로, 다음에 도착할 열차와 그 열차의 정보 등을 표시합니다. 열차에는 사용할 수 없습니다!", + + "enum.createrailwaysnavigator.side": "전광판 위치", + "enum.createrailwaysnavigator.side.description": "전광판이 어느 쪽에 표시될지 결정합니다.", + "enum.createrailwaysnavigator.side.front": "앞쪽", + "enum.createrailwaysnavigator.side.info.front": "전광판의 앞면에 표시됩니다. (기본)", + "enum.createrailwaysnavigator.side.both": "양쪽", + "enum.createrailwaysnavigator.side.info.both": "전광판의 양면에 표시됩니다.", + + "enum.createrailwaysnavigator.time_display": "시간 표기법", + "enum.createrailwaysnavigator.time_display.description": "시간을 표시할 방법을 결정합니다.", + "enum.createrailwaysnavigator.time_display.abs": "절대", + "enum.createrailwaysnavigator.time_display.info.abs": "정확한 시간을 띄웁니다. (기본)", + "enum.createrailwaysnavigator.time_display.eta": "상대", + "enum.createrailwaysnavigator.time_display.info.eta": "남은 시간을 띄웁니다.", + + "create.display_source.advanced_display": "고급 전광판", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "기차 이름 폭", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "픽셀 단위. (기본: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "타는 곳 이름 폭", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "픽셀 단위. (기본: 자동)", + + "createrailwaysnavigator.moin": "moin" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json index d981ee89..58006b95 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json @@ -36,7 +36,6 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Buiten dienst!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Rangeertocht", "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Toon Route Overlay Opties", @@ -107,16 +106,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "U heeft %s bereikt. Bedankt voor het reizen en een fijne dag verder.", "gui.createrailwaysnavigator.route_overview.next_connections": "Volgende verbindingen", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Overstap", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Trein geannuleerd", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Trein geannuleerd", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informatie over %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Geannuleerd", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Geannuleerd", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Verbinding in gevaar", "gui.createrailwaysnavigator.route_overview.connection_missed": "Verbinding gemist", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Trein geannuleerd", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Trein geannuleerd", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Uw reis naar %s kan niet worden voortgezet.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Door een treinvertraging heeft u uw aansluitende trein gemist. Zoek naar een alternatief in de navigator. Onze excuses voor het ongemak.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Informatie over %s: Deze trein is vandaag geannuleerd! Onze excuses voor het ongemak. Zoek naar een alternatief in de navigator.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Trein is geannuleerd", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informatie over %s: Deze trein is vandaag geannuleerd! Onze excuses voor het ongemak. Zoek naar een alternatief in de navigator.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Trein is geannuleerd", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Uw reis naar %s kan niet worden voortgezet. Zoek naar een alternatief in de navigator.", "gui.createrailwaysnavigator.route_overview.options": "Druk op %s voor opties.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Uw reis naar %s begint!", @@ -126,8 +125,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Uw trein in %s vertrekt vandaag vanaf platform %s.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Aankomst %s vertraagd.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s in plaats van %s in %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Trein geannuleerd", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s naar %s is vandaag geannuleerd.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Trein geannuleerd", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s naar %s is vandaag geannuleerd.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Overstap komt eraan", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Wissel naar %s → %s op platform %s", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Wissel naar %s → %s op platform %s", @@ -150,16 +149,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Trein Zwarte Lijst", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Sluit treinen uit, bijv. vrachttreinen, speciale treinen, etc., zodat ze niet worden gebruikt in de routesuggesties.", - "gui.createrailwaysnavigator.alias_settings.title": "Treinstation Tag Instellingen", - "gui.createrailwaysnavigator.alias_settings.summary": "Bevat %s Spoor Stations", - "gui.createrailwaysnavigator.alias_settings.editor": "Laatst bewerkt door %s op %s", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Nieuwe invoer maken", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Tag verwijderen", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Station verwijderen", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Station toevoegen", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Spoor Station Naam", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Platform", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Voer hier de naam in", + "gui.createrailwaysnavigator.station_tags.title": "Treinstation Tag Instellingen", + "gui.createrailwaysnavigator.station_tags.summary": "Bevat %s Spoor Stations", + "gui.createrailwaysnavigator.station_tags.editor": "Laatst bewerkt door %s op %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Nieuwe invoer maken", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Tag verwijderen", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Station verwijderen", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Station toevoegen", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Spoor Station Naam", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Platform", + "gui.createrailwaysnavigator.station_tags.enter_name": "Voer hier de naam in", "gui.createrailwaysnavigator.train_group_settings.title": "Trein Groep Instellingen", "gui.createrailwaysnavigator.train_group_settings.summary": "Bevat %s Treinen", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json index f2e7e617..1999b03d 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json @@ -36,7 +36,6 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Nieczynny!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Wycieczka manewrowa", "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Pokaż opcje nakładki trasy", @@ -107,16 +106,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "Dotarłeś do %s. Dziękujemy za podróż i życzymy miłego dnia.", "gui.createrailwaysnavigator.route_overview.next_connections": "Następne połączenia", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Przesiadka", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Pociąg odwołany", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Pociąg odwołany", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informacja o %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Odwołano", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Odwołano", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Połączenie zagrożone", "gui.createrailwaysnavigator.route_overview.connection_missed": "Połączenie przegapione", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Pociąg odwołany", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Pociąg odwołany", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Twoja podróż do %s jest niemożliwa do kontynuowania.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "W związku z opóżnieniem pociągu, przegapiłeś połączenie. Wyszukaj inne połączenie w Nawigatorze. Za utrudnienia przepraszamy.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Informacja o %s: Ten pociąg został odwołany! Za utrudnienia przepraszamy. Wyszukaj inne połączenie w Nawigatorze.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Pociąg został odwołany", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informacja o %s: Ten pociąg został odwołany! Za utrudnienia przepraszamy. Wyszukaj inne połączenie w Nawigatorze.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Pociąg został odwołany", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Twoja podróż do %s jest niemożliwa do kontynuowania. Wyszukaj inne połączenie w Nawigatorze.", "gui.createrailwaysnavigator.route_overview.options": "Wciśnij %s aby zobaczyć opcje.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Twoja podróż do %s rozpoczyna się!", @@ -126,8 +125,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Twój pociąg w %s wyrusza dzisiaj z peronu %s.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Przyjazd %s opóżniony.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s zamiast %s w %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Pociąg odwołany", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s do %s został odwołany.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Pociąg odwołany", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s do %s został odwołany.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Nadchodzi przesiadka", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Przesiadka %s → %s na peronie %s", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Przesiadka %s → %s na peronie %s", @@ -150,16 +149,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Czarna lista pociągów", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Wyklucz pociągi, np. towarowe, specjalne, itp., aby nie pojawiały się one w wynikach wyszukiwania.", - "gui.createrailwaysnavigator.alias_settings.title": "Ustawienia tagów stacji", - "gui.createrailwaysnavigator.alias_settings.summary": "Zawiera %s stacji", - "gui.createrailwaysnavigator.alias_settings.editor": "Ostatnio edytowane przez %s o %s", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Utwórz nowy wpis", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Usuń tag", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Usuń stację", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Dodaj stację", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Nazwa stacji", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Peron", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Wprowadź nazwę", + "gui.createrailwaysnavigator.station_tags.title": "Ustawienia tagów stacji", + "gui.createrailwaysnavigator.station_tags.summary": "Zawiera %s stacji", + "gui.createrailwaysnavigator.station_tags.editor": "Ostatnio edytowane przez %s o %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Utwórz nowy wpis", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Usuń tag", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Usuń stację", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Dodaj stację", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nazwa stacji", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Peron", + "gui.createrailwaysnavigator.station_tags.enter_name": "Wprowadź nazwę", "gui.createrailwaysnavigator.train_group_settings.title": "Ustawienia grupy pociągów", "gui.createrailwaysnavigator.train_group_settings.summary": "Zawiera %s pociągów", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json new file mode 100644 index 00000000..928591c4 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json @@ -0,0 +1,250 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Obrigado por viajar", + "advancement.createrailwaysnavigator.navigator.description": "Crie um navegador para procurar ligações ferroviárias de uma estação ferroviária para outra.", + "advancement.createrailwaysnavigator.advanced_display": "Não é bem 4k", + "advancement.createrailwaysnavigator.advanced_display.description": "Atualize as suas placas de exibição para exibir mais informações e até mesmo colocá-las nos seus comboios.", + + "itemGroup.createrailwaysnavigator.tab": "Criar Navegador Ferroviário", + + "item.createrailwaysnavigator.navigator": "Criar Navegador Ferroviário", + "item.createrailwaysnavigator.navigator.tooltip.summary": "O _navegador_ mostra possíveis _ligações de comboios_ com informações adicionais, como _escalas_, data_ em tempo _real e muito mais.", + + "block.createrailwaysnavigator.train_station_clock": "Relógio da Estação de Comboios", + "block.createrailwaysnavigator.advanced_display_block": "Bloco de exibição avançada", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display": "Placa de exibição avançada", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_small": "Ecrã de exibição pequena avançada", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_panel": "Painel de exibição avançada", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Painel de exibição meio avançado", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Ecrã avançado inclinado", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Fora de Serviço!", + "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Viagem sem passageiros", + + "category.createrailwaysnavigator.crn": "Criar Navegador Ferroviário", + "key.createrailwaysnavigator.route_overlay_options": "Mostrar opções de sobreposição de rota", + + "enum.createrailwaysnavigator.overlay_position": "Posição do ecrã", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Canto superior esquerdo", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Canto superior direito", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Canto inferior esquerdo", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Canto inferior direito", + + "gui.createrailwaysnavigator.loading.title": "Baixando dados do servidor...", + + "gui.createrailwaysnavigator.overlay_settings.title": "Configurações de sobreposição de rota", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Mostrar detalhes", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Remover sobreposicao de rota", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Anuncios do Narrador ativados.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Anuncios do Narrador desativados.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Notificações ativadas", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Notificações desativadas", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Escala GUI", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Anuncios do Narrador", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "O Narrador anuncia eventos importantes em sua jornada, por exemplo, a proxima parada, mudancas, etc.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notificações", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Receba notificacoes do sistema sobre eventos importantes em sua jornada, por exemplo, a proxima parada, alteracoes, etc.", + + "gui.createrailwaysnavigator.common.expand": "Mostrar detalhes", + "gui.createrailwaysnavigator.common.collapse": "Ocultar detalhes", + "gui.createrailwaysnavigator.common.go_back": "Voltar", + "gui.createrailwaysnavigator.common.go_to_top": "Rolar para cima", + "gui.createrailwaysnavigator.common.reset_defaults": "Redefinir para o padrão", + "gui.createrailwaysnavigator.common.count": "Contar", + "gui.createrailwaysnavigator.common.true": "Sim", + "gui.createrailwaysnavigator.common.false": "Não", + "gui.createrailwaysnavigator.common.search": "Procurar", + "gui.createrailwaysnavigator.common.auto": "Automático", + "gui.createrailwaysnavigator.common.server_error": "Erro do servidor durante a execucao da tarefa. Procure detalhes no console.", + + "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.navigator.no_connections": "Nenhuma conexao encontrada.", + "gui.createrailwaysnavigator.navigator.not_searched": "Nada pesquisado ainda.", + "gui.createrailwaysnavigator.navigator.searching": "Procurar conexões ...", + "gui.createrailwaysnavigator.navigator.error_title": "Incapaz de navegar!", + "gui.createrailwaysnavigator.navigator.start_end_null": "O início ou o destino estao vazios.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Início e destino sao iguais.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Conexão no passado", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Trans.", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "de %s", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Configurações globais", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Configurações de pesquisa", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Procurar", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Estacao mais proxima da posicao atual", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Alternar campos", + + "gui.createrailwaysnavigator.route_details.title": "Detalhes da Rota", + "gui.createrailwaysnavigator.route_details.departure": "Partida em", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transferência em", + "gui.createrailwaysnavigator.route_details.transfer": "Transferência", + "gui.createrailwaysnavigator.route_details.save_route": "Salvar conexão", + + "gui.createrailwaysnavigator.route_overview.title": "Detalhes da Rota", + "gui.createrailwaysnavigator.route_overview.journey_begins": "A sua jornada começa aqui! %s a %s, partida %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "A sua jornada começa aqui! %s a %s, partida %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.train_details": "%s a %s", + "gui.createrailwaysnavigator.route_overview.next_stop": "Próxima estação: %s", + "gui.createrailwaysnavigator.route_overview.transfer": "Mude para %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Mude para %s → %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Jornada concluída", + "gui.createrailwaysnavigator.route_overview.after_journey": "Atingiu! %s. Obrigado por viajar e tenha um bom dia.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Próximas conexões", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Transferência", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Comboio cancelado", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informação sobre %s", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Cancelado", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "Conexão ameaçada", + "gui.createrailwaysnavigator.route_overview.connection_missed": "Conexão perdida", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Comboio cancelado", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "A sua jornada para %s não pode ser continuada.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Devido a um atraso no comboio, perdeu o comboio de ligacao. Procure uma alternativa no navegador. Pedimos desculpas pelo inconveniente.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informações sobre %s: Este comboio foi cancelado hoje! Pedimos desculpas pelo inconveniente. Procure uma alternativa no navegador.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "O comboio foi cancelado", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "A sua jornada para %s não pode ser continuada. Procure uma alternativa no navegador.", + "gui.createrailwaysnavigator.route_overview.options": "Pressione %s para obter opções.", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Sua jornada para %s começa!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s a %s, partida %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s a %s, partida %s da plataforma %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Sua plataforma mudou!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "O seu comboio em %s sai da plataforma %s hoje.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Chegada %s atrasada.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s em vez de %s em %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Comboio cancelado", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s a %s foi cancelado hoje.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Transferência esta chegando", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Mudar para %s → %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Mudar para %s → %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "A sua conexão esta ameaçada!", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Você provavelmente não conseguirá alcancar %s a %s.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Conexao perdida", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Você perdeu o comboio de conexão %s para %s.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Chegou ao seu destino!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Obrigado por viajar e tenha um bom dia", + "gui.createrailwaysnavigator.route_overview.date": "Dia %s, %s", + + "gui.createrailwaysnavigator.global_settings.title": "Configurações globais", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Clique para editar", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Tags da estaçao de comboio", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Defina tags de estacao para ameacar estacoes multiplataforma (por exemplo, MyStation 1, MyStation 2, ... ) como uma unica estacao (por exemplo, MyStation) com nomes personalizados.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Lista negra da estacao de comboio", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Excluir estações de trilha que nao devem aparecer nos resultados da navegacao. Essas estacoes serao ignoradas ao gerar rotas.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Grupos de comboios", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Criar grupos de comboios para organizar todos os comboios (por exemplo, servicos regionais, servicos de longa distancia, ... ). Os usuarios podem decidir quais grupos desejam usar.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Lista negra de comboios", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Excluir comboios, por exemplo, comboios de mercadorias, comboios especiais, etc., para que nao sejam utilizados nas sugestoes de rota.", + + "gui.createrailwaysnavigator.station_tags.title": "Configurações de tags da estação de comboios", + "gui.createrailwaysnavigator.station_tags.summary": "Contém %s Track Stations", + "gui.createrailwaysnavigator.station_tags.editor": "Última edição por %s em %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Criar nova entrada", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Excluir Tag", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Remover estaçãon", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Adicionar estações", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nome de estação de trilha", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Plataforma", + "gui.createrailwaysnavigator.station_tags.enter_name": "Digite o nome aqui", + + "gui.createrailwaysnavigator.train_group_settings.title": "Configurações do grupo de Comboio", + "gui.createrailwaysnavigator.train_group_settings.summary": "Contém %s Comboios", + "gui.createrailwaysnavigator.train_group_settings.editor": "Ultima edição por %s em %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Delete Group", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Remover comboio", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Adicionar Comboio", + + "gui.createrailwaysnavigator.blacklist.title": "Lista negra da estacao de comboios", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Adicionar a lista negra", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Remover da lista negra", + + "gui.createrailwaysnavigator.train_blacklist.title": "Lista negra de comboios", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Adicionar a lista negra", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Remover da lista negra", + + "gui.createrailwaysnavigator.search_settings.title": "Configurações de pesquisa", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Tempo minimo de transferência", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "O tempo minimo que deve estar disponivel para mudar de comboio. (1h ~ 50 segundos da vida real)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Filtro de categoria de comboio", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Decida quais comboios de quais categorias de trem voce deseja usar.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s categorias selecionadas", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Todo", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Não", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Redefinir filtro", + + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Adicionar", + + "gui.createrailwaysnavigator.time": "Tempo: %s", + "gui.createrailwaysnavigator.time.now": "agora", + "gui.createrailwaysnavigator.time_format.dhm": "%s dias %s hrs. %s min.", + "gui.createrailwaysnavigator.time_format.hm": "%s hrs. %s min.", + "gui.createrailwaysnavigator.time_format.m": "%s min.", + + "gui.createrailwaysnavigator.platform": "Plataforma", + "gui.createrailwaysnavigator.departure": "Partida", + "gui.createrailwaysnavigator.destination": "Destino", + "gui.createrailwaysnavigator.line": "Linha", + "gui.createrailwaysnavigator.following_trains": "Seguintes Comboios:", + "gui.createrailwaysnavigator.via": "Via", + + "gui.createrailwaysnavigator.advanced_display_settings.title": "Configurações avançadas de exibição", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Tipo de exibição", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Determina as informações exibidas.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Tipo de informação", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Determina o quão detalhadas sao as informações.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dupla face", + + "enum.createrailwaysnavigator.display_info_type": "Tipo de Informações de Exibição", + "enum.createrailwaysnavigator.display_info_type.description": "Determina a quantidade de informações que devem ser exibidas no quadro de exibição.", + "enum.createrailwaysnavigator.display_info_type.simple": "Simples", + "enum.createrailwaysnavigator.display_info_type.info.simple": "A tela mostrara apenas as informacoes mais importantes, sem detalhes adicionais.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Detalhado", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "As informações mais importantes com alguns detalhes serão mostradas, como velocidade do comboio, escalas, etc.", + "enum.createrailwaysnavigator.display_info_type.informative": "Informativo", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Mostra todas as informacoes que poderiam ser interessantes e as exibe em um layout sofisticado. Nao recomendado para telas pequenas, pois o texto pode ficar muito pequeno.", + + "enum.createrailwaysnavigator.display_type": "Tipo de exibição", + "enum.createrailwaysnavigator.display_type.description": "O tipo de exibicao que depende de sua finalidade.", + "enum.createrailwaysnavigator.display_type.train_destination": "Destino do Comboio", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Inteded para ser usado fora dos trens, pois mostra informações sobre o próprio trem, como o nome, destino e (se selecionado) escalas e outras informações.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Informações ao Passageiro", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Representa as telas encontradas dentro dos comboios. Essas exibições mostrarao a proxima estação, a direção de saída e (se selecionada) a velocidade do comboio e uma visão geral da rota.", + "enum.createrailwaysnavigator.display_type.platform": "Exibição da plataforma", + "enum.createrailwaysnavigator.display_type.info.platform": "Esses displays devem ser usados nas plataformas das estações de trem e mostram os próximos comboios com detalhes adicionais, se selecionados. Não pode ser usado em Comboios!", + + "enum.createrailwaysnavigator.side": "Lado", + "enum.createrailwaysnavigator.side.description": "O lado do bloco onde as informações devem ser renderizadas.", + "enum.createrailwaysnavigator.side.front": "Parte Frontal", + "enum.createrailwaysnavigator.side.info.front": "As informações serão renderizadas apenas na parte frontal. Esse é o comportamento padrão", + "enum.createrailwaysnavigator.side.both": "Ambos os lados", + "enum.createrailwaysnavigator.side.info.both": "As informações serao prestadas em ambos os lados.", + + "enum.createrailwaysnavigator.time_display": "Exibição de tempo", + "enum.createrailwaysnavigator.time_display.description": "Determina como a hora deve ser exibida.", + "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absoluto)", + "enum.createrailwaysnavigator.time_display.eta": "ETA", + "enum.createrailwaysnavigator.time_display.info.eta": "ETA (hora prevista de chegada)", + + "create.display_source.advanced_display": "Monitores Avancados", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Largura da coluna do nome do comboio", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "em pixeis de bloco. (Padrão: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Largura da coluna da plataforma", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "em píxeis de bloco (Padrão: Auto)", + + "createrailwaysnavigator.moin": "moin" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json index aa3c4a38..0a2d7a74 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json @@ -36,7 +36,6 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Вышел из строя!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Поездка без пассажиров", "category.createrailwaysnavigator.crn": "Create: Навигатор железных дорог", "key.createrailwaysnavigator.route_overlay_options": "Показать параметры наложения маршрута", @@ -107,16 +106,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "Вы достигли станции %s. Спасибо за поездку.", "gui.createrailwaysnavigator.route_overview.next_connections": "Пересадки на", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Пересадка", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Поезд отменён", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Поезд отменён", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Информация о %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ отменено", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ отменено", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Соединение под угрозой", "gui.createrailwaysnavigator.route_overview.connection_missed": "Соединение потеряно", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Поезд отменён", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Поезд отменён", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Ваша поездка к %s не может быть продолжена.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Из-за задержки поезда вы опоздали на стыковочный поезд. Найдите альтернативный вариант в навигаторе. Приносим извинения за доставленные неудобства.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Информация о %s: Этот поезд сегодня отменен! Приносим извинения за доставленные неудобства. Найдите альтернативный вариант в навигаторе.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Поезд был отменен", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Информация о %s: Этот поезд сегодня отменен! Приносим извинения за доставленные неудобства. Найдите альтернативный вариант в навигаторе.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Поезд был отменен", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Вашу поездку к %s невозможно продолжить. Выполните поиск альтернативного маршрута в навигаторе.", "gui.createrailwaysnavigator.route_overview.options": "Нажмите %s для опций", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Ваша поездка к %s начинается!", @@ -126,8 +125,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ваш поезд в %s отходит от платформы %s сегодня.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: прибытие %s задержка.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s вместо %s на %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Поезд отменён", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s в сторону %s отменён сегодня.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Поезд отменён", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s в сторону %s отменён сегодня.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Пересадка скоро", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Изменить %s → %s на платформе %s", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Изменить %s → %s на платформе %s", @@ -150,16 +149,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Чёрный список поездов", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Исключите поезда, например, грузовые, специальные и т.д., чтобы они не использовались в предложениях по маршруту.", - "gui.createrailwaysnavigator.alias_settings.title": "Настройки тегов станций", - "gui.createrailwaysnavigator.alias_settings.summary": "Количество станций: %s", - "gui.createrailwaysnavigator.alias_settings.editor": "Изменён %s в %s", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Создать новую запись", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Удалить тег", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Удалить станцию", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Добавить станцию", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Имя станции", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Платформа", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Ввести имя здесь", + "gui.createrailwaysnavigator.station_tags.title": "Настройки тегов станций", + "gui.createrailwaysnavigator.station_tags.summary": "Количество станций: %s", + "gui.createrailwaysnavigator.station_tags.editor": "Изменён %s в %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Создать новую запись", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Удалить тег", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Удалить станцию", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Добавить станцию", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Имя станции", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Платформа", + "gui.createrailwaysnavigator.station_tags.enter_name": "Ввести имя здесь", "gui.createrailwaysnavigator.train_group_settings.title": "Найстройки групп поездов", "gui.createrailwaysnavigator.train_group_settings.summary": "Включает %s Tпоездов", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json new file mode 100644 index 00000000..57b9c4cf --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json @@ -0,0 +1,250 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Tack för att du reser med oss!", + "advancement.createrailwaysnavigator.navigator.description": "Tillverka en Navigator för att söka efter tågförbildelser från en station till en annan.", + "advancement.createrailwaysnavigator.advanced_display": "Inte riktigt 4k", + "advancement.createrailwaysnavigator.advanced_display.description": "Upgradera dina displaytavlor till att visa mer information som även funkar i tåg.", + + "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", + + "item.createrailwaysnavigator.navigator": "Create Railways Navigator", + "item.createrailwaysnavigator.navigator.tooltip.summary": "The _navigator_ shows possible _train connections_ with additional information such as _stopovers_, _real-time data_ and more.", + + "block.createrailwaysnavigator.train_station_clock": "Tåstationsklocka", + "block.createrailwaysnavigator.advanced_display_block": "Avancerat Displayblock", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Använd den på _tåg_, som en _tågdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller på _tågstationer_ som förbättrade _plattformsdisplayer_ som också visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öppna en meny för att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display": "Avancerad Displaytavla", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Använd den på _tåg_, som en _tågdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller på _tågstationer_ som förbättrade _plattformsdisplayer_ som också visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öppna en meny för att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_small": "Liten Avancerad Skärm", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Använd den på _tåg_, som en _tågdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller på _tågstationer_ som förbättrade _plattformsdisplayer_ som också visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öppna en meny för att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_panel": "Avancerad Skärmpanel", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Använd den på _tåg_, som en _tågdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller på _tågstationer_ som förbättrade _plattformsdisplayer_ som också visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öppna en meny för att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Halv Avancerad Skärmpanel", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Använd den på _tåg_, som en _tågdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller på _tågstationer_ som förbättrade _plattformsdisplayer_ som också visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öppna en meny för att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Lutande Avancerad Skärm", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Använd den på _tåg_, som en _tågdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller på _tågstationer_ som förbättrade _plattformsdisplayer_ som också visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öppna en meny för att _konfigurera_ _displayen_.", + + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Ej i trafik", + "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Abonnerad", + + "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "key.createrailwaysnavigator.route_overlay_options": "Visa Alternativ För Ruttöverlagring", + + "enum.createrailwaysnavigator.overlay_position": "Visa Position", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Övre Vänstra Hörnet", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Övre Högra Hörnet", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Nedre Vänstra Hörnet", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Nedre Högra Hörnet", + + "gui.createrailwaysnavigator.loading.title": "Downloading data from server...", + + "gui.createrailwaysnavigator.overlay_settings.title": "Inställningar för ruttöverlagring", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Visa detaljer", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Ta bort ruttöverlagring", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Berättarmeddelanden har aktiverats.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Berättarmeddelanden har inaktiverats.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Aviseringar har aktiverats", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Aviseringar har inaktiverats", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "GUI Storlek", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Berättarmeddelanden", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Berättaren tillkännager viktiga händelser på din resa, t.ex. nästa stopp, förändringar osv.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Aviseringar", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Få toast-aviseringar om viktiga händelser på din resa, t.ex. nästa stopp, förändringar osv.", + + "gui.createrailwaysnavigator.common.expand": "Visa Detaljer", + "gui.createrailwaysnavigator.common.collapse": "Dölj Detaljer", + "gui.createrailwaysnavigator.common.go_back": "Gå Tillbaka", + "gui.createrailwaysnavigator.common.go_to_top": "Skrolla Till Toppen", + "gui.createrailwaysnavigator.common.reset_defaults": "Återställ till standard", + "gui.createrailwaysnavigator.common.count": "Räkna", + "gui.createrailwaysnavigator.common.true": "Ja", + "gui.createrailwaysnavigator.common.false": "Nej", + "gui.createrailwaysnavigator.common.search": "Sök", + "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.server_error": "Serverfel när uppgiften körs. Se konsolen för detaljer.", + + "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.navigator.no_connections": "Inga anslutningar hittades.", + "gui.createrailwaysnavigator.navigator.not_searched": "Inget sökt ännu.", + "gui.createrailwaysnavigator.navigator.searching": "Sök efter anslutningar...", + "gui.createrailwaysnavigator.navigator.error_title": "Ingen rutt hittades!", + "gui.createrailwaysnavigator.navigator.start_end_null": "Start eller destination är tom.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Du har angett samma start och destination.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Anslutning i det förflutna", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Trans.", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "från %s", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Globala Inställningar", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Sökinställningar", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Sök", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Närmaste station från nuvarande position", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Växla fält", + + "gui.createrailwaysnavigator.route_details.title": "Ruttdetaljer", + "gui.createrailwaysnavigator.route_details.departure": "Avgår om", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Vidarekoppling om", + "gui.createrailwaysnavigator.route_details.transfer": "Byte", + "gui.createrailwaysnavigator.route_details.save_route": "Spara Anslutning", + + "gui.createrailwaysnavigator.route_overview.title": "Ruttdetaljer", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Din resa börjar! %s mot %s, avgår %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Din resa börjar! %s mot %s, avgår %s från plattform %s", + "gui.createrailwaysnavigator.route_overview.train_details": "%s mot %s", + "gui.createrailwaysnavigator.route_overview.next_stop": "Nästa: %s", + "gui.createrailwaysnavigator.route_overview.transfer": "Byt till %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Byt till %s → %s on platform %s", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Resa avklarad", + "gui.createrailwaysnavigator.route_overview.after_journey": "Du har nått %s. Tack för att du reser med oss och ha en trevlig dag.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Anslutningar", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Byte", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Inställt", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Information angående %s", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Inställd", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "Risk att missa bytet", + "gui.createrailwaysnavigator.route_overview.connection_missed": "Anslutning missad", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Inställt", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Din resa till %s kan inte fortsätta.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "På grund av en försening missade du din anslutning. Sök efter en alternativ rutt i navigatorn. Vi ber om ursäkt för besväret.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Information om %s: Den här avgången är inställd idag! Vi ber om ursäkt för besväret. Sök efter en alternativ rutt i navigatorn.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Tåget är inställt", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Din resa till %s kan inte fortsätta. Sök efter en alternativ rutt i navigatorn.", + "gui.createrailwaysnavigator.route_overview.options": "Tryck på %s för alternativ.", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Din resa till %s börjar!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s till %s, avgår %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s till %s, avgår %s från plattform %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Din anslutning har ändrat plattform!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ditt tåg i %s går från perrong %s idag.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Ankomst %s försenad.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s istället för %s i %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Avgång inställd", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s mot %s är tyvärr inställd idag.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Din anslutning är på väg", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Byt till %s → %s på plattform %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Byt till %s → %s på plattform %s", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Risk att missa bytet", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Du har en hög risk att missa %s mot %s.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Du har tyvärr missat ditt byte", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Du har missat ditt byte %s mot %s.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Du är framme vid din destination!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Tack för att du reser med oss och ha en fortsatt trevlig dag!", + "gui.createrailwaysnavigator.route_overview.date": "Dag %s, %s", + + "gui.createrailwaysnavigator.global_settings.title": "Globala Inställningar", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Klicka för att ändra", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Stationstaggar", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Definiera stationstaggar för att definera multiplattformsstationer (t.ex. MinStation 1, MinStation 2, ...) som en enda station (t.ex. MinStation) med anpassade namn.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Stationssvartlista", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Uteslut stationer som inte ska visas i navigeringsresultaten. Dessa stationer kommer att ignoreras när rutter genereras.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Tåggrupper", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Skapa tåggrupper för att organisera alla tåg (t.ex. regionaltrafik, fjärrtrafik, ...). Användare kan bestämma vilka grupper de vill använda.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Tågsvartlista", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Uteslut tåg, t.ex. godståg, specialtåg etc. så att de inte används i ruttförslagen.", + + "gui.createrailwaysnavigator.station_tags.title": "Inställningar För Stationstaggar", + "gui.createrailwaysnavigator.station_tags.summary": "Innehåller %s stationer", + "gui.createrailwaysnavigator.station_tags.editor": "Senast redigerad av %s den %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Skapa ny Post", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Radera Tag", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Radera Station", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Lägg till Station", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Stationsnamn", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Plattform", + "gui.createrailwaysnavigator.station_tags.enter_name": "Ange namn här", + + "gui.createrailwaysnavigator.train_group_settings.title": "Tåggruppinställningar", + "gui.createrailwaysnavigator.train_group_settings.summary": "Innehåller %s tåg", + "gui.createrailwaysnavigator.train_group_settings.editor": "Senast redigerad av %s den %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Radera Grupp", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Radera Tåg", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Lägg till Tåg", + + "gui.createrailwaysnavigator.blacklist.title": "Stationssvartlista", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Lägg till på svartlistan", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Ta bort från svartlistan", + + "gui.createrailwaysnavigator.train_blacklist.title": "Tågsvartlista", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Lägg till på svartlistan", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Ta bort från svartlistan", + + "gui.createrailwaysnavigator.search_settings.title": "Sökinställningar", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Minsta överföringstid", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Den minsta tid som bör finnas tillgänglig för att byta tåg. (1h ~ 50 Sekunder i Verkliga Livet)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Tågkategorifilter", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Bestäm vilka tåg från vilka tågkategorier du vill använda.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s kategorier har valts", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Alla", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Inga", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Återställ filter", + + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Lägg till", + + "gui.createrailwaysnavigator.time": "Tid: %s", + "gui.createrailwaysnavigator.time.now": "Nu", + "gui.createrailwaysnavigator.time_format.dhm": "%s dgr %s tim. %s min.", + "gui.createrailwaysnavigator.time_format.hm": "%s tim. %s min.", + "gui.createrailwaysnavigator.time_format.m": "%s min.", + + "gui.createrailwaysnavigator.platform": "Plattform", + "gui.createrailwaysnavigator.departure": "Avgpng", + "gui.createrailwaysnavigator.destination": "Destination", + "gui.createrailwaysnavigator.line": "Linje", + "gui.createrailwaysnavigator.following_trains": "Följande anslutningar:", + "gui.createrailwaysnavigator.via": "via", + + "gui.createrailwaysnavigator.advanced_display_settings.title": "Avancerade Skärminställningar", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Skärmtyp", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Avgör vilken information som visas.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informationstyp", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Avgör hur detaljerad informationen är.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dubbelsidig", + + "enum.createrailwaysnavigator.display_info_type": "Visa Infotyp", + "enum.createrailwaysnavigator.display_info_type.description": "Bestämmer hur mycket information som ska visas på skärmen.", + "enum.createrailwaysnavigator.display_info_type.simple": "Enkel", + "enum.createrailwaysnavigator.display_info_type.info.simple": "Skärmen visar bara den viktigaste informationen utan ytterligare detaljer.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Detaljerad", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "Den viktigaste informationen med vissa detaljer kommer att visas, såsom tåghastighet, mellanstationer, osv.", + "enum.createrailwaysnavigator.display_info_type.informative": "Informativ", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Visar all information som kan vara intressant och visar den i en snygg layout. Rekommenderas inte för små skärmar eftersom texten kan bli väldigt liten.", + + "enum.createrailwaysnavigator.display_type": "Bildskärmstyp", + "enum.createrailwaysnavigator.display_type.description": "Typen av din skärm som beror på deras syfte.", + "enum.createrailwaysnavigator.display_type.train_destination": "Tågdestination", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Avsedd att användas utanför tåg då den visar information om själva tåget såsom namn, destination och (om valt) mellanstationer och annan information.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Passagerarinformation", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Representerar skärmarna som finns inuti tågen. Dessa displayer visar nästa stopp, utfartsriktningen och (om valt) tåghastighet och en ruttöversikt.", + "enum.createrailwaysnavigator.display_type.platform": "Plattformsdisplay", + "enum.createrailwaysnavigator.display_type.info.platform": "Dessa displayer bör användas på plattformar och perronger och visar nästa ankommande tåg med ytterligare detaljer om de väljs. Kan inte användas i tåg!", + + "enum.createrailwaysnavigator.side": "Sida", + "enum.createrailwaysnavigator.side.description": "Den sida av blocket där informationen ska återges.", + "enum.createrailwaysnavigator.side.front": "Framsida", + "enum.createrailwaysnavigator.side.info.front": "Informationen återges endast på framsidan. Detta är standardbeteendet.", + "enum.createrailwaysnavigator.side.both": "Båda Sidor", + "enum.createrailwaysnavigator.side.info.both": "Informationen kommer att återges på båda sidor.", + + "enum.createrailwaysnavigator.time_display": "Tidsvisning", + "enum.createrailwaysnavigator.time_display.description": "Bestämmer hur tiden ska visas.", + "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS (visar klockslaget då fordonet ankommer)", + "enum.createrailwaysnavigator.time_display.eta": "ETA", + "enum.createrailwaysnavigator.time_display.info.eta": "ETA (visar fordonets beräknade ankomsttid)", + + "create.display_source.advanced_display": "Avancerade Skärmar", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Tågnamnets kolumnbredd", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "i blockpixlar. (Standard: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Plattformens kolumnbredd", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "i blockpixlar. (Standard: Auto)", + + "createrailwaysnavigator.moin": "moin" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json index 767b1612..e1d8537b 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json @@ -35,8 +35,7 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "R-Gligg midd nem Schraubnschlüssl", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne ä _Menü_, um dä Anzeichn ze _gonfiguriern_.", - "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Ausser Beddrieb", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Beddriebschfoahrd", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Beddriebschfoahrd", "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Schdreggnanzeiche Eenschdellungn", @@ -108,16 +107,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "Sie hamm %s orreichd. Wir bedanggen uns für Ihre Reise und wünschen nen schönen Dahch.", "gui.createrailwaysnavigator.route_overview.next_connections": "Nächsde Anschlüsse", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Umschdiech", - "gui.createrailwaysnavigator.route_overview.train_canceled": "Zuhch fälld aus", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Zuhch fälld aus", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informadschion ze %s", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ Fälld aus", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Fälld aus", "gui.createrailwaysnavigator.route_overview.connection_endangered": "Anschluss gefährd'", "gui.createrailwaysnavigator.route_overview.connection_missed": "Anschluss forbassd", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "Zuhch fälld aus", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Zuhch fälld aus", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Ihre Reise nooch %s gann ne fordgesedsd wer'n.", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Ufgrund änner Zuchforschbädung hamm Se Ihren Anschluss forbassd. Suchen Se nooch änner Aldornaddife im Wegweesr. Wir endschuldschn uns für dä Unannehmlichgeidn.", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "Informadschion ze %s: Diesorr Zuhch fälld heude aus! Wir endschuldschen uns für dä Unannehmlichgeidn. Suchen Se nooch änner Aldornaddife im Wegweesr.", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "Zuhch fälld heude aus", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informadschion ze %s: Diesorr Zuhch fälld heude aus! Wir endschuldschen uns für dä Unannehmlichgeidn. Suchen Se nooch änner Aldornaddife im Wegweesr.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Zuhch fälld heude aus", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Ihre Reise nooch %s gann ne fordgesedzd wer'n. Suchen Se nooch änner Aldornaddife im Wegweesr..", "gui.createrailwaysnavigator.route_overview.options": "Drügge %s für Obdschonen.", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Ihre Reise nooch %s beginnd!", @@ -127,8 +126,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ihr Zuhch in %s fährd heude fonn Gleis %s ab.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Anngunfd %s ferschbäded.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s schdadd %s in %s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s: Zuhch fälld aus", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "%s nooch %s fälld heude aus.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Zuhch fälld aus", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s nooch %s fälld heude aus.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Ihr Umschdiech schdehd befor", "gui.createrailwaysnavigator.route_overview.notification.transfer": "Schdeichen se in %s → %s uf Gleis %s um", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Schdeichen e in %s → %s um", @@ -151,16 +150,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Zuhch Blägglisd", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Schließe Zühche aus, z.B. Güdorzühche, Sonderzühche, edc., damidd diese ne in'dn Forschlächn Ufdauchn.", - "gui.createrailwaysnavigator.alias_settings.title": "Boahnhof Dägs Eenschdellungn", - "gui.createrailwaysnavigator.alias_settings.summary": "Endhäld %s Schdadsionen", - "gui.createrailwaysnavigator.alias_settings.editor": "Z'ledsd fonn %s am %s beoarbeided", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "Neuen Eindrahch erschdelln", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "Dahch löschn", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "Schdadschion endfernn", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "Schdadschion hinzufüchn", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "Boahnhofsname", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "Gleisbezeichnung", - "gui.createrailwaysnavigator.alias_settings.enter_name": "Name eingeb'n", + "gui.createrailwaysnavigator.station_tags.title": "Boahnhof Dägs Eenschdellungn", + "gui.createrailwaysnavigator.station_tags.summary": "Endhäld %s Schdadsionen", + "gui.createrailwaysnavigator.station_tags.editor": "Z'ledsd fonn %s am %s beoarbeided", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Neuen Eindrahch erschdelln", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Dahch löschn", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Schdadschion endfernn", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Schdadschion hinzufüchn", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Boahnhofsname", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Gleisbezeichnung", + "gui.createrailwaysnavigator.station_tags.enter_name": "Name eingeb'n", "gui.createrailwaysnavigator.train_group_settings.title": "Zuhchgaddegorien Eenschdellungn", "gui.createrailwaysnavigator.train_group_settings.summary": "Endhäld %s Zühche", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json index 06a2ab67..ec0cfa8b 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json @@ -36,7 +36,6 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "打开菜单以_配置_显示器。", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "未启用", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "调车行程", "category.createrailwaysnavigator.crn": "机械动力:铁路导航", "key.createrailwaysnavigator.route_overlay_options": "显示路线覆盖选项", @@ -107,16 +106,16 @@ "gui.createrailwaysnavigator.route_overview.after_journey": "您已抵达%s。感谢您的乘坐,祝您旅途愉快。", "gui.createrailwaysnavigator.route_overview.next_connections": "下一车次", "gui.createrailwaysnavigator.route_overview.schedule_transfer": "换乘", - "gui.createrailwaysnavigator.route_overview.train_canceled": "车次取消", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "车次取消", "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "关于%s的信息", - "gui.createrailwaysnavigator.route_overview.stop_canceled": "❌ 已取消", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ 已取消", "gui.createrailwaysnavigator.route_overview.connection_endangered": "换乘受阻", "gui.createrailwaysnavigator.route_overview.connection_missed": "错过换乘", - "gui.createrailwaysnavigator.route_overview.connection_canceled": "车次取消", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "车次取消", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "您前往%s的旅程无法继续。", "gui.createrailwaysnavigator.route_overview.connection_missed_info": "由于列车延误,您错过了换乘列车。请在导航仪中搜索替代路线。对于给您造成的不便,我们深表歉意。", - "gui.createrailwaysnavigator.route_overview.train_canceled_info": "关于%s的信息:该车次已取消!对于给您造成的不便,我们深表歉意。请在导航仪中搜索替代路线。", - "gui.createrailwaysnavigator.route_overview.train_canceled_title": "车次取消", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "关于%s的信息:该车次已取消!对于给您造成的不便,我们深表歉意。请在导航仪中搜索替代路线。", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "车次取消", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "您前往%s的旅程无法继续。请在导航仪中搜索替代路线。", "gui.createrailwaysnavigator.route_overview.options": "按%s键查看选项。", "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "您前往%s的旅程开始了!", @@ -126,8 +125,8 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "您在%s的列车将从%s号站台出发。", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s:延误%s", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "原定%2$s,现将于%1$s抵达%3$s", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled.title": "%s:列车取消", - "gui.createrailwaysnavigator.route_overview.notification.train_canceled": "前往%2$s的%1$s已取消。", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s:列车取消", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "前往%2$s的%1$s已取消。", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "即将换乘", "gui.createrailwaysnavigator.route_overview.notification.transfer": "换乘%s→%s,在%s站台乘车", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "换乘%s→%s,在%s站台乘车", @@ -150,16 +149,16 @@ "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "列车黑名单", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "排除列车,例如货运列车、专列等,以便在路线建议中不显示它们。", - "gui.createrailwaysnavigator.alias_settings.title": "车站标签设定", - "gui.createrailwaysnavigator.alias_settings.summary": "包含%s个车站", - "gui.createrailwaysnavigator.alias_settings.editor": "最后编辑者:%s,于%s", - "gui.createrailwaysnavigator.alias_settings.add.tooltip": "创建新条目", - "gui.createrailwaysnavigator.alias_settings.delete_alias.tooltip": "删除标签", - "gui.createrailwaysnavigator.alias_settings.delete_station.tooltip": "移除车站", - "gui.createrailwaysnavigator.alias_settings.add_station.tooltip": "添加车站", - "gui.createrailwaysnavigator.alias_settings.hint.station_name": "车站名称", - "gui.createrailwaysnavigator.alias_settings.hint.platform": "站台", - "gui.createrailwaysnavigator.alias_settings.enter_name": "在此输入名称", + "gui.createrailwaysnavigator.station_tags.title": "车站标签设定", + "gui.createrailwaysnavigator.station_tags.summary": "包含%s个车站", + "gui.createrailwaysnavigator.station_tags.editor": "最后编辑者:%s,于%s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "创建新条目", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "删除标签", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "移除车站", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "添加车站", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "车站名称", + "gui.createrailwaysnavigator.station_tags.hint.platform": "站台", + "gui.createrailwaysnavigator.station_tags.enter_name": "在此输入名称", "gui.createrailwaysnavigator.train_group_settings.title": "列车分类设置", "gui.createrailwaysnavigator.train_group_settings.summary": "包含%s辆列车", "gui.createrailwaysnavigator.train_group_settings.editor": "最后编辑者:%s,%s", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json new file mode 100644 index 00000000..9c33aad6 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json @@ -0,0 +1,62 @@ +{ + "credit": "Made with Blockbench", + "texture_size": [64, 64], + "textures": { + "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_small", + "particle": "createrailwaysnavigator:block/advanced_display" + }, + "elements": [ + { + "from": [0, 4, 0], + "to": [16, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, + "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, + "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, + "down": {"uv": [0, 0, 8, 8], "texture": "#2"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, -1], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, 225, 0], + "translation": [0, -0.25, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 0, -8] + }, + "fixed": { + "translation": [0, 0, -0.75], + "scale": [0.5, 0.5, 0.5] + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json new file mode 100644 index 00000000..67a2fbe4 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json @@ -0,0 +1,62 @@ +{ + "credit": "Made with Blockbench", + "texture_size": [64, 64], + "textures": { + "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_small", + "particle": "createrailwaysnavigator:block/advanced_display" + }, + "elements": [ + { + "from": [0, 4, 0], + "to": [16, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, + "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, + "down": {"uv": [0, 0, 8, 8], "texture": "#2"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, -1], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, 225, 0], + "translation": [1.75, 1, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 0, -8] + }, + "fixed": { + "translation": [0, 0, -0.75], + "scale": [0.5, 0.5, 0.5] + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json new file mode 100644 index 00000000..e566e226 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json @@ -0,0 +1,62 @@ +{ + "credit": "Made with Blockbench", + "texture_size": [64, 64], + "textures": { + "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_small", + "particle": "createrailwaysnavigator:block/advanced_display" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 8, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, -4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, + "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, + "down": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "down"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, -1], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, 225, 0], + "translation": [1.75, 1, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 0, -8] + }, + "fixed": { + "translation": [0, 0, -0.75], + "scale": [0.5, 0.5, 0.5] + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json new file mode 100644 index 00000000..146c2ec8 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json @@ -0,0 +1,62 @@ +{ + "credit": "Made with Blockbench", + "texture_size": [64, 64], + "textures": { + "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_small", + "particle": "createrailwaysnavigator:block/advanced_display" + }, + "elements": [ + { + "from": [0, 8, 0], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, + "up": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "up"}, + "down": {"uv": [0, 0, 8, 8], "texture": "#2"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, -1], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, 225, 0], + "translation": [1.75, 1, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 0, -8] + }, + "fixed": { + "translation": [0, 0, -0.75], + "scale": [0.5, 0.5, 0.5] + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json new file mode 100644 index 00000000..37d2a568 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json @@ -0,0 +1,62 @@ +{ + "credit": "Made with Blockbench", + "texture_size": [64, 64], + "textures": { + "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_small", + "particle": "createrailwaysnavigator:block/advanced_display" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 8, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, -4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, + "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, + "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, + "down": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "down"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, -1], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, 225, 0], + "translation": [1.75, 1, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 0, -8] + }, + "fixed": { + "translation": [0, 0, -0.75], + "scale": [0.5, 0.5, 0.5] + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json new file mode 100644 index 00000000..6bf910ae --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json @@ -0,0 +1,62 @@ +{ + "credit": "Made with Blockbench", + "texture_size": [64, 64], + "textures": { + "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_small", + "particle": "createrailwaysnavigator:block/advanced_display" + }, + "elements": [ + { + "from": [0, 8, 0], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, + "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, + "up": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "up"}, + "down": {"uv": [0, 0, 8, 8], "texture": "#2"} + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "thirdperson_lefthand": { + "rotation": [75, 45, 0], + "translation": [0, 2.5, 0], + "scale": [0.375, 0.375, 0.375] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "firstperson_lefthand": { + "rotation": [0, 45, 0], + "translation": [-1.5, 0, 0], + "scale": [0.4, 0.4, 0.4] + }, + "ground": { + "translation": [0, 3, -1], + "scale": [0.25, 0.25, 0.25] + }, + "gui": { + "rotation": [30, 225, 0], + "translation": [0, -2.25, 0], + "scale": [0.625, 0.625, 0.625] + }, + "head": { + "translation": [0, 0, -8] + }, + "fixed": { + "translation": [0, 0, -0.75], + "scale": [0.5, 0.5, 0.5] + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_slab.json b/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_slab.json new file mode 100644 index 00000000..21a0303f --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_slab.json @@ -0,0 +1,3 @@ +{ + "parent": "createrailwaysnavigator:block/advanced_display_slab_cen" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/container_blue.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/container_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..28ce1258984b7ee5b23408556c5dbc30e006bf78 GIT binary patch literal 827 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 FV1m$CZ7~1< literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/gui.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/gui.png new file mode 100644 index 0000000000000000000000000000000000000000..e77d23eed7e946ed79cdfebbdbe1d021bbe63899 GIT binary patch literal 7821 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 diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/section_settings.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/section_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..afa8413047d997c6e22ee7c53901ec6f16ad3a7b GIT binary patch literal 6459 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 MpX;{!%jl*51}jAqn*aa+ literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/shadow.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..d408b047862c72b4f419cf2891115cb30dad8060 GIT binary patch literal 4514 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 delta 2063 zcmZWpeK^x?8@_+r%zTe(2#pD=&}hx(Bcl%%iJp>CEM`dgNNR*l9{H%}#q;a&k&-8g zNGl5!u|jG~^!cilCLT(yQIv1*dfxYVkM}s<`}*U)j_bPqIPdd5uTw}Fw#)bBO;Im0 zF5bDjf@j5gdTG?dW}vIDFM~1rPEnBNQ?UAB;SS-5H#Q+5DsHtFvzb$WwJPp|QJL*- z)-r#OdlpyaqJe~t6&txPLw>tAfR0VF4Ae-uGZ;-iJ=0G#Am%;1(U}=}QSa1&jy%@s zJB89`jjC@eH{?YXw6JZ_2i+^#SzaNTS6JtzjGbQi?NL5wM5iJy^Wo3pt-UprfYBuH zpe8M*50O2oeJ?e%@4iPxbcvujX}%%vYq#*J>&mIb^f$>?WAJjwjMpi^ zxjAK1+0{Nr{B*l^bP@uH2Q&Rmylv-?TWT?KPgckR@p-95#}IjUDB2Y<`9dG7wL z57$KNHg2(8zulOCJ85Ell(fch^|)qyfZuwte!cCQvrW|6KbA+oh1*E^|MY*lr;2uh zT9{AQBe0+QZc)JzZtTBU?D-_^QM>Et6g>3%!(C|eE<}y587*zhb?R*$v$%|R2u9eI z&vrJt-cK*vTh45fE5f8PWndvfwl#XTR*Uroo=6VTz(W~QQDk{({c2SSQZT?;`5%C}!5qXsTi> z|J)Y1nbp=~zH1^bc|&Md+tlKTzB-{$xSHQ!2{URR<_S!6<5N=)Zf{>RpVl^>cYS`M zG*}Rk*Iv;XTJSzTt-cm%wibeA2Kr{zmERQ$oF-{QrsuwfKsAxq$xcGI)rAK(->mjY zDeA=#dGAi#7#@ zHbs)9`YB4(%J+Hkv*Y}H7p&V=yIvo$4L}?ik`%2SpZ+$|CzU4P=wWh&LW3^^_2P*y z`DUid{_ywj6apb8CguR`0T0J-Zf-u`ikNJC(F{C1JbKy|XC`awR9|tx?%lf^m$S7* z5+N{sq9=>OU{-4K2k0pf3UX?EFslyDL;?)moITU4S}Hj_So|D{V~}2! zbaSFseDYTyoSmIJekk4z4h;OMD_F2q7YBV&D1K?7-y{-?A!C357vEnfA4BV_G$h>B z6EQ)_O=7hW=q331!ea)@VTA$KjP>J<@#(}j#$>f7beM^Q>b9?=isAB>&Th9QEi+C? zktQrRFi}qG@cP>yuT{StV13}PqrIOdKkM|nf>;;bSE<~P*xAH)=07w_E$A6NAb11A zWUfVG2=L|z?KG6g8dyV0p+MKv{v#MqGr5%iZTe0B$hM5TriGG9Kz?=iZhC}cbfV+A zrO-7LSf}KELHYek?$kJ7aQx=#TmpedC~FN44)zacDm9xV;rxnLEhpv{`&6~1@TkTw zzq<8|Bp~SzsK~f6rPQfY18WOGIPA=TM=Db!~PbQibJhb&CSQLk*ND8SH97>Xjv`~L_xeD;wnu0O?dTrs!STqXoE zE6DBN(tkjZPXcs?+hIqO9)L2f`0G%~`{Oz+wZHd{50w1>T)}+tW1cqadO)8z-ww>D zy~CZbR4JFrVlHse!%pcH{t(s0rNP4V^60oe`0w#GT7*WU U;^T+sd4Ja!n|(H2b!Q#_7nst7d;kCd diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/mod_icon.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/mod_icon.png index 632b7c7333ee5d84be020e2a75167bedb75abfb7..62ba06515beb59f0ba6bc4c8558ee75659fb6680 100644 GIT binary patch literal 7724 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 FV1nGonFjy> diff --git a/common/src/main/resources/createrailwaysnavigator.accesswidener b/common/src/main/resources/createrailwaysnavigator.accesswidener index d22c6f26..443dfac2 100644 --- a/common/src/main/resources/createrailwaysnavigator.accesswidener +++ b/common/src/main/resources/createrailwaysnavigator.accesswidener @@ -1,2 +1,4 @@ accessWidener v2 named -accessible class net/minecraft/client/gui/Font$StringRenderOutput \ No newline at end of file +accessible class net/minecraft/client/gui/Font$StringRenderOutput +accessible field net/minecraft/client/gui/chat/NarratorChatListener narrator Lcom/mojang/text2speech/Narrator; +accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen topPos I \ No newline at end of file diff --git a/common/src/main/resources/createrailwaysnavigator.mixins.json b/common/src/main/resources/createrailwaysnavigator.mixins.json index e70bfca5..e4c0bd9b 100644 --- a/common/src/main/resources/createrailwaysnavigator.mixins.json +++ b/common/src/main/resources/createrailwaysnavigator.mixins.json @@ -4,10 +4,19 @@ "compatibilityLevel": "JAVA_17", "minVersion": "0.8", "client": [ + "ScheduleScreenAccessor", + "ModularGuiLineBuilderAccessor", + "ScheduleScreenMixin" ], "mixins": [ - "ScheduleDataAccessor", - "MountedStorageManagerMixin" + "ScheduleRuntimeAccessor", + "TrainStatusAccessor", + "ControlsHandlerMixin", + "MountedStorageManagerMixin", + "ScheduleRuntimeMixin", + "GlobalTrainDisplayDataMixin", + "TrainMixin", + "ReloadableServerResourcesMixin" ], "injectors": { "defaultRequire": 1 diff --git a/common/src/main/resources/data/createrailwaysnavigator/crn_website/index.html b/common/src/main/resources/data/createrailwaysnavigator/crn_website/index.html new file mode 100644 index 00000000..80d22a9f --- /dev/null +++ b/common/src/main/resources/data/createrailwaysnavigator/crn_website/index.html @@ -0,0 +1,13 @@ + + + + + + Create Railways Navigator + + + +

HELLO CRN WORLD!

+ + + \ No newline at end of file diff --git a/common/src/main/resources/data/createrailwaysnavigator/crn_website/script.js b/common/src/main/resources/data/createrailwaysnavigator/crn_website/script.js new file mode 100644 index 00000000..dd6aceff --- /dev/null +++ b/common/src/main/resources/data/createrailwaysnavigator/crn_website/script.js @@ -0,0 +1,4 @@ +function test() { + let text = document.getElementById("text").innerText; + alert(text); +} \ No newline at end of file diff --git a/common/src/main/resources/data/createrailwaysnavigator/crn_website/style.css b/common/src/main/resources/data/createrailwaysnavigator/crn_website/style.css new file mode 100644 index 00000000..bbac77c3 --- /dev/null +++ b/common/src/main/resources/data/createrailwaysnavigator/crn_website/style.css @@ -0,0 +1,10 @@ + +.button { + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + } \ No newline at end of file diff --git a/common/src/main/resources/data/createrailwaysnavigator/loot_tables/blocks/advanced_display_slab.json b/common/src/main/resources/data/createrailwaysnavigator/loot_tables/blocks/advanced_display_slab.json new file mode 100644 index 00000000..22c4d72a --- /dev/null +++ b/common/src/main/resources/data/createrailwaysnavigator/loot_tables/blocks/advanced_display_slab.json @@ -0,0 +1,20 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1.0, + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:item", + "name": "createrailwaysnavigator:advanced_display_slab" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] + } \ No newline at end of file diff --git a/common/src/main/resources/data/createrailwaysnavigator/tags/blocks/advanced_displays.json b/common/src/main/resources/data/createrailwaysnavigator/tags/blocks/advanced_displays.json index 56791a79..2d5e4f03 100644 --- a/common/src/main/resources/data/createrailwaysnavigator/tags/blocks/advanced_displays.json +++ b/common/src/main/resources/data/createrailwaysnavigator/tags/blocks/advanced_displays.json @@ -6,6 +6,7 @@ "createrailwaysnavigator:advanced_display_panel", "createrailwaysnavigator:advanced_display_half_panel", "createrailwaysnavigator:advanced_display_small", - "createrailwaysnavigator:advanced_display_sloped" + "createrailwaysnavigator:advanced_display_sloped", + "createrailwaysnavigator:advanced_display_slab" ] } \ No newline at end of file diff --git a/common/src/main/resources/data/createrailwaysnavigator/tags/items/advanced_displays.json b/common/src/main/resources/data/createrailwaysnavigator/tags/items/advanced_displays.json index 07778035..21d1678f 100644 --- a/common/src/main/resources/data/createrailwaysnavigator/tags/items/advanced_displays.json +++ b/common/src/main/resources/data/createrailwaysnavigator/tags/items/advanced_displays.json @@ -6,6 +6,7 @@ "createrailwaysnavigator:advanced_display_panel", "createrailwaysnavigator:advanced_display_half_panel", "createrailwaysnavigator:advanced_display_sloped", - "createrailwaysnavigator:advanced_display_small" + "createrailwaysnavigator:advanced_display_small", + "createrailwaysnavigator:advanced_display_slab" ] } \ No newline at end of file diff --git a/common/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json b/common/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json index 059f2a61..060c572d 100644 --- a/common/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json +++ b/common/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json @@ -7,6 +7,7 @@ "createrailwaysnavigator:advanced_display_half_panel", "createrailwaysnavigator:advanced_display_small", "createrailwaysnavigator:advanced_display_sloped", + "createrailwaysnavigator:advanced_display_slab", "createrailwaysnavigator:train_station_clock" ] } \ No newline at end of file diff --git a/common/src/main/resources/icon.png b/common/src/main/resources/icon.png index 32e3e897a800dfa26f83bbb5ce3a9ccdcf3247c5..11fbdf9d0c4a2fdd533575188891ece5454495d4 100644 GIT binary patch literal 412476 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

BU9F22eygc)*6$?N;+RwbA>4`aA$d&RF&YW@e1D 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

RmzFTCQbi`>shE^ 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 aTt`6Cp;*;X8KoHLkFJ)XX05tI^#1|-u+~`s delta 41908 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 NBOeHIMFPOeiL=n;X!_hxHW11Mcxh8&R3=&WoO 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 S#I!{K0000 ModExtras.register()); + ClientWorldEvents.LOAD.register((mc, level) -> ModExtras.init()); } - ServerWorldEvents.LOAD.register((server, level) -> ModExtras.register()); - ModExtras.register(); + ServerWorldEvents.LOAD.register((server, level) -> ModExtras.init()); + ModExtras.init(); } } diff --git a/fabric/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json b/fabric/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json new file mode 100644 index 00000000..d140d663 --- /dev/null +++ b/fabric/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "BBB" + ], + "key": { + "B": { + "item": "createrailwaysnavigator:advanced_display_block" + } + }, + "result": { + "item": "createrailwaysnavigator:advanced_display_slab", + "count": 6 + } + } \ No newline at end of file diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index b6c4f67a..e42cd924 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -27,7 +27,7 @@ "fabric": "*", "minecraft": ">=1.19.2", "architectury": ">=6.5.85", - "dragonlib": ">=1.19.2-2.1.13", + "dragonlib": ">=1.19.2-2.2.16", "create": "*" }, "accessWidener": "createrailwaysnavigator.accesswidener" diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index 633a5c4d..abfbcae1 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -40,7 +40,7 @@ side = "BOTH" [[dependencies.createrailwaysnavigator]] modId = "dragonlib" mandatory = true -versionRange = "[1.19.2-2.1.13,)" +versionRange = "[1.19.2-2.2.16,)" ordering = "AFTER" side = "BOTH" diff --git a/forge/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json b/forge/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json new file mode 100644 index 00000000..d140d663 --- /dev/null +++ b/forge/src/main/resources/data/createrailwaysnavigator/recipes/advanced_display_slab.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "BBB" + ], + "key": { + "B": { + "item": "createrailwaysnavigator:advanced_display_block" + } + }, + "result": { + "item": "createrailwaysnavigator:advanced_display_slab", + "count": 6 + } + } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 83ffdc0c..38fadcf8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx6G org.gradle.parallel=true # Mod properties -mod_version = 0.5.4 +mod_version = 0.6.0 release_channel = beta maven_group = de.mrjulsen.crn archives_name = createrailwaysnavigator @@ -19,7 +19,7 @@ fabric_loader_version = 0.15.11 fabric_api_version = 0.77.0+1.19.2 forge_version = 1.19.2-43.3.13 -dragonlib_version = 2.1.13 +dragonlib_version = 2.2.16 modmenu_version=4.1.2 forge_config_api_port_version=4.2.11 create_fabric_version = 0.5.1-f-build.1416+mc1.19.2 diff --git a/icon.png b/icon.png index dbf0e24294dde05e6ab98e047cb88290182c8452..11fbdf9d0c4a2fdd533575188891ece5454495d4 100644 GIT binary patch literal 412476 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

BU9F22eygc)*6$?N;+RwbA>4`aA$d&RF&YW@e1D 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

RmzFTCQbi`>shE^ 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 aTt`6Cp;*;X8KoHLkFJ)XX05tI^#1|-u+~`s literal 84414 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 diff --git a/icon_256px.png b/icon_256px.png index 32e3e897a800dfa26f83bbb5ce3a9ccdcf3247c5..838d1695ade9d11dd6b998379ca77630f21ffb79 100644 GIT binary patch delta 36401 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 al>Y}UFQj`o#I!{K0000 Date: Sat, 12 Oct 2024 18:57:43 +0200 Subject: [PATCH 03/10] Fixed exception --- .vscode/settings.json | 3 ++- .../main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b6c0dc1..a1deeb97 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "commentTranslate.hover.enabled": false, - "java.compile.nullAnalysis.mode": "disabled" + "java.compile.nullAnalysis.mode": "disabled", + "java.configuration.updateBuildConfiguration": "automatic" } \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java b/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java index 17d2146a..cb7d3940 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java @@ -862,6 +862,9 @@ public static record DeparturesData(UUID stationTagId, UUID trainId) {} return new DeparturesData(nbt.getUUID("Tag"), nbt.getUUID("Train")); }, (player, in, temp, nbt, iteration) -> { try { + if (!GlobalSettings.getInstance().stationTagExists(in.stationTagId())) { + return false; + } StationTag tag = GlobalSettings.getInstance().getStationTag(in.stationTagId()).get(); ListTag list = new ListTag(); list.addAll(TrainUtils.getDeparturesAt(tag, in.trainId()).stream().map(x -> x.toNbt(true)).toList()); @@ -871,7 +874,7 @@ public static record DeparturesData(UUID stationTagId, UUID trainId) {} } return false; }, (hasMore, data, iteration, nbt) -> { - return nbt.getList(DataAccessorType.DEFAULT_NBT_DATA, Tag.TAG_COMPOUND).stream().map(x -> (ClientTrainStop)ClientTrainStop.fromNbt((CompoundTag)x)).toList(); + return nbt.contains(DataAccessorType.DEFAULT_NBT_DATA) ? nbt.getList(DataAccessorType.DEFAULT_NBT_DATA, Tag.TAG_COMPOUND).stream().map(x -> (ClientTrainStop)ClientTrainStop.fromNbt((CompoundTag)x)).toList() : List.of(); } )); From af7f228e5526edecf0e6e29e96017ff83545c16b Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Sat, 12 Oct 2024 19:03:17 +0200 Subject: [PATCH 04/10] Fixed accesswidener --- common/src/main/resources/createrailwaysnavigator.accesswidener | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/resources/createrailwaysnavigator.accesswidener b/common/src/main/resources/createrailwaysnavigator.accesswidener index 443dfac2..4d681a3f 100644 --- a/common/src/main/resources/createrailwaysnavigator.accesswidener +++ b/common/src/main/resources/createrailwaysnavigator.accesswidener @@ -1,4 +1,3 @@ accessWidener v2 named accessible class net/minecraft/client/gui/Font$StringRenderOutput -accessible field net/minecraft/client/gui/chat/NarratorChatListener narrator Lcom/mojang/text2speech/Narrator; accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen topPos I \ No newline at end of file From d9007d8459eb1f7ed85f281ed01db05988b21cc0 Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Sat, 12 Oct 2024 20:07:53 +0200 Subject: [PATCH 05/10] Fixed crash with keybinds --- .../main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java | 7 +++++-- .../main/java/de/mrjulsen/crn/event/ModClientEvents.java | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java b/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java index 0aaca184..4dbbe69d 100644 --- a/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java +++ b/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java @@ -8,6 +8,7 @@ import com.simibubi.create.foundation.item.TooltipHelper.Palette; import de.mrjulsen.crn.block.AdvancedDisplayBlock; +import de.mrjulsen.crn.client.input.ModKeys; import de.mrjulsen.crn.event.CRNClientEventsRegistryEvent; import de.mrjulsen.crn.event.CRNEventsManager; import de.mrjulsen.crn.event.ModClientEvents; @@ -71,6 +72,10 @@ public static void load() {} public static void init() { + CRNPlatformSpecific.registerConfig(); + if (Platform.getEnv() == EnvType.CLIENT) { + ModKeys.init(); + } ModBlocks.init(); ModItems.init(); ModBlockEntities.init(); @@ -88,8 +93,6 @@ public static void init() { ServerErrorPacket.class )); - CRNPlatformSpecific.registerConfig(); - ModCommonEvents.init(); if (Platform.getEnv() == EnvType.CLIENT) { ModClientEvents.init(); diff --git a/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java b/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java index 8951f20e..40c6c06d 100644 --- a/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java +++ b/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java @@ -2,7 +2,6 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.ClientWrapper; -import de.mrjulsen.crn.client.input.ModKeys; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.SavedRoutesManager; import de.mrjulsen.crn.data.navigation.ClientTrainListener; @@ -28,7 +27,6 @@ public class ModClientEvents { public static void init() { ClientLifecycleEvent.CLIENT_SETUP.register((mc) -> { - ModKeys.init(); ModDisplayTags.register(); }); From fc8d93a5851c9d87a04bed28d666c3ef93ab1b6e Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Sat, 12 Oct 2024 20:10:24 +0200 Subject: [PATCH 06/10] Fixed crash on server with fabric --- .../de/mrjulsen/crn/client/ClientWrapper.java | 100 ++++++++++++++++++ .../condition/DynamicDelayCondition.java | 49 +-------- .../instruction/ResetTimingsInstruction.java | 26 +---- .../instruction/TravelSectionInstruction.java | 36 +------ .../crn/mixin/ScheduleScreenMixin.java | 10 +- 5 files changed, 109 insertions(+), 112 deletions(-) diff --git a/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java b/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java index 7f3637aa..7385ce21 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java @@ -4,21 +4,42 @@ import java.util.function.Supplier; import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.content.trains.schedule.ScheduleScreen; +import com.simibubi.create.content.trains.schedule.condition.TimedWaitCondition.TimeUnit; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import com.simibubi.create.foundation.utility.Lang; +import com.simibubi.create.foundation.utility.Pair; import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.gui.NavigatorToast; import de.mrjulsen.crn.client.gui.screen.AdvancedDisplaySettingsScreen; import de.mrjulsen.crn.client.gui.screen.NavigatorScreen; import de.mrjulsen.crn.client.gui.screen.TrainDebugScreen; +import de.mrjulsen.crn.client.gui.screen.TrainSectionSettingsScreen; +import de.mrjulsen.crn.client.gui.widgets.ResizableButton; import de.mrjulsen.crn.client.lang.ELanguage; import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; +import de.mrjulsen.crn.data.schedule.instruction.ResetTimingsInstruction; +import de.mrjulsen.crn.data.schedule.instruction.TravelSectionInstruction; +import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; +import de.mrjulsen.crn.mixin.ScheduleScreenAccessor; import de.mrjulsen.crn.network.packets.stc.ServerErrorPacket; +import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; import de.mrjulsen.mcdragonlib.util.TextUtils; import dev.architectury.networking.NetworkManager.PacketContext; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.components.MultiLineLabel; @@ -95,4 +116,83 @@ public static void showTrainDebugScreen() { DLScreen.setScreen(new TrainDebugScreen(null)); }); } + + @SuppressWarnings("resource") + public static void initScheduleSectionInstruction(TravelSectionInstruction instruction, ModularGuiLineBuilder builder) { + + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + + ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 121, 16, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction." + instruction.getId().getPath() + ".configure"), + (b) -> { + if (Minecraft.getInstance().screen instanceof ScheduleScreen scheduleScreen) { + ((ScheduleScreenAccessor)scheduleScreen).crn$getOnEditorClose().accept(true); + builder.customArea(0, 0).speechBubble(); + Minecraft.getInstance().setScreen(new TrainSectionSettingsScreen(scheduleScreen, instruction.getData())); + } + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + int j = isActive() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x + width / 2, y + (height - 8) / 2, this.getMessage(), j, EAlignment.CENTER, true); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "config_btn")); + } + + public static void initResetTimingsInstruction(ResetTimingsInstruction instruction, ModularGuiLineBuilder builder) { + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 16, 16, TextUtils.empty(), + (b) -> { + Util.getPlatform().openUri(Constants.HELP_PAGE_SCHEDULED_TIMES_AND_REAL_TIME); + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + ModGuiIcons.HELP.render(graphics, x, y); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); + } + + public static void initDynamicDelayCondition(DynamicDelayCondition condition, ModularGuiLineBuilder builder) { + + builder.addScrollInput(0, 26, (i, l) -> { + i.titled(Lang.translateDirect("generic.duration")) + .withShiftStep(15) + .withRange(0, 121); + i.lockedTooltipX = -15; + i.lockedTooltipY = 35; + }, "Value"); + + builder.addScrollInput(26, 26, (i, l) -> { + i.titled(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.condition." + condition.getId().getPath() + ".min_duration")) + .withShiftStep(15) + .withRange(0, 121); + i.lockedTooltipX = -15; + i.lockedTooltipY = 35; + }, DynamicDelayCondition.NBT_MIN); + + builder.addSelectionScrollInput(52, 58, (i, l) -> { + i.forOptions(TimeUnit.translatedOptions()) + .titled(Lang.translateDirect("generic.timeUnit")); + }, "TimeUnit"); + + + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + ResizableButton btn = new ResizableButton(accessor.crn$getX() + 110, accessor.crn$getY() - 4, 16, 16, TextUtils.empty(), + (b) -> { + Util.getPlatform().openUri(Constants.HELP_PAGE_DYNAMIC_DELAYS); + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + ModGuiIcons.HELP.render(graphics, x, y); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); + } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java index b8889efc..48516f9c 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/DynamicDelayCondition.java @@ -4,7 +4,6 @@ import java.util.Optional; import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.content.trains.entity.Train; import com.simibubi.create.content.trains.schedule.condition.ScheduledDelay; import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; @@ -12,24 +11,16 @@ import com.simibubi.create.foundation.utility.Lang; import com.simibubi.create.foundation.utility.Pair; -import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.gui.widgets.ResizableButton; +import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.data.train.TrainData; import de.mrjulsen.crn.data.train.TrainListener; import de.mrjulsen.crn.data.train.TrainPrediction; -import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; -import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.util.TextUtils; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.ChatFormatting; -import net.minecraft.Util; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; @@ -39,7 +30,7 @@ public class DynamicDelayCondition extends ScheduledDelay { - private static final String NBT_MIN = "Min"; + public static final String NBT_MIN = "Min"; public DynamicDelayCondition() { super(); @@ -108,40 +99,6 @@ public int minWaitTicks() { @Override @Environment(EnvType.CLIENT) public void initConfigurationWidgets(ModularGuiLineBuilder builder) { - builder.addScrollInput(0, 26, (i, l) -> { - i.titled(Lang.translateDirect("generic.duration")) - .withShiftStep(15) - .withRange(0, 121); - i.lockedTooltipX = -15; - i.lockedTooltipY = 35; - }, "Value"); - - builder.addScrollInput(26, 26, (i, l) -> { - i.titled(TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.condition." + getId().getPath() + ".min_duration")) - .withShiftStep(15) - .withRange(0, 121); - i.lockedTooltipX = -15; - i.lockedTooltipY = 35; - }, NBT_MIN); - - builder.addSelectionScrollInput(52, 58, (i, l) -> { - i.forOptions(TimeUnit.translatedOptions()) - .titled(Lang.translateDirect("generic.timeUnit")); - }, "TimeUnit"); - - - ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; - ResizableButton btn = new ResizableButton(accessor.crn$getX() + 110, accessor.crn$getY() - 4, 16, 16, TextUtils.empty(), - (b) -> { - Util.getPlatform().openUri(Constants.HELP_PAGE_DYNAMIC_DELAYS); - }) { - @Override - public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { - Graphics graphics = new Graphics(poseStack); - DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); - ModGuiIcons.HELP.render(graphics, x, y); - } - }; - accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); + ClientWrapper.initDynamicDelayCondition(this, builder); } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java index ca894572..a5b54bac 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java @@ -2,29 +2,20 @@ import java.util.List; -import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.content.trains.entity.Train; import com.simibubi.create.content.trains.schedule.ScheduleRuntime; import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction; import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; import com.simibubi.create.foundation.utility.Pair; -import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.gui.widgets.ResizableButton; +import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.data.train.TrainData; -import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; -import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.ChatFormatting; -import net.minecraft.Util; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; @@ -59,20 +50,7 @@ public List getTitleAs(String type) { @Override @Environment(EnvType.CLIENT) public void initConfigurationWidgets(ModularGuiLineBuilder builder) { - - ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; - ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 16, 16, TextUtils.empty(), - (b) -> { - Util.getPlatform().openUri(Constants.HELP_PAGE_SCHEDULED_TIMES_AND_REAL_TIME); - }) { - @Override - public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { - Graphics graphics = new Graphics(poseStack); - DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); - ModGuiIcons.HELP.render(graphics, x, y); - } - }; - accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); + ClientWrapper.initResetTimingsInstruction(this, builder); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java index 86fd2467..114e8eb2 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java @@ -3,36 +3,23 @@ import java.util.ArrayList; import java.util.List; -import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.content.trains.entity.Train; import com.simibubi.create.content.trains.schedule.ScheduleRuntime; -import com.simibubi.create.content.trains.schedule.ScheduleScreen; import com.simibubi.create.content.trains.schedule.destination.ScheduleInstruction; import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; import com.simibubi.create.foundation.utility.Pair; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.gui.screen.TrainSectionSettingsScreen; -import de.mrjulsen.crn.client.gui.widgets.ResizableButton; +import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.data.storage.GlobalSettings; import de.mrjulsen.crn.data.train.TrainData; import de.mrjulsen.crn.data.train.TrainTravelSection; -import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; -import de.mrjulsen.crn.mixin.ScheduleScreenAccessor; import de.mrjulsen.crn.registry.ModBlocks; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; -import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; -import de.mrjulsen.mcdragonlib.client.util.Graphics; -import de.mrjulsen.mcdragonlib.client.util.GuiUtils; -import de.mrjulsen.mcdragonlib.core.EAlignment; import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; @@ -89,27 +76,8 @@ public List getTitleAs(String type) { /** HERE BE DRAGONS! This code is very illegal, but it works... */ @Override @Environment(EnvType.CLIENT) - @SuppressWarnings("resource") public void initConfigurationWidgets(ModularGuiLineBuilder builder) { - ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; - - ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 121, 16, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction." + getId().getPath() + ".configure"), - (b) -> { - if (Minecraft.getInstance().screen instanceof ScheduleScreen scheduleScreen) { - ((ScheduleScreenAccessor)scheduleScreen).crn$getOnEditorClose().accept(true); - builder.customArea(0, 0).speechBubble(); - Minecraft.getInstance().setScreen(new TrainSectionSettingsScreen(scheduleScreen, data)); - } - }) { - @Override - public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { - Graphics graphics = new Graphics(poseStack); - DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); - int j = isActive() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; - GuiUtils.drawString(graphics, Minecraft.getInstance().font, x + width / 2, y + (height - 8) / 2, this.getMessage(), j, EAlignment.CENTER, true); - } - }; - accessor.crn$getTarget().add(Pair.of(btn, "config_btn")); + ClientWrapper.initScheduleSectionInstruction(this, builder); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java index 3acb5b6a..6d737ab3 100644 --- a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java @@ -4,22 +4,14 @@ import java.util.List; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.At.Shift; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import com.simibubi.create.content.trains.schedule.DestinationSuggestions; import com.simibubi.create.content.trains.schedule.IScheduleInput; import com.simibubi.create.content.trains.schedule.ScheduleScreen; import com.simibubi.create.foundation.utility.IntAttached; import de.mrjulsen.crn.data.StationTag; import de.mrjulsen.crn.data.storage.GlobalSettings; -import de.mrjulsen.crn.data.schedule.instruction.ICustomSuggestionsInstruction; import de.mrjulsen.crn.data.schedule.instruction.IStationTagInstruction; import de.mrjulsen.crn.data.schedule.instruction.ITrainNameInstruction; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; @Mixin(ScheduleScreen.class) @@ -37,6 +29,7 @@ public int getTopPos() { return ((AbstractContainerScreen)(Object)this).topPos; } + /* @SuppressWarnings("resource") @Inject(method = "updateEditorSubwidgets", remap = false, at = @At(value = "INVOKE", shift = Shift.AFTER, target = "Lcom/simibubi/create/foundation/gui/ModularGuiLine;loadValues(Lnet/minecraft/nbt/CompoundTag;Ljava/util/function/Consumer;Ljava/util/function/Consumer;)V"), cancellable = true) public void onUpdateEditorSubwidgets(IScheduleInput field, CallbackInfo ci) { @@ -52,6 +45,7 @@ public void onUpdateEditorSubwidgets(IScheduleInput field, CallbackInfo ci) { } } + */ public List> onGetViableStations(IScheduleInput field) { From c6d3fc5dd87a2c65414a15fd79c45abc4953a39e Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Sat, 14 Dec 2024 19:57:57 +0100 Subject: [PATCH 07/10] 1.19.2 Port --- .gitignore | 5 +- .vscode/settings.json | 3 +- build.gradle | 58 ++- common/build.gradle | 7 +- .../de/mrjulsen/crn/CRNPlatformSpecific.java | 1 - .../main/java/de/mrjulsen/crn/Constants.java | 4 + .../mrjulsen/crn/CreateRailwaysNavigator.java | 7 +- .../block/AbstractAdvancedDisplayBlock.java | 32 +- .../crn/block/TrainStationClockBlock.java | 2 +- .../AdvancedDisplayBlockEntity.java | 199 +++---- .../blockentity/IColorableBlockEntity.java | 6 - .../TrainStationClockBlockEntity.java | 7 +- .../block/display/AdvancedDisplaySource.java | 86 +--- .../block/display/AdvancedDisplayTarget.java | 32 +- .../properties/AbstractDisplaySettings.java | 36 ++ .../AdvancedDisplaySettingsData.java | 53 ++ .../properties/BasicDisplaySettings.java | 60 +++ .../DepartureBoardDisplayTableSettings.java | 148 ++++++ .../display/properties/IDisplaySettings.java | 35 ++ .../PassengerInformationDetailedSettings.java | 147 ++++++ ...engerInformationScrollingTextSettings.java | 91 ++++ .../PlatformDisplayFocusSettings.java | 151 ++++++ .../PlatformDisplayScrollingTextSettings.java | 49 ++ .../PlatformDisplayTableSettings.java | 112 ++++ .../TrainDestinationCompactSettings.java | 48 ++ .../TrainDestinationDetailedSettings.java | 61 +++ .../TrainDestinationExtendedSettings.java | 48 ++ .../components/GuiBuilderWrapper.java | 372 +++++++++++++ .../components/ICarriageIndexSetting.java | 36 ++ .../properties/components/IColorSetting.java | 39 ++ .../components/ICustomTextWidthSetting.java | 16 + .../components/IPlatformWidthSetting.java | 36 ++ .../components/IShowArrivalSetting.java | 33 ++ .../components/IShowExitDirectionSetting.java | 32 ++ .../components/IShowLineColorSetting.java | 33 ++ .../components/IShowNextConnections.java | 32 ++ .../components/IShowTimeAndDateSetting.java | 32 ++ .../components/IShowTrainStatsSetting.java | 32 ++ .../components/ITimeDisplaySetting.java | 33 ++ .../components/ITrainNameWidthSetting.java | 43 ++ .../components/ITrainTextSetting.java | 76 +++ .../crn/block/properties/EDisplayType.java | 7 +- .../crn/block/properties/ETimeDisplay.java | 47 ++ .../crn/client/AdvancedDisplaysRegistry.java | 121 +++-- .../de/mrjulsen/crn/client/ClientWrapper.java | 68 ++- .../ber/AdvancedDisplayRenderInstance.java | 9 +- .../client/ber/TrainStationClockRenderer.java | 8 +- .../AbstractAdvancedDisplayRenderer.java | 22 + .../ber/variants/BERDepartureBoardTable.java | 487 ++++++++++++++++++ .../crn/client/ber/variants/BERError.java | 4 +- .../variants/BERPassengerInfoInformative.java | 97 ++-- .../ber/variants/BERPassengerInfoSimple.java | 45 +- .../ber/variants/BERPlatformDetailed.java | 85 +-- .../ber/variants/BERPlatformInformative.java | 185 +++++-- .../ber/variants/BERPlatformSimple.java | 41 +- .../variants/BERTrainDestinationDetailed.java | 39 +- .../BERTrainDestinationInformative.java | 26 +- .../variants/BERTrainDestinationSimple.java | 34 +- .../crn/client/gui/CreateDynamicWidgets.java | 25 +- .../mrjulsen/crn/client/gui/ModGuiIcons.java | 12 +- .../gui/overlay/RouteDetailsOverlay.java | 29 +- .../overlay/pages/NextConnectionsPage.java | 6 +- .../gui/overlay/pages/RouteOverviewPage.java | 4 +- .../gui/overlay/pages/TransferPage.java | 12 +- .../client/gui/overlay/pages/WelcomePage.java | 6 +- .../gui/screen/AbstractNavigatorScreen.java | 2 +- .../screen/AdvancedDisplaySettingsScreen.java | 338 ++++++++---- .../gui/screen/GlobalSettingsScreen.java | 12 +- .../client/gui/screen/NavigatorScreen.java | 2 +- .../client/gui/screen/RouteDetailsScreen.java | 9 +- ...rneySreen.java => TrainJourneyScreen.java} | 6 +- .../screen/TrainSectionSettingsScreen.java | 4 +- .../screen/TrainSeparationSettingsScreen.java | 178 +++++++ .../gui/widgets/AbstractFlyoutWidget.java | 5 +- .../widgets/AbstractNotificationPopup.java | 4 +- .../crn/client/gui/widgets/CRNListBox.java | 15 +- .../client/gui/widgets/ColorPickerWidget.java | 36 +- .../client/gui/widgets/ColorSlotWidget.java | 83 +++ .../gui/widgets/DLCreateScrollInput.java | 168 ++++++ .../widgets/DLCreateSelectionScrollInput.java | 101 +++- .../client/gui/widgets/DLCreateTextBox.java | 36 ++ .../crn/client/gui/widgets/DLLabel.java | 116 +++++ .../client/gui/widgets/IconSlotWidget.java | 31 ++ .../gui/widgets/RouteDetailsViewer.java | 9 +- .../crn/client/gui/widgets/RouteViewer.java | 8 +- .../crn/client/gui/widgets/RouteWidget.java | 16 +- .../client/gui/widgets/SavedRouteWidget.java | 8 +- .../client/gui/widgets/SavedRoutesViewer.java | 10 +- .../gui/widgets/StationDeparturesViewer.java | 8 +- .../gui/widgets/StationDeparturesWidget.java | 10 +- .../client/gui/widgets/TrainDebugViewer.java | 13 +- .../create/CreateTimeSelectionWidget.java | 6 +- .../FlyoutAdvancedSearchsettingsWidget.java | 5 - .../widgets/flyouts/FlyoutColorPicker.java | 5 +- .../flyouts/FlyoutDepartureInWidget.java | 2 +- .../widgets/flyouts/FlyoutScrollInput.java | 72 +++ .../flyouts/FlyoutTrainGroupsWidget.java | 2 +- .../flyouts/FlyoutTransferTimeWidget.java | 2 +- .../widgets/modular/GuiBuilderContext.java | 19 + .../widgets/modular/ModularWidgetBuilder.java | 63 +++ .../modular/ModularWidgetContainer.java | 112 ++++ .../widgets/modular/ModularWidgetLine.java | 67 +++ .../options/AbstractDataListEntry.java | 4 +- .../gui/widgets/options/DLOptionsList.java | 12 +- .../widgets/options/DataListContainer.java | 4 +- .../gui/widgets/options/NewEntryWidget.java | 4 +- .../gui/widgets/options/OptionEntry.java | 4 +- .../RouteDetailsTransferWidget.java | 8 +- .../routedetails/RoutePartEntryWidget.java | 12 +- .../RoutePartTrainDetailsWidget.java | 11 +- .../widgets/routedetails/RoutePartWidget.java | 11 +- .../{ELanguage.java => CustomLanguage.java} | 14 +- .../de/mrjulsen/crn/cmd/DebugCommand.java | 13 +- .../mrjulsen/crn/config/ModClientConfig.java | 6 +- .../mrjulsen/crn/config/ModCommonConfig.java | 7 + .../crn/data/ISaveableNavigatorData.java | 2 +- .../java/de/mrjulsen/crn/data/StationTag.java | 19 +- .../java/de/mrjulsen/crn/data/TrainInfo.java | 4 +- .../de/mrjulsen/crn/data/UserSettings.java | 3 +- .../crn/data/navigation/ClientRoute.java | 82 +-- .../crn/data/navigation/ClientRoutePart.java | 112 +++- .../data/navigation/ClientTrainListener.java | 33 +- .../crn/data/navigation/NavigatableGraph.java | 2 +- .../de/mrjulsen/crn/data/navigation/Node.java | 9 +- .../mrjulsen/crn/data/navigation/Route.java | 13 +- .../crn/data/navigation/RoutePart.java | 10 +- .../crn/data/navigation/TrainSchedule.java | 38 +- .../IConditionsRequiresInstruction.java | 4 + .../data/schedule/INavigationExtension.java | 10 + .../condition/IDelayedWaitCondition.java | 18 + .../condition/TrainSeparationCondition.java | 103 ++++ .../instruction/ResetTimingsInstruction.java | 2 + .../instruction/TravelSectionInstruction.java | 2 +- .../crn/data/storage/GlobalSettings.java | 92 ++-- .../data/train/StationDepartureHistory.java | 369 +++++++++++++ .../de/mrjulsen/crn/data/train/TrainData.java | 131 ++++- .../crn/data/train/TrainListener.java | 70 ++- .../crn/data/train/TrainPrediction.java | 21 +- .../mrjulsen/crn/data/train/TrainStatus.java | 63 ++- .../de/mrjulsen/crn/data/train/TrainStop.java | 8 +- .../crn/data/train/TrainTravelSection.java | 46 +- .../mrjulsen/crn/data/train/TrainUtils.java | 197 +++++-- .../train/portable/BasicTrainDisplayData.java | 53 +- .../portable/NextConnectionsDisplayData.java | 15 +- .../train/portable/StationDisplayData.java | 18 +- .../data/train/portable/TrainDisplayData.java | 17 +- .../train/portable/TrainStopDisplayData.java | 4 +- .../de/mrjulsen/crn/debug/DebugOverlay.java | 4 +- .../mrjulsen/crn/event/ModClientEvents.java | 13 +- .../mrjulsen/crn/event/ModCommonEvents.java | 24 +- .../de/mrjulsen/crn/item/NavigatorItem.java | 23 + .../java/de/mrjulsen/crn/mixin/ItemMixin.java | 36 ++ .../mrjulsen/crn/mixin/NavigationMixin.java | 60 +++ .../mixin/ReloadableServerResourcesMixin.java | 36 -- .../crn/mixin/ScheduleEntryMixin.java | 22 + .../crn/mixin/ScheduleScreenMixin.java | 2 +- .../crn/mixin/StationBlockEntityMixin.java | 109 ++++ .../cts/AdvancedDisplayUpdatePacket.java | 22 +- .../mrjulsen/crn/registry/ClientWrapper.java | 7 +- .../crn/registry/ModAccessorTypes.java | 84 ++- .../de/mrjulsen/crn/registry/ModBlocks.java | 21 +- .../crn/registry/ModDisplayTypes.java | 94 ++-- .../de/mrjulsen/crn/registry/ModSchedule.java | 2 + .../crn/registry/ModTrainStatusInfos.java | 45 +- .../de/mrjulsen/crn/util/IListenable.java | 26 +- .../java/de/mrjulsen/crn/util/ModUtils.java | 23 +- .../de/mrjulsen/crn/web/SimpleWebServer.java | 274 ---------- .../web/WebsitePreparableReloadListener.java | 57 -- .../createrailwaysnavigator/lang/bar.json | 69 +-- .../createrailwaysnavigator/lang/de_de.json | 91 +++- .../createrailwaysnavigator/lang/en_us.json | 84 ++- .../createrailwaysnavigator/lang/es_es.json | 69 ++- .../createrailwaysnavigator/lang/eu_es.json | 315 +++++++++++ .../createrailwaysnavigator/lang/fr_fr.json | 77 +-- .../createrailwaysnavigator/lang/it_it.json | 315 +++++++++++ .../createrailwaysnavigator/lang/ja_jp.json | 315 +++++++++++ .../createrailwaysnavigator/lang/ko_kr.json | 66 +-- .../createrailwaysnavigator/lang/nl_nl.json | 169 ++++-- .../createrailwaysnavigator/lang/pl_pl.json | 229 +++++--- .../createrailwaysnavigator/lang/pt_br.json | 315 +++++++++++ .../createrailwaysnavigator/lang/pt_pt.json | 73 +-- .../createrailwaysnavigator/lang/ru_ru.json | 99 ++-- .../createrailwaysnavigator/lang/sv_se.json | 181 ++++--- .../createrailwaysnavigator/lang/sxu.json | 160 ++++-- .../createrailwaysnavigator/lang/uk_ua.json | 315 +++++++++++ .../createrailwaysnavigator/lang/zh_cn.json | 164 ++++-- .../models/block/advanced_display_block.json | 11 +- .../block/advanced_display_block_double.json | 14 +- .../models/block/advanced_display_cen.json | 13 +- .../block/advanced_display_double_cen.json | 14 +- .../block/advanced_display_double_neg.json | 14 +- .../block/advanced_display_double_pos.json | 14 +- .../advanced_display_half_panel_cen_cen.json | 13 +- .../advanced_display_half_panel_cen_neg.json | 13 +- .../advanced_display_half_panel_cen_pos.json | 13 +- ...ced_display_half_panel_double_cen_cen.json | 16 +- ...ced_display_half_panel_double_cen_neg.json | 16 +- ...ced_display_half_panel_double_cen_pos.json | 16 +- ...ced_display_half_panel_double_neg_cen.json | 16 +- ...ced_display_half_panel_double_neg_neg.json | 16 +- ...ced_display_half_panel_double_neg_pos.json | 16 +- ...ced_display_half_panel_double_pos_cen.json | 16 +- ...ced_display_half_panel_double_pos_neg.json | 16 +- ...ced_display_half_panel_double_pos_pos.json | 16 +- .../advanced_display_half_panel_neg_cen.json | 13 +- .../advanced_display_half_panel_neg_neg.json | 13 +- .../advanced_display_half_panel_neg_pos.json | 13 +- .../advanced_display_half_panel_pos_cen.json | 13 +- .../advanced_display_half_panel_pos_neg.json | 13 +- .../advanced_display_half_panel_pos_pos.json | 13 +- .../models/block/advanced_display_neg.json | 11 +- .../block/advanced_display_panel_cen.json | 11 +- .../advanced_display_panel_double_cen.json | 14 +- .../advanced_display_panel_double_neg.json | 14 +- .../advanced_display_panel_double_pos.json | 14 +- .../block/advanced_display_panel_neg.json | 11 +- .../block/advanced_display_panel_pos.json | 11 +- .../models/block/advanced_display_pos.json | 11 +- .../block/advanced_display_slab_cen.json | 14 +- .../advanced_display_slab_double_cen.json | 17 +- .../advanced_display_slab_double_neg.json | 17 +- .../advanced_display_slab_double_pos.json | 17 +- .../block/advanced_display_slab_neg.json | 14 +- .../block/advanced_display_slab_pos.json | 14 +- .../models/block/advanced_display_sloped.json | 25 +- .../block/advanced_display_small_cen_cen.json | 13 +- .../block/advanced_display_small_cen_neg.json | 13 +- .../block/advanced_display_small_cen_pos.json | 13 +- ...advanced_display_small_double_cen_cen.json | 16 +- ...advanced_display_small_double_cen_neg.json | 16 +- ...advanced_display_small_double_cen_pos.json | 16 +- ...advanced_display_small_double_neg_cen.json | 16 +- ...advanced_display_small_double_neg_neg.json | 16 +- ...advanced_display_small_double_neg_pos.json | 16 +- ...advanced_display_small_double_pos_cen.json | 16 +- ...advanced_display_small_double_pos_neg.json | 16 +- ...advanced_display_small_double_pos_pos.json | 16 +- .../block/advanced_display_small_neg_cen.json | 13 +- .../block/advanced_display_small_neg_neg.json | 13 +- .../block/advanced_display_small_neg_pos.json | 13 +- .../block/advanced_display_small_pos_cen.json | 13 +- .../block/advanced_display_small_pos_neg.json | 13 +- .../block/advanced_display_small_pos_pos.json | 13 +- .../models/item/advanced_display_sloped.json | 27 +- .../textures/block/advanced_display.png | Bin 946 -> 4433 bytes .../textures/block/advanced_display.xcf | Bin 0 -> 293721 bytes .../block/advanced_display_border.png | Bin 0 -> 824 bytes .../advanced_display_border_connected.png | Bin 0 -> 10818 bytes .../block/advanced_display_connected.png | Bin 4995 -> 10508 bytes .../textures/block/advanced_display_small.png | Bin 4485 -> 4872 bytes .../textures/block/advanced_display_small.xcf | Bin 0 -> 10498 bytes .../advanced_display_small_border.png} | Bin 5328 -> 4604 bytes ...vanced_display_small_border_connected.png} | Bin 5776 -> 5151 bytes .../advanced_display_small_connected.png | Bin 5134 -> 5455 bytes .../gui/advanced_display_settings.png | Bin 6204 -> 0 bytes .../textures/gui/gui.png | Bin 7821 -> 8098 bytes .../textures/gui/icons.png | Bin 9953 -> 11076 bytes .../textures/gui/navigator.png | Bin 6859 -> 0 bytes .../textures/gui/settings_widgets.png | Bin 9636 -> 0 bytes .../createrailwaysnavigator.mixins.json | 9 +- fabric/build.gradle | 2 +- .../createrailwaysnavigator.accesswidener | 3 + fabric/src/main/resources/fabric.mod.json | 45 +- forge/build.gradle | 4 +- .../forge/CRNPlatformSpecificClientImpl.java | 3 - forge/src/main/resources/META-INF/mods.toml | 50 +- forge/src/main/resources/pack.mcmeta | 5 +- gradle.properties | 26 +- settings.gradle | 4 +- update.json | 4 +- 270 files changed, 9723 insertions(+), 2381 deletions(-) delete mode 100644 common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/AbstractDisplaySettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/AdvancedDisplaySettingsData.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/BasicDisplaySettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/DepartureBoardDisplayTableSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/IDisplaySettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationDetailedSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationScrollingTextSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayFocusSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayScrollingTextSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayTableSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationCompactSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationDetailedSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationExtendedSettings.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/GuiBuilderWrapper.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICarriageIndexSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IColorSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICustomTextWidthSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IPlatformWidthSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowArrivalSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowExitDirectionSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowLineColorSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowNextConnections.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTimeAndDateSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTrainStatsSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITimeDisplaySetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainNameWidthSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainTextSetting.java create mode 100644 common/src/main/java/de/mrjulsen/crn/block/properties/ETimeDisplay.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERDepartureBoardTable.java rename common/src/main/java/de/mrjulsen/crn/client/gui/screen/{TrainJourneySreen.java => TrainJourneyScreen.java} (89%) create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorSlotWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateScrollInput.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateTextBox.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLLabel.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IconSlotWidget.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutScrollInput.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/GuiBuilderContext.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetBuilder.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetContainer.java create mode 100644 common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetLine.java rename common/src/main/java/de/mrjulsen/crn/client/lang/{ELanguage.java => CustomLanguage.java} (83%) create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/IConditionsRequiresInstruction.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/INavigationExtension.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/condition/IDelayedWaitCondition.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java create mode 100644 common/src/main/java/de/mrjulsen/crn/data/train/StationDepartureHistory.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ItemMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/NavigationMixin.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/ScheduleEntryMixin.java create mode 100644 common/src/main/java/de/mrjulsen/crn/mixin/StationBlockEntityMixin.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java delete mode 100644 common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/eu_es.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/it_it.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/ja_jp.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/pt_br.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/lang/uk_ua.json create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display.xcf create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_border.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_border_connected.png create mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_small.xcf rename common/src/main/resources/assets/createrailwaysnavigator/textures/{gui/settings.png => block/advanced_display_small_border.png} (53%) rename common/src/main/resources/assets/createrailwaysnavigator/textures/{gui/route_details.png => block/advanced_display_small_border_connected.png} (51%) delete mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/advanced_display_settings.png delete mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/navigator.png delete mode 100644 common/src/main/resources/assets/createrailwaysnavigator/textures/gui/settings_widgets.png create mode 100644 fabric/src/main/resources/createrailwaysnavigator.accesswidener diff --git a/.gitignore b/.gitignore index 93a3a465..9de9901b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,12 +9,13 @@ out/ output/ bin/ libs/ +doc/ .classpath .project .idea/ classes/ .metadata -.vscode .settings -*.launch \ No newline at end of file +*.launch +.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json index a1deeb97..1b6c0dc1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { "commentTranslate.hover.enabled": false, - "java.compile.nullAnalysis.mode": "disabled", - "java.configuration.updateBuildConfiguration": "automatic" + "java.compile.nullAnalysis.mode": "disabled" } \ No newline at end of file diff --git a/build.gradle b/build.gradle index eaa5f824..699af0f6 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ architectury { allprojects { group = rootProject.maven_group - version = "$rootProject.minecraft_version-$rootProject.mod_version" + version = "$rootProject.minecraft_version${rootProject.release_channel != "release" ? "-$rootProject.release_channel" : ""}-$rootProject.mod_version" } subprojects { @@ -23,33 +23,25 @@ subprojects { } repositories { + maven { url = "file://${System.getProperty('user.home')}/.m2/github/modsrepo/maven" } // Local maven { url = "https://raw.githubusercontent.com/MisterJulsen/modsrepo/main/maven" } // DragonLib maven { url = "https://raw.githubusercontent.com/Fuzss/modresources/main/maven/" } // Forge Config API maven { url = "https://maven.parchmentmc.org" } // ParchmentMC maven { url = "https://maven.terraformersmc.com/" } // ModMenu + maven { url = "https://maven.tterrag.com/" } // Flywheel maven { url = "https://maven.shedaniel.me/" } // Cloth Config, REI maven { url = "https://maven.blamejared.com/" } // JEI maven { url = "https://maven.quiltmc.org/repository/release" } // Quilt Mappings maven { url = "https://api.modrinth.com/maven" } // LazyDFU - maven { url = "https://maven.terraformersmc.com/releases/" } // Mod Menu maven { url = "https://mvn.devos.one/snapshots/" } // Create, Porting Lib, Forge Tags, Milk Lib, Registrate maven { url = "https://maven.jamieswhiteshirt.com/libs-release" } // Reach Entity Attributes maven { url = "https://jitpack.io/" } // Mixin Extras, Fabric ASM - + maven { url = "https://maven.tterrag.com/" } // Flywheel maven { url = "https://cursemaven.com" content { includeGroup "curse.maven" } } - - maven { url = "https://maven.tterrag.com/" // Create Forge, Registrate Forge, Flywheel - content { - includeGroup("com.jozufozu.flywheel") - includeGroup("com.tterrag.registrate") - includeGroup("com.simibubi.create") - } - } - } dependencies { @@ -60,6 +52,36 @@ subprojects { } } + processResources { + def expandProps = [ + "version": version, + "minecraft_version": rootProject.minecraft_version, + "issues": rootProject.issues, + "source": rootProject.source, + "license": rootProject.license, + "modid": rootProject.archives_name, + "display_name": rootProject.display_name, + "homepage": rootProject.homepage, + "authors": rootProject.authors, + "description": rootProject.description, + "icon": rootProject.icon, + "discord": rootProject.discord, + "next_unsupported_minecraft_version": rootProject.next_unsupported_minecraft_version, + "dragonlib_version": "$rootProject.minecraft_version-$rootProject.dragonlib_version", + "forge_version_int": rootProject.forge_version_int, + "fabric_loader_version": rootProject.fabric_loader_version, + "java_version": rootProject.java_version, + "maven_group": rootProject.maven_group, + "create_forge_version": rootProject.create_forge_version, + "main_class_name": rootProject.main_class_name, + ] + filesMatching(['pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'createrailwaysnavigator.mixins.json']) { + expand expandProps + } + inputs.properties(expandProps) + exclude ".cache" // Remove cache from generated data + } + java { withSourcesJar() withJavadocJar() @@ -68,18 +90,6 @@ subprojects { targetCompatibility = JavaVersion.VERSION_17 } - javadoc { - options { - title = "DragNSounds API ${rootProject.release_channel}-${rootProject.mod_version}" - tags("apiNote:a:API Note:") - tags("implNote:a:Impl Note:") - tags("implSpec:a:Impl Spec:") - tags("related:a:Related:") - tags("example:a:Examples:") - tags("side:a:Valid on side:") - } - } - tasks.withType(JavaCompile).configureEach { it.options.release = 17 } diff --git a/common/build.gradle b/common/build.gradle index be3455d2..41024776 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -9,9 +9,10 @@ loom { dependencies { modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version" - modImplementation("dev.architectury:architectury:${rootProject.architectury_api_version}") - modImplementation("com.electronwill.night-config:toml:3.6.0") - modImplementation("net.minecraftforge:forgeconfigapiport-fabric:${rootProject.forge_config_api_port_version}") + modCompileOnly("dev.architectury:architectury:${rootProject.architectury_api_version}") + modCompileOnly("com.electronwill.night-config:toml:3.6.0") + modCompileOnly("net.minecraftforge:forgeconfigapiport-fabric:${rootProject.forge_config_api_port_version}") modImplementation("de.mrjulsen.mcdragonlib:dragonlib-fabric:${rootProject.minecraft_version}-${rootProject.dragonlib_version}") + modImplementation("com.simibubi.create:create-fabric-${rootProject.minecraft_version}:${rootProject.create_fabric_version}") } \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java b/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java index 5992e2f9..63411f11 100644 --- a/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java +++ b/common/src/main/java/de/mrjulsen/crn/CRNPlatformSpecific.java @@ -39,7 +39,6 @@ public static void registerConfig() { throw new AssertionError(); } - @ExpectPlatform public static GlobalStation getStationFromBlockEntity(BlockEntity be) { throw new AssertionError(); diff --git a/common/src/main/java/de/mrjulsen/crn/Constants.java b/common/src/main/java/de/mrjulsen/crn/Constants.java index 89951cbf..e66fb382 100644 --- a/common/src/main/java/de/mrjulsen/crn/Constants.java +++ b/common/src/main/java/de/mrjulsen/crn/Constants.java @@ -20,6 +20,9 @@ public class Constants { public static final Component TEXT_SERVER_ERROR = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.server_error"); public static final Component TEXT_SEARCH = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.search"); public static final Component TEXT_HELP = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.help"); + public static final Component TEXT_COPY = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.copy"); + public static final Component TEXT_PASTE = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.paste"); + public static final Component TEXT_RESET = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.reset_defaults"); public static final UUID ZERO_UUID = new UUID(0, 0); public static final int[] DEFAULT_TRAIN_TYPE_COLORS = new int[] { 0xFF393939, 0xFFf0f3f5, 0xFFafb4bb, 0xFF878c96, 0xFF2a7230, 0xFF814997, 0xFF1455c0, 0xFFa9455d, 0xFF55b9e6, 0xFFffd800 }; @@ -30,6 +33,7 @@ public class Constants { public static final String GITHUB_WIKI = "https://github.com/MisterJulsen/Create-Train-Navigator/wiki/"; public static final String HELP_PAGE_ADVANCED_DISPLAYS = GITHUB_WIKI + "Advanced-Displays"; public static final String HELP_PAGE_DYNAMIC_DELAYS = GITHUB_WIKI + "Dynamic-Delays"; + public static final String HELP_PAGE_TRAIN_SEPARATION = GITHUB_WIKI + "Train-Separation"; public static final String HELP_PAGE_GLOBAL_SETTINGS = GITHUB_WIKI + "Global-Settings"; public static final String HELP_PAGE_NAVIGATION_WARNING = GITHUB_WIKI + "Navigation-Warning"; public static final String HELP_PAGE_SCHEDULE_SECTIONS = GITHUB_WIKI + "Train-Schedule-Sections"; diff --git a/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java b/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java index 4dbbe69d..0aaca184 100644 --- a/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java +++ b/common/src/main/java/de/mrjulsen/crn/CreateRailwaysNavigator.java @@ -8,7 +8,6 @@ import com.simibubi.create.foundation.item.TooltipHelper.Palette; import de.mrjulsen.crn.block.AdvancedDisplayBlock; -import de.mrjulsen.crn.client.input.ModKeys; import de.mrjulsen.crn.event.CRNClientEventsRegistryEvent; import de.mrjulsen.crn.event.CRNEventsManager; import de.mrjulsen.crn.event.ModClientEvents; @@ -72,10 +71,6 @@ public static void load() {} public static void init() { - CRNPlatformSpecific.registerConfig(); - if (Platform.getEnv() == EnvType.CLIENT) { - ModKeys.init(); - } ModBlocks.init(); ModItems.init(); ModBlockEntities.init(); @@ -93,6 +88,8 @@ public static void init() { ServerErrorPacket.class )); + CRNPlatformSpecific.registerConfig(); + ModCommonEvents.init(); if (Platform.getEnv() == EnvType.CLIENT) { ModClientEvents.init(); diff --git a/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java b/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java index 3b44a902..c53efa8f 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/AbstractAdvancedDisplayBlock.java @@ -10,10 +10,14 @@ import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.properties.BasicDisplaySettings; import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.registry.ModBlockEntities; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.color.block.BlockColor; import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos.MutableBlockPos; import net.minecraft.core.Direction; @@ -52,6 +56,8 @@ public abstract class AbstractAdvancedDisplayBlock extends Block implements IWrenchable, IBE { + public static final int DEFAULT_DISPLAY_COLOR = 0xFF404040; + public static final DirectionProperty FACING = HorizontalDirectionalBlock.FACING; public static final BooleanProperty UP = BooleanProperty.create("up"); @@ -66,6 +72,19 @@ public AbstractAdvancedDisplayBlock(Properties properties) { .setValue(FACING, Direction.NORTH) ); } + + @Environment(EnvType.CLIENT) + public static BlockColor getDisplayColor() { + return (state, world, pos, layer) -> { + if (world.getBlockEntity(pos) instanceof AdvancedDisplayBlockEntity be) { + return be.getSettingsAs(BasicDisplaySettings.class).map(x -> { + int color = x.getBackColor(); + return color == 0 ? null : color; + }).orElse(DEFAULT_DISPLAY_COLOR); + } + return DEFAULT_DISPLAY_COLOR; + }; + } @Override public BlockState rotate(BlockState pState, Rotation pRotation) { @@ -299,7 +318,6 @@ public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState @Override public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHit) { - ItemStack heldItem = pPlayer.getItemInHand(pHand); AdvancedDisplayBlockEntity blockEntity = ((AdvancedDisplayBlockEntity)pLevel.getBlockEntity(pPos)).getController(); @@ -307,9 +325,17 @@ public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Pla DyeColor dye = dyeItem.getDyeColor(); if (dye != null) { pLevel.playSound(null, pPos, SoundEvents.DYE_USE, SoundSource.BLOCKS, 1.0F, 1.0F); + int dyeColor = dye == DyeColor.ORANGE ? 0xFFFF9900 : dye.getTextColor(); + blockEntity.applyToAll(be -> { - be.setColor(dye == DyeColor.ORANGE ? 0xFF9900 : dye.getMaterialColor().col); - be.notifyUpdate(); + be.getSettingsAs(BasicDisplaySettings.class).ifPresent(x -> { + if (pPlayer.isShiftKeyDown()) { + x.setBackColor(dyeColor); + } else { + x.setFontColor(dyeColor); + } + be.notifyUpdate(); + }); }); if (pLevel.isClientSide) { diff --git a/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java b/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java index 2f2ee6e6..33e31548 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java +++ b/common/src/main/java/de/mrjulsen/crn/block/TrainStationClockBlock.java @@ -83,7 +83,7 @@ public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Pla } if (pLevel.isClientSide) { - pPlayer.displayClientMessage(TextUtils.translate("gui.createrailwaysnavigator.time", TimeUtils.parseTime((int)(pLevel.getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), true); + pPlayer.displayClientMessage(TextUtils.translate("gui.createrailwaysnavigator.time", TimeUtils.parseTime((int)(pLevel.getDayTime() % DragonLib.ticksPerDay() + DragonLib.daytimeShift()), ModClientConfig.TIME_FORMAT.get())), true); } return InteractionResult.SUCCESS; } diff --git a/common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java index 458f4f6d..9327f5f1 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/AdvancedDisplayBlockEntity.java @@ -2,6 +2,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; + +import javax.annotation.Nullable; import com.simibubi.create.content.trains.display.FlapDisplayBlock; import com.simibubi.create.content.trains.entity.CarriageContraption; @@ -11,12 +14,17 @@ import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.block.AbstractAdvancedDisplayBlock; -import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.block.display.properties.BasicDisplaySettings; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.block.display.properties.components.IPlatformWidthSetting; +import de.mrjulsen.crn.block.display.properties.components.ITimeDisplaySetting; +import de.mrjulsen.crn.block.display.properties.components.ITrainNameWidthSetting; import de.mrjulsen.crn.block.properties.EDisplayInfo; import de.mrjulsen.crn.block.properties.EDisplayType; import de.mrjulsen.crn.block.properties.EDisplayType.EDisplayTypeDataSource; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; -import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeInfo; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayProperties; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; import de.mrjulsen.crn.data.CarriageData; @@ -29,6 +37,7 @@ import de.mrjulsen.crn.registry.ModDisplayTypes; import de.mrjulsen.mcdragonlib.block.IBERInstance; import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; import de.mrjulsen.mcdragonlib.data.Cache; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.data.Tripple; @@ -43,7 +52,6 @@ import net.minecraft.nbt.Tag; import net.minecraft.network.Connection; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; -import net.minecraft.world.item.DyeColor; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.HorizontalDirectionalBlock; @@ -55,21 +63,25 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements IMultiblockBlockEntity, IContraptionBlockEntity, - IBERInstance, - IColorableBlockEntity + IBERInstance { + private static final String NBT_DISPLAY_TYPE_SETTINGS = "DisplaySettings"; + + private static final String NBT_FILTER = "Filter"; + private static final String NBT_XSIZE = "XSize"; private static final String NBT_YSIZE = "YSize"; private static final String NBT_CONTROLLER = "IsController"; - private static final String NBT_COLOR = "Color"; private static final String NBT_GLOWING = "Glowing"; - private static final String NBT_DISPLAY_TYPE_KEY = "DisplayTypeKey"; - private static final String NBT_FILTER = "Filter"; + private static final String NBT_LAST_REFRESH_TIME = "LastRefreshed"; - private static final String NBT_PLATFORM_WIDTH = "PlatformWidth"; - private static final String NBT_TRAIN_NAME_WIDTH = "TrainNameWidth"; - private static final String NBT_TIME_DISPLAY = "TimeDisplay"; private static final String NBT_TRAIN_STOPS = "TrainStops"; + + @Deprecated private static final String LEGACY_NBT_PLATFORM_WIDTH = "PlatformWidth"; + @Deprecated private static final String LEGACY_NBT_TRAIN_NAME_WIDTH = "TrainNameWidth"; + @Deprecated private static final String LEGACY_NBT_COLOR = "Color"; + @Deprecated private static final String LEGACY_NBT_TIME_DISPLAY = "TimeDisplay"; + @Deprecated private static final String LEGACY_NBT_DISPLAY_TYPE_KEY = "DisplayTypeKey"; @Deprecated private static final String LEGACY_NBT_INFO_TYPE = "InfoType"; @Deprecated private static final String LEGACY_NBT_DISPLAY_TYPE = "DisplayType"; @@ -79,6 +91,7 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements private static final int REFRESH_FREQUENCY = 100; // DATA + private DisplayTypeResourceKey displayTypeId = ModDisplayTypes.TRAIN_DESTINATION_SIMPLE; private byte xSize = 1; private byte ySize = 1; private boolean isController; @@ -86,16 +99,9 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements private boolean dataOrderChanged = false; private String stationNameFilter; private StationInfo stationInfo; - private byte trainNameWidth; - private byte platformWidth; - private ETimeDisplay timeDisplay = ETimeDisplay.ABS; - - // USER SETTINGS - private int color = DyeColor.WHITE.getTextColor(); private boolean glowing = false; - private DisplayTypeResourceKey displayTypeKey = ModDisplayTypes.TRAIN_DESTINATION_SIMPLE; + private IDisplaySettings displayTypeSettings = AdvancedDisplaysRegistry.createSettings(ModDisplayTypes.TRAIN_DESTINATION_SIMPLE); - // CLIENT DISPLAY ONLY - this data is not saved! private long lastRefreshedTime; private TrainDisplayData trainData = TrainDisplayData.empty(); @@ -103,7 +109,7 @@ public class AdvancedDisplayBlockEntity extends SmartBlockEntity implements // OTHER private int syncTicks = REFRESH_FREQUENCY - 1; - private final Cache> renderer = new Cache<>(() -> new AdvancedDisplayRenderInstance(this)); + private final Cache> renderer = new Cache<>(() -> new AdvancedDisplayRenderInstance(this), ECachingPriority.ALWAYS); public final Cache relativeExitDirection = new Cache<>(() -> { if (getCarriageData() == null || !getTrainData().getNextStop().isPresent() || !(getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock)) { @@ -177,18 +183,6 @@ public long getLastRefreshedTime() { return lastRefreshedTime; } - public byte getTrainNameWidth() { - return trainNameWidth; - } - - public ETimeDisplay getTimeDisplay() { - return timeDisplay; - } - - public byte getPlatformWidth() { - return platformWidth; - } - public byte getXSize() { return xSize; } @@ -209,18 +203,12 @@ public boolean isController() { return isController; } - @Override - public int getColor() { - return color; - } - - @Override public boolean isGlowing() { return glowing; } - public DisplayTypeResourceKey getDisplayTypeKey() { - return displayTypeKey; + public DisplayTypeResourceKey getDisplayType() { + return displayTypeId; } @Override @@ -261,7 +249,7 @@ public boolean isPlatformFixed() { return !stationNameFilter.contains("*"); } - public StationInfo getStationInfo2() { + public StationInfo getStationInfo() { return stationInfo; } @@ -271,21 +259,13 @@ public String getStationNameFilter() { public boolean isSingleLine() { if (getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock block) { - return block.isSingleLined() || AdvancedDisplaysRegistry.getInfo(displayTypeKey).singleLined(); + return block.isSingleLined() || AdvancedDisplaysRegistry.getProperties(displayTypeId).singleLined(); } return false; } - public DisplayTypeInfo getDisplayTypeInfo() { - return AdvancedDisplaysRegistry.getInfo(displayTypeKey); - } - - public void setColor(int color) { - this.color = color; - if (level.isClientSide) { - getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.LAYOUT_CHANGED); - } - + public DisplayProperties getDisplayProperties() { + return AdvancedDisplaysRegistry.getProperties(displayTypeId); } public void setGlowing(boolean glowing) { @@ -295,24 +275,30 @@ public void setGlowing(boolean glowing) { } } - - public void setDisplayTypeKey(DisplayTypeResourceKey key) { - this.displayTypeKey = key; + + /** + * Updates the display type. + * @param key The new display type key. + * @param settings Custom display settings or {@code null} for default settings. + */ + public void setDisplayType(DisplayTypeResourceKey key, @Nullable IDisplaySettings settings) { + this.displayTypeId = key; + this.displayTypeSettings = settings; if (level.isClientSide) { getRenderer().update(level, worldPosition, getBlockState(), this, EUpdateReason.LAYOUT_CHANGED); } } - public void setDepartureData(List predictions, String stationNameFilter, StationInfo staionInfo, long lastRefreshedTime, byte platformWidth, byte trainNameWidth, byte timeDisplayId) { + public void setDepartureData(List predictions, String stationNameFilter, StationInfo staionInfo, long lastRefreshedTime) { this.dataOrderChanged = dataOrderChanged || !ListUtils.compareCollections(this.predictions, predictions, StationDisplayData::equals); this.predictions = predictions; this.stationNameFilter = stationNameFilter; this.stationInfo = staionInfo; this.lastRefreshedTime = lastRefreshedTime; - this.platformWidth = platformWidth; - this.trainNameWidth = trainNameWidth; - this.timeDisplay = ETimeDisplay.getById(timeDisplayId); + //this.platformWidth = platformWidth; + //this.trainNameWidth = trainNameWidth; + //this.timeDisplay = ETimeDisplay.getById(timeDisplayId); } @@ -324,7 +310,7 @@ public boolean connectable(BlockGetter getter, BlockPos a, BlockPos b) { if (getter.getBlockEntity(a) instanceof AdvancedDisplayBlockEntity be1 && getter.getBlockEntity(b) instanceof AdvancedDisplayBlockEntity be2 && be1.getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock block1 && be2.getBlockState().getBlock() instanceof AbstractAdvancedDisplayBlock block2) { return block1 == block2 && - be1.getDisplayTypeKey().equals(be2.getDisplayTypeKey()) && + be1.getDisplayType().equals(be2.getDisplayType()) && block1.canConnectWithBlock(getter, getter.getBlockState(a), getter.getBlockState(b)) && block2.canConnectWithBlock(getter, getter.getBlockState(b), getter.getBlockState(a)) && (!a.above().equals(b) || (be1.getBlockState().getValue(AbstractAdvancedDisplayBlock.UP) && !be1.isSingleLine())) && (!a.below().equals(b) || (be1.getBlockState().getValue(AbstractAdvancedDisplayBlock.DOWN) && !be1.isSingleLine())) @@ -351,7 +337,7 @@ public AdvancedDisplayBlockEntity getController() { } BlockEntity found = level.getBlockEntity(pos); - if (found instanceof AdvancedDisplayBlockEntity flap && flap.isController) + if (found instanceof AdvancedDisplayBlockEntity flap && flap.isController()) return flap; break; @@ -364,7 +350,7 @@ public AdvancedDisplayBlockEntity getController() { } BlockEntity found = level.getBlockEntity(pos); - if (found instanceof AdvancedDisplayBlockEntity flap && flap.isController) + if (found instanceof AdvancedDisplayBlockEntity flap && flap.isController()) return flap; break; @@ -374,16 +360,16 @@ public AdvancedDisplayBlockEntity getController() { } public void copyFrom(AdvancedDisplayBlockEntity other) { - if (getColor() == other.getColor() && - getDisplayTypeKey().equals(other.getDisplayTypeKey()) && + if ( + getDisplayType().equals(other.getDisplayType()) && isGlowing() == other.isGlowing() ) { return; } - color = other.getColor(); glowing = other.isGlowing(); - displayTypeKey = other.getDisplayTypeKey(); + displayTypeId = other.getDisplayType(); + displayTypeSettings = other.getSettings(); notifyUpdate(); } @@ -392,8 +378,6 @@ public void reset() { predictions = List.of(); stationNameFilter = ""; - platformWidth = -1; - trainNameWidth = 14; xSize = 1; ySize = 1; isController = false; @@ -467,7 +451,7 @@ public void tick() { super.tick(); - if (getDisplayTypeKey().category().getSource() != EDisplayTypeDataSource.PLATFORM) { + if (getDisplayType().category().getSource() != EDisplayTypeDataSource.PLATFORM) { return; } @@ -497,13 +481,13 @@ public void contraptionTick(Level level, BlockPos pos, BlockState state, Carriag return; } - if (getDisplayTypeKey().category().getSource() != EDisplayTypeDataSource.TRAIN_INFORMATION) { + if (getDisplayType().category().getSource() != EDisplayTypeDataSource.TRAIN_INFORMATION) { return; } syncTicks++; if ((syncTicks %= 100) == 0 && level.isClientSide) { - DataAccessor.getFromServer(((CarriageContraptionEntity)carriage.entity).trainId, ModAccessorTypes.GET_TRAIN_DISPLAY_DATA, (data) -> { + DataAccessor.getFromServer(((CarriageContraptionEntity)carriage.entity).trainId, ModAccessorTypes.GET_TRAIN_DISPLAY_DATA_FROM_SERVER, (data) -> { if (data.isEmpty() && this.trainData.isEmpty()) { return; } @@ -517,20 +501,16 @@ public void contraptionTick(Level level, BlockPos pos, BlockState state, Carriag prediction.getStationEntryIndex() != data.getNextStop().get().getStationEntryIndex() || this.trainData.getNextStopExitSide() != data.getNextStopExitSide() || this.trainData.isWaitingAtStation() != data.isWaitingAtStation() - //(getInfoType() == EDisplayInfo.INFORMATIVE && getDisplayType() == EDisplayType.PASSENGER_INFORMATION && trainData.getNextStop().get().departureTicks() + lastRefreshedTime != data.getNextStop().get().departureTicks() + refreshTime) // It's not clean but it works ... for now ; } boolean outOfService = this.trainData != null && !this.trainData.getTrainData().getId().equals(Constants.ZERO_UUID) && !data.getNextStop().isPresent(); if (outOfService) { shouldUpdate = true; } - //this.lastRefreshedTime = refreshTime; this.trainData = outOfService ? TrainDisplayData.empty() : data; this.carriageData = new CarriageData(((CarriageContraptionEntity)carriage.entity).carriageIndex, carriage.getAssemblyDirection(), data.isOppositeDirection()); this.relativeExitDirection.clear(); - if (shouldUpdate) { - } getRenderer().update(level, pos, state, this, shouldUpdate ? EUpdateReason.LAYOUT_CHANGED : EUpdateReason.DATA_CHANGED); }); } @@ -541,21 +521,26 @@ protected void write(CompoundTag pTag, boolean clientPacket) { super.write(pTag, clientPacket); pTag.putByte(NBT_XSIZE, getXSize()); pTag.putByte(NBT_YSIZE, getYSize()); - pTag.putInt(NBT_COLOR, getColor()); pTag.putBoolean(NBT_CONTROLLER, isController()); - pTag.put(NBT_DISPLAY_TYPE_KEY, getDisplayTypeKey().toNbt()); pTag.putString(NBT_FILTER, getStationNameFilter()); pTag.putBoolean(NBT_GLOWING, isGlowing()); pTag.putLong(NBT_LAST_REFRESH_TIME, getLastRefreshedTime()); - pTag.putByte(NBT_PLATFORM_WIDTH, getPlatformWidth()); - pTag.putByte(NBT_TRAIN_NAME_WIDTH, getTrainNameWidth()); - pTag.putByte(NBT_TIME_DISPLAY, getTimeDisplay().getId()); - getStationInfo2().writeNbt(pTag); + //pTag.putByte(NBT_TIME_DISPLAY, getTimeDisplay().getId()); + //pTag.putInt(NBT_COLOR, getColor()); + //pTag.putByte(NBT_PLATFORM_WIDTH, getPlatformWidth()); + //pTag.putByte(NBT_TRAIN_NAME_WIDTH, getTrainNameWidth()); + + displayTypeId.toNbt(pTag); + pTag.put(NBT_DISPLAY_TYPE_SETTINGS, displayTypeSettings.serializeNbt()); + + getStationInfo().writeNbt(pTag); if (getStops() != null && !getStops().isEmpty()) { ListTag list = new ListTag(); - list.addAll(getStops().stream().map(x -> x.toNbt()).toList()); + for (StationDisplayData data : getStops()) { + list.add(data.toNbt()); + } pTag.put(NBT_TRAIN_STOPS, list); } } @@ -569,8 +554,9 @@ public void read(CompoundTag pTag, boolean clientPacket) { isController() != pTag.getBoolean(NBT_CONTROLLER) || getXSize() != pTag.getByte(NBT_XSIZE) || getYSize() != pTag.getByte(NBT_YSIZE) || - getPlatformWidth() != pTag.getByte(NBT_PLATFORM_WIDTH) || - getTrainNameWidth() != pTag.getByte(NBT_TRAIN_NAME_WIDTH) || + // TODO + //getPlatformWidth() != pTag.getByte(LEGACY_NBT_PLATFORM_WIDTH) || + //getTrainNameWidth() != pTag.getByte(LEGACY_NBT_TRAIN_NAME_WIDTH) || (getStops().isEmpty() ^ !pTag.contains(NBT_TRAIN_STOPS)) ) { updateClient = true; @@ -583,24 +569,43 @@ public void read(CompoundTag pTag, boolean clientPacket) { xSize = pTag.getByte(NBT_XSIZE); ySize = pTag.getByte(NBT_YSIZE); - if (pTag.contains(NBT_COLOR)) { - color = pTag.getInt(NBT_COLOR); - } glowing = pTag.getBoolean(NBT_GLOWING); isController = pTag.getBoolean(NBT_CONTROLLER); + + // ### Convert deprecated data if (pTag.contains(LEGACY_NBT_INFO_TYPE) && pTag.contains(LEGACY_NBT_DISPLAY_TYPE)) { - displayTypeKey = ModDisplayTypes.legacy_getKeyForType(EDisplayType.getTypeById(pTag.getInt(LEGACY_NBT_DISPLAY_TYPE)), EDisplayInfo.getTypeById(pTag.getInt(LEGACY_NBT_INFO_TYPE))); + displayTypeId = ModDisplayTypes.legacy_getKeyForType(EDisplayType.getTypeById(pTag.getInt(LEGACY_NBT_DISPLAY_TYPE)), EDisplayInfo.getTypeById(pTag.getInt(LEGACY_NBT_INFO_TYPE))); + displayTypeSettings = AdvancedDisplaysRegistry.createSettings(displayTypeId); + } else if (pTag.contains(LEGACY_NBT_DISPLAY_TYPE_KEY)) { + displayTypeId = DisplayTypeResourceKey.legacy_fromNbt(pTag.getCompound(LEGACY_NBT_DISPLAY_TYPE_KEY)); + displayTypeSettings = AdvancedDisplaysRegistry.createSettings(displayTypeId); } else { - displayTypeKey = DisplayTypeResourceKey.fromNbt(pTag.getCompound(NBT_DISPLAY_TYPE_KEY)); + displayTypeId = DisplayTypeResourceKey.fromNbt(pTag); + displayTypeSettings = AdvancedDisplaysRegistry.createSettings(displayTypeId); + displayTypeSettings.deserializeNbt(pTag.getCompound(NBT_DISPLAY_TYPE_SETTINGS)); } + + if (pTag.contains(LEGACY_NBT_COLOR)) { + getSettingsAs(BasicDisplaySettings.class).ifPresent(x -> x.setFontColor(pTag.getInt(LEGACY_NBT_COLOR))); + } + if (displayTypeId.category().getSource() == EDisplayTypeDataSource.PLATFORM) { + if (pTag.contains(LEGACY_NBT_PLATFORM_WIDTH)) { + getSettingsAs(IPlatformWidthSetting.class).ifPresent(x -> x.setPlatformWidth(pTag.getByte(LEGACY_NBT_PLATFORM_WIDTH))); + } + if (pTag.contains(LEGACY_NBT_TRAIN_NAME_WIDTH)) { + getSettingsAs(ITrainNameWidthSetting.class).ifPresent(x -> x.setTrainNameWidth(pTag.getByte(LEGACY_NBT_TRAIN_NAME_WIDTH))); + } + if (pTag.contains(LEGACY_NBT_TIME_DISPLAY)) { + getSettingsAs(ITimeDisplaySetting.class).ifPresent(x -> x.setTimeDisplay(ETimeDisplay.getById(pTag.getByte(LEGACY_NBT_TIME_DISPLAY)))); + } + } + // ### + setDepartureData( pTag.contains(NBT_TRAIN_STOPS) ? new ArrayList<>(pTag.getList(NBT_TRAIN_STOPS, Tag.TAG_COMPOUND).stream().map(x -> StationDisplayData.fromNbt((CompoundTag)x)).toList()) : new ArrayList<>(), pTag.getString(NBT_FILTER), info, - pTag.getLong(NBT_LAST_REFRESH_TIME), - pTag.getByte(NBT_PLATFORM_WIDTH), - pTag.getByte(NBT_TRAIN_NAME_WIDTH), - pTag.getByte(NBT_TIME_DISPLAY) + pTag.getLong(NBT_LAST_REFRESH_TIME) ); if (updateClient) { @@ -628,6 +633,14 @@ public IBlockEntityRendererInstance getRenderer() { return renderer.get(); } + public IDisplaySettings getSettings() { + return displayTypeSettings; + } + + public Optional getSettingsAs(Class clazz) { + return Optional.ofNullable(clazz.isInstance(getSettings()) ? clazz.cast(getSettings()) : null); + } + @Override protected AABB createRenderBoundingBox() { AABB aabb = new AABB(worldPosition); diff --git a/common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java deleted file mode 100644 index 3bdc918a..00000000 --- a/common/src/main/java/de/mrjulsen/crn/block/blockentity/IColorableBlockEntity.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.mrjulsen.crn.block.blockentity; - -public interface IColorableBlockEntity { - int getColor(); - boolean isGlowing(); -} diff --git a/common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java b/common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java index c6ddca2d..3b5ef407 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java +++ b/common/src/main/java/de/mrjulsen/crn/block/blockentity/TrainStationClockBlockEntity.java @@ -8,18 +8,19 @@ import de.mrjulsen.crn.client.ber.TrainStationClockRenderer; import de.mrjulsen.mcdragonlib.block.IBERInstance; import de.mrjulsen.mcdragonlib.client.ber.IBlockEntityRendererInstance; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; import de.mrjulsen.mcdragonlib.data.Cache; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -public class TrainStationClockBlockEntity extends SmartBlockEntity implements IBERInstance, IColorableBlockEntity { +public class TrainStationClockBlockEntity extends SmartBlockEntity implements IBERInstance { private static final String NBT_COLOR = "Color"; private static final String NBT_GLOWING = "IsGlowing"; - private final Cache> renderer = new Cache<>(() -> new TrainStationClockRenderer(this)); + private final Cache> renderer = new Cache<>(() -> new TrainStationClockRenderer(this), ECachingPriority.ALWAYS); private int color = 0xFFFFFFFF; private boolean glowing; @@ -36,12 +37,10 @@ public IBlockEntityRendererInstance getRenderer() return renderer.get(); } - @Override public int getColor() { return color; } - @Override public boolean isGlowing() { return glowing; } diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java index 5be02f7c..d7d7fcd4 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java +++ b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java @@ -1,6 +1,5 @@ package de.mrjulsen.crn.block.display; -import java.util.Arrays; import java.util.List; import com.google.common.collect.ImmutableList; @@ -8,17 +7,15 @@ import com.simibubi.create.content.redstone.displayLink.source.DisplaySource; import com.simibubi.create.content.redstone.displayLink.target.DisplayTargetStats; import com.simibubi.create.content.trains.station.GlobalStation; +import com.simibubi.create.content.trains.station.StationBlockEntity; import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; import com.simibubi.create.foundation.utility.Lang; import de.mrjulsen.crn.CRNPlatformSpecific; -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; -import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.crn.block.properties.ETimeDisplay; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.MutableComponent; -import net.minecraft.util.StringRepresentable; public class AdvancedDisplaySource extends DisplaySource { @@ -78,82 +75,5 @@ public void initConfigurationWidgets(DisplayLinkContext context, ModularGuiLineB }, NBT_FILTER); return; } - - builder.addScrollInput(0, 43, (si, l) -> { - si.titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width")) - .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description")) - .withRange(0, 65) - .withShiftStep(4); - si.setState(16); - l.withSuffix("px"); - }, NBT_TRAIN_NAME_WIDTH); - - builder.addScrollInput(47, 43, (si, l) -> { - si.titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width")) - .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description")) - .withRange(-1, 65) - .withShiftStep(4); - si.setState(16); - si.format((val) -> { - if (val >= 0) { - return TextUtils.text(String.valueOf(val) + "px"); - } - return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); - }); - }, NBT_PLATFORM_WIDTH); - - builder.addSelectionScrollInput(47 * 2, 43, (si, l) -> { - si - .forOptions(Arrays.stream(ETimeDisplay.values()).map(x -> TextUtils.translate(x.getValueInfoTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) - .titled(TextUtils.translate("enum.createrailwaysnavigator.time_display")) - .addHint(TextUtils.translate("enum.createrailwaysnavigator.time_display.description")) - .format((val) -> { - return TextUtils.translate(ETimeDisplay.getById(val).getValueTranslationKey(CreateRailwaysNavigator.MOD_ID)); - }) - .setState(ETimeDisplay.ABS.getId()); - }, NBT_TIME_DISPLAY_TYPE); - } - - public static enum ETimeDisplay implements StringRepresentable, ITranslatableEnum { - ABS((byte)0, "abs"), - ETA((byte)1, "eta"); - - private byte id; - private String name; - - private ETimeDisplay(byte id, String name) { - this.id = id; - this.name = name; - } - - public byte getId() { - return id; - } - - public String getName() { - return name; - } - - public static ETimeDisplay getById(int id) { - return Arrays.stream(values()).filter(x -> x.getId() == id).findFirst().orElse(ABS); - } - - @Override - public String getEnumName() { - return "time_display"; - } - - @Override - public String getEnumValueName() { - return getName(); - } - - @Override - public String getSerializedName() { - return getName(); - } - - } - -} +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java index ac4b0efe..e71ec518 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java +++ b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java @@ -1,5 +1,6 @@ package de.mrjulsen.crn.block.display; +import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -13,6 +14,7 @@ import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.properties.EDisplayType.EDisplayTypeDataSource; import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.TrainStop; import de.mrjulsen.crn.data.train.TrainUtils; import de.mrjulsen.crn.data.train.portable.StationDisplayData; import de.mrjulsen.crn.event.ModCommonEvents; @@ -79,35 +81,39 @@ public void acceptFlapText(int line, List> text, DisplayL return; } - String filter = context.sourceConfig().getString("Filter"); - - if (context.getTargetBlockEntity() instanceof AdvancedDisplayBlockEntity blockEntity) { + if (context.getTargetBlockEntity() instanceof AdvancedDisplayBlockEntity blockEntity && ModCommonEvents.hasServer()) { final AdvancedDisplayBlockEntity controller = blockEntity.getController(); long dayTime = context.getTargetBlockEntity().getLevel().getDayTime(); queueAdvancedDisplayWorkerTask(() -> { - if (controller != null & controller.getDisplayTypeKey().category().getSource() == EDisplayTypeDataSource.PLATFORM) { - List preds = prepare(filter, controller.getDisplayTypeInfo().platformDisplayTrainsCount().apply(controller)); + if (controller != null && controller.getDisplayType().category().getSource() == EDisplayTypeDataSource.PLATFORM) { + String filter = context.sourceConfig().getString("Filter"); + List preds = prepare(filter, controller.getDisplayProperties().platformDisplayTrainsCount().apply(controller)); controller.setDepartureData( preds, filter, GlobalSettings.getInstance().getOrCreateStationTagFor(filter).getInfoForStation(filter), - dayTime, - (byte)context.sourceConfig().getInt(AdvancedDisplaySource.NBT_PLATFORM_WIDTH), - (byte)context.sourceConfig().getInt(AdvancedDisplaySource.NBT_TRAIN_NAME_WIDTH), - context.sourceConfig().getByte(AdvancedDisplaySource.NBT_TIME_DISPLAY_TYPE) + dayTime ); - if (ModCommonEvents.hasServer()) { - ModCommonEvents.getCurrentServer().get().executeIfPossible(controller::sendData); - } + ModCommonEvents.getCurrentServer().get().executeIfPossible(controller::sendData); } }); } } public static List prepare(String filter, int maxLines) { - return TrainUtils.getDeparturesAtStationName(filter, null).stream().limit(maxLines).map(x -> StationDisplayData.of(x)).toList(); + List result = new ArrayList<>(maxLines); + + int i = 0; + for (TrainStop stop : TrainUtils.getDeparturesAtStationName(filter, null)) { + i++; + result.add(StationDisplayData.of(stop)); + if (i >= maxLines) { + break; + } + } + return result; } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/AbstractDisplaySettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/AbstractDisplaySettings.java new file mode 100644 index 00000000..3c7f2a5f --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/AbstractDisplaySettings.java @@ -0,0 +1,36 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public abstract class AbstractDisplaySettings implements IDisplaySettings { + + public static final class EmptyDisplaySettings extends AbstractDisplaySettings { + + @Override + public void deserializeNbt(CompoundTag nbt) { } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { } + + @Override + public void serializeNbt(CompoundTag nbt) { } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { } + } + + public AbstractDisplaySettings() {} + + @Override + public final CompoundTag serializeNbt() { + CompoundTag nbt = new CompoundTag(); + this.serializeNbt(nbt); + return nbt; + } + + public abstract void serializeNbt(CompoundTag nbt); +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/AdvancedDisplaySettingsData.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/AdvancedDisplaySettingsData.java new file mode 100644 index 00000000..acd713e5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/AdvancedDisplaySettingsData.java @@ -0,0 +1,53 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; +import de.mrjulsen.mcdragonlib.data.INBTSerializable; +import net.minecraft.nbt.CompoundTag; + +public class AdvancedDisplaySettingsData implements INBTSerializable { + + public static final String NBT_SETTINGS = "Settings"; + public static final String NBT_DOUBLE_SIDED = "DoubleSided"; + + private IDisplaySettings settings; + private DisplayTypeResourceKey key; + private boolean doubleSided; + + public AdvancedDisplaySettingsData() {} + + public AdvancedDisplaySettingsData(DisplayTypeResourceKey key, IDisplaySettings settings, boolean doubleSided) { + this.key = key; + this.settings = settings; + this.doubleSided = doubleSided; + } + + @Override + public CompoundTag serializeNbt() { + CompoundTag nbt = new CompoundTag(); + key.toNbt(nbt); + nbt.put(NBT_SETTINGS, settings.serializeNbt()); + nbt.putBoolean(NBT_DOUBLE_SIDED, doubleSided); + return nbt; + } + + @Override + public void deserializeNbt(CompoundTag nbt) { + this.key = DisplayTypeResourceKey.fromNbt(nbt); + this.settings = AdvancedDisplaysRegistry.createSettings(key); + this.settings.deserializeNbt(nbt.getCompound(NBT_SETTINGS)); + this.doubleSided = nbt.getBoolean(NBT_DOUBLE_SIDED); + } + + public IDisplaySettings getSettings() { + return settings; + } + + public DisplayTypeResourceKey getKey() { + return key; + } + + public boolean isDoubleSided() { + return doubleSided; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/BasicDisplaySettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/BasicDisplaySettings.java new file mode 100644 index 00000000..30cf5ba8 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/BasicDisplaySettings.java @@ -0,0 +1,60 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.IColorSetting; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +/** + * General settings that all displays share. + */ +public class BasicDisplaySettings extends AbstractDisplaySettings implements IColorSetting { + + protected int fontColor = 0xFFFFFFFF; + protected int backColor = 0; + + @Override + public void deserializeNbt(CompoundTag nbt) { + if (nbt.contains(NBT_FONT_COLOR)) this.fontColor = nbt.getInt(NBT_FONT_COLOR); + if (nbt.contains(NBT_BACK_COLOR)) this.backColor = nbt.getInt(NBT_BACK_COLOR); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + nbt.putInt(NBT_FONT_COLOR, fontColor); + nbt.putInt(NBT_BACK_COLOR, backColor); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + this.buildColorGui(context); + } + + @Override + public int getFontColor() { + return fontColor; + } + + @Override + public void setFontColor(int fontColor) { + this.fontColor = fontColor; + } + + @Override + public int getBackColor() { + return backColor; + } + + @Override + public void setBackColor(int backColor) { + this.backColor = backColor; + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + this.copyColorSetting(oldSettings); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/DepartureBoardDisplayTableSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/DepartureBoardDisplayTableSettings.java new file mode 100644 index 00000000..77d361cd --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/DepartureBoardDisplayTableSettings.java @@ -0,0 +1,148 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.GuiBuilderWrapper; +import de.mrjulsen.crn.block.display.properties.components.IPlatformWidthSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowArrivalSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowLineColorSetting; +import de.mrjulsen.crn.block.display.properties.components.ITimeDisplaySetting; +import de.mrjulsen.crn.block.display.properties.components.ITrainNameWidthSetting; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.MathUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class DepartureBoardDisplayTableSettings extends BasicDisplaySettings implements ITimeDisplaySetting, ITrainNameWidthSetting, IPlatformWidthSetting, IShowArrivalSetting, IShowLineColorSetting { + + protected static final String NBT_INFO_WIDTH = "InfoWidth"; + protected static final String NBT_STOPOVERS_WIDTH = "StopoversWidth"; + + protected ETimeDisplay timeDisplay = ETimeDisplay.ABS; + protected byte trainNameWidth = ITrainNameWidthSetting.DEFAULT_TRAIN_NAME_WIDTH; + protected byte platformWidth = ITrainNameWidthSetting.DEFAULT_TRAIN_NAME_WIDTH; + protected boolean showArrival = false; + protected boolean showLineColor = false; + protected float infoWidthPercentage = 0.25f; + protected float stopoversWidthPercentage = 0.33f; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TIME_DISPLAY)) this.timeDisplay = ETimeDisplay.getById(nbt.getByte(NBT_TIME_DISPLAY)); + if (nbt.contains(NBT_TRAIN_NAME_WIDTH)) this.trainNameWidth = nbt.getByte(NBT_TRAIN_NAME_WIDTH); + if (nbt.contains(NBT_PLATFORM_WIDTH)) this.platformWidth = nbt.getByte(NBT_PLATFORM_WIDTH); + if (nbt.contains(NBT_SHOW_ARRIVAL)) this.showArrival = nbt.getBoolean(NBT_SHOW_ARRIVAL); + if (nbt.contains(NBT_SHOW_LINE_COLOR)) this.showLineColor = nbt.getBoolean(NBT_SHOW_LINE_COLOR); + if (nbt.contains(NBT_INFO_WIDTH)) this.infoWidthPercentage = MathUtils.clamp(nbt.getFloat(NBT_INFO_WIDTH), 0, 1); + if (nbt.contains(NBT_STOPOVERS_WIDTH)) this.stopoversWidthPercentage = MathUtils.clamp(nbt.getFloat(NBT_STOPOVERS_WIDTH), 0, 1); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TIME_DISPLAY, timeDisplay.getId()); + nbt.putByte(NBT_TRAIN_NAME_WIDTH, trainNameWidth); + nbt.putByte(NBT_PLATFORM_WIDTH, platformWidth); + nbt.putBoolean(NBT_SHOW_ARRIVAL, showArrival); + nbt.putBoolean(NBT_SHOW_LINE_COLOR, showLineColor); + nbt.putFloat(NBT_INFO_WIDTH, infoWidthPercentage); + nbt.putFloat(NBT_STOPOVERS_WIDTH, stopoversWidthPercentage); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTimeDisplayGui(context); + this.buildTrainNameGui(context, false, false); + this.buildPlatformWidthGui(context, false); + GuiBuilderWrapper.buildDepartureBoardTableGui(this, context); + this.buildShowArrivalGui(context); + this.buildShowLineColorGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTimeDisplaySetting(oldSettings); + copyTrainNameSetting(oldSettings); + copyPlatformWidthSetting(oldSettings); + copyShowArrivalSetting(oldSettings); + copyShowLineColorSetting(oldSettings); + } + + @Override + public ETimeDisplay getTimeDisplay() { + return timeDisplay; + } + + @Override + public void setTimeDisplay(ETimeDisplay display) { + this.timeDisplay = display; + } + + @Override + public byte getPlatformWidth() { + return platformWidth; + } + + @Override + public void setPlatformWidth(byte b) { + this.platformWidth = b; + } + + @Override + public byte getTrainNameWidth() { + return trainNameWidth; + } + + @Override + public void setTrainNameWidth(byte b) { + this.trainNameWidth = b; + } + + @Override + public boolean showArrival() { + return showArrival; + } + + @Override + public void setShowArrival(boolean b) { + this.showArrival = b; + } + + public float getInfoWidthPercentage() { + return infoWidthPercentage; + } + + public float getStopoversWidthPercentage() { + return stopoversWidthPercentage; + } + + public void setInfoWidthPercentage(float f) { + this.infoWidthPercentage = MathUtils.clamp(f, 0, 1); + } + + public void setStopoversWidthPercentage(float f) { + this.stopoversWidthPercentage = MathUtils.clamp(f, 0, 1); + } + + public void setInfoWidthPercentageInt(byte f) { + this.infoWidthPercentage = (float)MathUtils.clamp(f, 0, 100) / 100f; + } + + public void setStopoversWidthPercentageInt(byte f) { + this.stopoversWidthPercentage = (float)MathUtils.clamp(f, 0, 100) / 100f; + } + + @Override + public boolean showLineColor() { + return showLineColor; + } + + @Override + public void setShowLineColor(boolean b) { + this.showLineColor = b; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/IDisplaySettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/IDisplaySettings.java new file mode 100644 index 00000000..de5b8eb2 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/IDisplaySettings.java @@ -0,0 +1,35 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.data.INBTSerializable; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public interface IDisplaySettings extends INBTSerializable { + + /** + * Serializes the data to NBT. + * @return A new {@code CompoundTag} containing the serialized data. + */ + CompoundTag serializeNbt(); + + /** + * Deserializes the NBT data. + * @param nbt The NBT Compound Tag. + */ + void deserializeNbt(CompoundTag nbt); + + /** + * Called when updateing the settings of the block. Can be used to transfer old settings. + * @param oldSettings The previous settings. + */ + void onChangeSettings(IDisplaySettings oldSettings); + + /** + * Called when building the "Advanced Settings" section in the Advanced Display Settings Screen. + * @param container The container of the settings. + * @param builder The builder to build the settings lines. + */ + @Environment(EnvType.CLIENT) void buildGui(GuiBuilderContext context); +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationDetailedSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationDetailedSettings.java new file mode 100644 index 00000000..cd74a19f --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationDetailedSettings.java @@ -0,0 +1,147 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.ICarriageIndexSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowExitDirectionSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowNextConnections; +import de.mrjulsen.crn.block.display.properties.components.IShowTrainStatsSetting; +import de.mrjulsen.crn.block.display.properties.components.ITimeDisplaySetting; +import de.mrjulsen.crn.block.display.properties.components.ITrainTextSetting; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class PassengerInformationDetailedSettings extends BasicDisplaySettings implements + ITimeDisplaySetting, + IShowTrainStatsSetting, + IShowExitDirectionSetting, + ICarriageIndexSetting, + IShowNextConnections, + ITrainTextSetting +{ + + protected ETimeDisplay timeDisplay = ETimeDisplay.ABS; + protected boolean showStats = true; + protected boolean showExit = true; + protected boolean showConnections = true; + protected byte carriageIndexOffset = 0; + protected boolean overwriteCarriageIndex = false; + protected ETrainTextComponents trainTextComponents = ETrainTextComponents.TRAIN_NAME; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TIME_DISPLAY)) this.timeDisplay = ETimeDisplay.getById(nbt.getByte(NBT_TIME_DISPLAY)); + if (nbt.contains(NBT_SHOW_STATS)) this.showStats = nbt.getBoolean(NBT_SHOW_STATS); + if (nbt.contains(NBT_SHOW_EXIT)) this.showExit = nbt.getBoolean(NBT_SHOW_EXIT); + if (nbt.contains(NBT_SHOW_CONNECTIONS)) this.showConnections = nbt.getBoolean(NBT_SHOW_CONNECTIONS); + if (nbt.contains(NBT_CARRIAGE_INDEX)) this.carriageIndexOffset = nbt.getByte(NBT_CARRIAGE_INDEX); + if (nbt.contains(NBT_OVERWRITE_CARRIAGE_INDEX)) this.overwriteCarriageIndex = nbt.getBoolean(NBT_OVERWRITE_CARRIAGE_INDEX); + if (nbt.contains(NBT_TRAIN_TEXT)) this.trainTextComponents = ETrainTextComponents.getById(nbt.getByte(NBT_TRAIN_TEXT)); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TIME_DISPLAY, timeDisplay.getId()); + nbt.putBoolean(NBT_SHOW_STATS, showStats); + nbt.putBoolean(NBT_SHOW_EXIT, showExit); + nbt.putBoolean(NBT_SHOW_CONNECTIONS, showConnections); + nbt.putByte(NBT_CARRIAGE_INDEX, carriageIndexOffset); + nbt.putBoolean(NBT_OVERWRITE_CARRIAGE_INDEX, overwriteCarriageIndex); + nbt.putByte(NBT_TRAIN_TEXT, trainTextComponents.getId()); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTimeDisplayGui(context); + this.buildShowStatsGui(context); + this.buildShowExitGui(context); + this.buildShowConnectionGui(context); + this.buildCarriageIndexGui(context); + this.buildTrainTextGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTimeDisplaySetting(oldSettings); + copyShowExitSetting(oldSettings); + copyShowStatsSetting(oldSettings); + copyShowConnectionSetting(oldSettings); + copyCarriageIndexSetting(oldSettings); + } + + @Override + public boolean showStats() { + return showStats; + } + + @Override + public void setShowStats(boolean b) { + this.showStats = b; + } + + @Override + public boolean showExit() { + return showExit; + } + + @Override + public void setShowExit(boolean b) { + this.showExit = b; + } + + @Override + public boolean showConnections() { + return showConnections; + } + + @Override + public void setShowConnection(boolean b) { + this.showConnections = b; + } + + @Override + public byte getCarriageIndex() { + return carriageIndexOffset; + } + + @Override + public boolean shouldOverwriteCarriageIndex() { + return overwriteCarriageIndex; + } + + @Override + public void setCarriageIndex(byte b) { + this.carriageIndexOffset = b; + } + + @Override + public void setOverwriteCarriageIndex(boolean b) { + this.overwriteCarriageIndex = b; + } + + @Override + public ETimeDisplay getTimeDisplay() { + return timeDisplay; + } + + @Override + public void setTimeDisplay(ETimeDisplay display) { + this.timeDisplay = display; + } + + @Override + public ETrainTextComponents getTrainTextComponents() { + return trainTextComponents; + } + + @Override + public void setTrainTextComponents(ETrainTextComponents v) { + this.trainTextComponents = v; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationScrollingTextSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationScrollingTextSettings.java new file mode 100644 index 00000000..2a9754f2 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PassengerInformationScrollingTextSettings.java @@ -0,0 +1,91 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.IShowExitDirectionSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowTimeAndDateSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowTrainStatsSetting; +import de.mrjulsen.crn.block.display.properties.components.ITrainTextSetting; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.minecraft.nbt.CompoundTag; + +public class PassengerInformationScrollingTextSettings extends BasicDisplaySettings implements IShowTrainStatsSetting, IShowExitDirectionSetting, IShowTimeAndDateSetting, ITrainTextSetting { + + protected boolean showStats = true; + protected boolean showExit = true; + protected boolean showTimeAndDate = true; + protected ETrainTextComponents trainTextComponents = ETrainTextComponents.ALL; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_SHOW_STATS)) this.showStats = nbt.getBoolean(NBT_SHOW_STATS); + if (nbt.contains(NBT_SHOW_EXIT)) this.showExit = nbt.getBoolean(NBT_SHOW_EXIT); + if (nbt.contains(NBT_SHOW_TIME_AND_DATE)) this.showTimeAndDate = nbt.getBoolean(NBT_SHOW_TIME_AND_DATE); + if (nbt.contains(NBT_TRAIN_TEXT)) this.trainTextComponents = ETrainTextComponents.getById(nbt.getByte(NBT_TRAIN_TEXT)); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putBoolean(NBT_SHOW_STATS, showStats); + nbt.putBoolean(NBT_SHOW_EXIT, showExit); + nbt.putBoolean(NBT_SHOW_TIME_AND_DATE, showTimeAndDate); + nbt.putByte(NBT_TRAIN_TEXT, trainTextComponents.getId()); + } + + @Override + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildShowStatsGui(context); + this.buildShowExitGui(context); + this.buildShowTimeAndDateGui(context); + this.buildTrainTextGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyShowExitSetting(oldSettings); + copyShowStatsSetting(oldSettings); + copyShowTimeAndDateSetting(oldSettings); + } + + @Override + public boolean showStats() { + return showStats; + } + + @Override + public void setShowStats(boolean b) { + this.showStats = b; + } + + @Override + public boolean showExit() { + return showExit; + } + + @Override + public void setShowExit(boolean b) { + this.showExit = b; + } + + @Override + public ETrainTextComponents getTrainTextComponents() { + return trainTextComponents; + } + + @Override + public void setTrainTextComponents(ETrainTextComponents v) { + this.trainTextComponents = v; + } + + @Override + public boolean showTimeAndDate() { + return showTimeAndDate; + } + + @Override + public void setShowTimeAndDate(boolean b) { + this.showTimeAndDate = b; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayFocusSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayFocusSettings.java new file mode 100644 index 00000000..1be5e6d9 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayFocusSettings.java @@ -0,0 +1,151 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.GuiBuilderWrapper; +import de.mrjulsen.crn.block.display.properties.components.IPlatformWidthSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowArrivalSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowLineColorSetting; +import de.mrjulsen.crn.block.display.properties.components.ITimeDisplaySetting; +import de.mrjulsen.crn.block.display.properties.components.ITrainNameWidthSetting; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class PlatformDisplayFocusSettings extends BasicDisplaySettings implements ITimeDisplaySetting, ITrainNameWidthSetting, IPlatformWidthSetting, IShowArrivalSetting, IShowLineColorSetting { + + public static final String NBT_TRAIN_NAME_WIDTH_NEXT_STOP = "TrainNameWidthNextStop"; + public static final String NBT_PLATFORM_WIDTH_NEXT_STOP = "PlatformWidthNextStop"; + + protected ETimeDisplay timeDisplay = ETimeDisplay.ABS; + protected byte trainNameWidth = 12; + protected byte trainNameWidthNextStop = -1; + protected byte platformWidth = -1; + protected byte platformWidthNextStop = -1; + protected boolean showArrival = true; + protected boolean showTrainLineColor = false; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TIME_DISPLAY)) this.timeDisplay = ETimeDisplay.getById(nbt.getByte(NBT_TIME_DISPLAY)); + if (nbt.contains(NBT_TRAIN_NAME_WIDTH)) this.trainNameWidth = nbt.getByte(NBT_TRAIN_NAME_WIDTH); + if (nbt.contains(NBT_TRAIN_NAME_WIDTH_NEXT_STOP)) this.trainNameWidthNextStop = nbt.getByte(NBT_TRAIN_NAME_WIDTH_NEXT_STOP); + if (nbt.contains(NBT_PLATFORM_WIDTH)) this.platformWidth = nbt.getByte(NBT_PLATFORM_WIDTH); + if (nbt.contains(NBT_PLATFORM_WIDTH_NEXT_STOP)) this.platformWidthNextStop = nbt.getByte(NBT_PLATFORM_WIDTH_NEXT_STOP); + if (nbt.contains(NBT_SHOW_ARRIVAL)) this.showArrival = nbt.getBoolean(NBT_SHOW_ARRIVAL); + if (nbt.contains(NBT_SHOW_LINE_COLOR)) this.showTrainLineColor = nbt.getBoolean(NBT_SHOW_LINE_COLOR); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TIME_DISPLAY, timeDisplay.getId()); + nbt.putByte(NBT_TRAIN_NAME_WIDTH, trainNameWidth); + nbt.putByte(NBT_TRAIN_NAME_WIDTH_NEXT_STOP, trainNameWidthNextStop); + nbt.putByte(NBT_PLATFORM_WIDTH, platformWidth); + nbt.putByte(NBT_PLATFORM_WIDTH_NEXT_STOP, platformWidthNextStop); + nbt.putBoolean(NBT_SHOW_ARRIVAL, showArrival); + nbt.putBoolean(NBT_SHOW_LINE_COLOR, showTrainLineColor); + + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTimeDisplayGui(context); + this.buildBasicTextWidthGui(context); + GuiBuilderWrapper.buildPlatformDisplayFocusGui(this, context); + this.buildShowArrivalGui(context); + this.buildShowLineColorGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTimeDisplaySetting(oldSettings); + copyTrainNameSetting(oldSettings); + copyPlatformWidthSetting(oldSettings); + copyShowArrivalSetting(oldSettings); + copyShowLineColorSetting(oldSettings); + + if (oldSettings instanceof PlatformDisplayFocusSettings o) { + setTrainNameWidthNextStop(o.getTrainNameWidthNextStop()); + } + } + + @Override + public ETimeDisplay getTimeDisplay() { + return timeDisplay; + } + + @Override + public void setTimeDisplay(ETimeDisplay display) { + this.timeDisplay = display; + } + + @Override + public byte getPlatformWidth() { + return platformWidth; + } + + @Override + public void setPlatformWidth(byte b) { + this.platformWidth = b; + } + + public byte getPlatformWidthNextStop() { + return platformWidthNextStop; + } + + public void setPlatformWidthNextStop(byte b) { + this.platformWidthNextStop = b; + } + + @Override + public byte getTrainNameWidth() { + return trainNameWidth; + } + + @Override + public void setTrainNameWidth(byte b) { + this.trainNameWidth = b; + } + + public byte getTrainNameWidthNextStop() { + return trainNameWidthNextStop; + } + + public void setTrainNameWidthNextStop(byte b) { + this.trainNameWidthNextStop = b; + } + + @Override + public boolean showArrival() { + return showArrival; + } + + @Override + public void setShowArrival(boolean b) { + this.showArrival = b; + } + + public boolean isAutoTrainNameWidthNextStop() { + return getTrainNameWidthNextStop() < 0; + } + + public boolean isAutoPlatformWidthNextStop() { + return getPlatformWidthNextStop() < 0; + } + + @Override + public void setShowLineColor(boolean b) { + this.showTrainLineColor = b; + } + + @Override + public boolean showLineColor() { + return showTrainLineColor; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayScrollingTextSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayScrollingTextSettings.java new file mode 100644 index 00000000..b807b977 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayScrollingTextSettings.java @@ -0,0 +1,49 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.ITimeDisplaySetting; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class PlatformDisplayScrollingTextSettings extends BasicDisplaySettings implements ITimeDisplaySetting { + + protected ETimeDisplay timeDisplay = ETimeDisplay.ABS; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TIME_DISPLAY)) this.timeDisplay = ETimeDisplay.getById(nbt.getByte(NBT_TIME_DISPLAY)); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TIME_DISPLAY, timeDisplay.getId()); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTimeDisplayGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTimeDisplaySetting(oldSettings); + } + + @Override + public ETimeDisplay getTimeDisplay() { + return timeDisplay; + } + + @Override + public void setTimeDisplay(ETimeDisplay display) { + this.timeDisplay = display; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayTableSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayTableSettings.java new file mode 100644 index 00000000..d7d669b0 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/PlatformDisplayTableSettings.java @@ -0,0 +1,112 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.IPlatformWidthSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowArrivalSetting; +import de.mrjulsen.crn.block.display.properties.components.IShowLineColorSetting; +import de.mrjulsen.crn.block.display.properties.components.ITimeDisplaySetting; +import de.mrjulsen.crn.block.display.properties.components.ITrainNameWidthSetting; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class PlatformDisplayTableSettings extends BasicDisplaySettings implements ITimeDisplaySetting, ITrainNameWidthSetting, IPlatformWidthSetting, IShowArrivalSetting, IShowLineColorSetting { + + protected ETimeDisplay timeDisplay = ETimeDisplay.ABS; + protected byte trainNameWidth = ITrainNameWidthSetting.DEFAULT_TRAIN_NAME_WIDTH; + protected byte platformWidth = -1; + protected boolean showArrival = true; + protected boolean showLineColor = false; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TIME_DISPLAY)) this.timeDisplay = ETimeDisplay.getById(nbt.getByte(NBT_TIME_DISPLAY)); + if (nbt.contains(NBT_TRAIN_NAME_WIDTH)) this.trainNameWidth = nbt.getByte(NBT_TRAIN_NAME_WIDTH); + if (nbt.contains(NBT_PLATFORM_WIDTH)) this.platformWidth = nbt.getByte(NBT_PLATFORM_WIDTH); + if (nbt.contains(NBT_SHOW_ARRIVAL)) this.showArrival = nbt.getBoolean(NBT_SHOW_ARRIVAL); + if (nbt.contains(NBT_SHOW_LINE_COLOR)) this.showLineColor = nbt.getBoolean(NBT_SHOW_LINE_COLOR); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TIME_DISPLAY, timeDisplay.getId()); + nbt.putByte(NBT_TRAIN_NAME_WIDTH, trainNameWidth); + nbt.putByte(NBT_PLATFORM_WIDTH, platformWidth); + nbt.putBoolean(NBT_SHOW_ARRIVAL, showArrival); + nbt.putBoolean(NBT_SHOW_LINE_COLOR, showLineColor); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTimeDisplayGui(context); + this.buildTrainNameGui(context, true, false); + this.buildPlatformWidthGui(context, true); + this.buildShowArrivalGui(context); + this.buildShowLineColorGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTimeDisplaySetting(oldSettings); + copyTrainNameSetting(oldSettings); + copyPlatformWidthSetting(oldSettings); + copyShowArrivalSetting(oldSettings); + copyShowLineColorSetting(oldSettings); + } + + @Override + public ETimeDisplay getTimeDisplay() { + return timeDisplay; + } + + @Override + public void setTimeDisplay(ETimeDisplay display) { + this.timeDisplay = display; + } + + @Override + public byte getPlatformWidth() { + return platformWidth; + } + + @Override + public void setPlatformWidth(byte b) { + this.platformWidth = b; + } + + @Override + public byte getTrainNameWidth() { + return trainNameWidth; + } + + @Override + public void setTrainNameWidth(byte b) { + this.trainNameWidth = b; + } + + @Override + public boolean showArrival() { + return showArrival; + } + + @Override + public void setShowArrival(boolean b) { + this.showArrival = b; + } + + @Override + public boolean showLineColor() { + return showLineColor; + } + + @Override + public void setShowLineColor(boolean b) { + this.showLineColor = b; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationCompactSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationCompactSettings.java new file mode 100644 index 00000000..eb09a059 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationCompactSettings.java @@ -0,0 +1,48 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.ITrainNameWidthSetting; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class TrainDestinationCompactSettings extends BasicDisplaySettings implements ITrainNameWidthSetting { + + protected byte trainNameWidth = -1; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TRAIN_NAME_WIDTH)) this.trainNameWidth = nbt.getByte(NBT_TRAIN_NAME_WIDTH); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TRAIN_NAME_WIDTH, trainNameWidth); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTrainNameGui(context, true, true); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTrainNameSetting(oldSettings); + } + + @Override + public byte getTrainNameWidth() { + return trainNameWidth; + } + + @Override + public void setTrainNameWidth(byte b) { + this.trainNameWidth = b; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationDetailedSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationDetailedSettings.java new file mode 100644 index 00000000..7aeccf71 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationDetailedSettings.java @@ -0,0 +1,61 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.ICarriageIndexSetting; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class TrainDestinationDetailedSettings extends BasicDisplaySettings implements ICarriageIndexSetting { + + protected byte carriageIndexOffset = 0; + protected boolean overwriteCarriageIndex = false; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_CARRIAGE_INDEX)) this.carriageIndexOffset = nbt.getByte(NBT_CARRIAGE_INDEX); + if (nbt.contains(NBT_OVERWRITE_CARRIAGE_INDEX)) this.overwriteCarriageIndex = nbt.getBoolean(NBT_OVERWRITE_CARRIAGE_INDEX); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_CARRIAGE_INDEX, carriageIndexOffset); + nbt.putBoolean(NBT_OVERWRITE_CARRIAGE_INDEX, overwriteCarriageIndex); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildCarriageIndexGui(context); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyCarriageIndexSetting(oldSettings); + } + + @Override + public byte getCarriageIndex() { + return carriageIndexOffset; + } + + @Override + public boolean shouldOverwriteCarriageIndex() { + return overwriteCarriageIndex; + } + + @Override + public void setCarriageIndex(byte b) { + this.carriageIndexOffset = b; + } + + @Override + public void setOverwriteCarriageIndex(boolean b) { + this.overwriteCarriageIndex = b; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationExtendedSettings.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationExtendedSettings.java new file mode 100644 index 00000000..23f7bdf0 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/TrainDestinationExtendedSettings.java @@ -0,0 +1,48 @@ +package de.mrjulsen.crn.block.display.properties; + +import de.mrjulsen.crn.block.display.properties.components.ITrainNameWidthSetting; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; + +public class TrainDestinationExtendedSettings extends BasicDisplaySettings implements ITrainNameWidthSetting { + + protected byte trainNameWidth = -1; + + @Override + public void deserializeNbt(CompoundTag nbt) { + super.deserializeNbt(nbt); + if (nbt.contains(NBT_TRAIN_NAME_WIDTH)) this.trainNameWidth = nbt.getByte(NBT_TRAIN_NAME_WIDTH); + } + + @Override + public void serializeNbt(CompoundTag nbt) { + super.serializeNbt(nbt); + nbt.putByte(NBT_TRAIN_NAME_WIDTH, trainNameWidth); + } + + @Override + @Environment(EnvType.CLIENT) + public void buildGui(GuiBuilderContext context) { + super.buildGui(context); + this.buildTrainNameGui(context, true, true); + } + + @Override + public void onChangeSettings(IDisplaySettings oldSettings) { + super.onChangeSettings(oldSettings); + copyTrainNameSetting(oldSettings); + } + + @Override + public byte getTrainNameWidth() { + return trainNameWidth; + } + + @Override + public void setTrainNameWidth(byte b) { + this.trainNameWidth = b; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/GuiBuilderWrapper.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/GuiBuilderWrapper.java new file mode 100644 index 00000000..d33dafc5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/GuiBuilderWrapper.java @@ -0,0 +1,372 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import java.util.Arrays; +import java.util.List; + +import com.simibubi.create.foundation.gui.widget.ScrollInput; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.DepartureBoardDisplayTableSettings; +import de.mrjulsen.crn.block.display.properties.PlatformDisplayFocusSettings; +import de.mrjulsen.crn.block.display.properties.components.ITrainTextSetting.ETrainTextComponents; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.ColorSlotWidget; +import de.mrjulsen.crn.client.gui.widgets.DLCreateScrollInput; +import de.mrjulsen.crn.client.gui.widgets.DLCreateSelectionScrollInput; +import de.mrjulsen.crn.client.gui.widgets.IconSlotWidget; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLCheckBox; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.MathUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; + +public class GuiBuilderWrapper { + + static void buildColorGui(IColorSetting setting, GuiBuilderContext context) { + context.builder().addLine(IColorSetting.GUI_LINE_COLORS_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.COLOR_PALETTE.getAsSprite(16, 16))); + line.add(new ColorSlotWidget( + context.container().getParentScreen(), + line.getCurrentX() + 4, + line.y() + 2, + setting.getFontColor() == 0 ? 0 : (0xFF << 24) | (setting.getFontColor() & 0x00FFFFFF), + ModUtils.getDyeColors(), + false, + false, + List.of(IColorSetting.textFontColor, IColorSetting.textClickToEdit), + () -> 0, + (b) -> setting.setFontColor(b.getSelectedColor()) + )); + line.add(new ColorSlotWidget( + context.container().getParentScreen(), + line.getCurrentX() + 4, + line.y() + 2, + setting.getBackColor() == 0 ? 0 : (0xFF << 24) | (setting.getBackColor() & 0x00FFFFFF), + ModUtils.getDyeColors(), + false, + true, + List.of(IColorSetting.textBackColor, IColorSetting.textClickToEdit), + () -> 0, + (b) -> setting.setBackColor(b.getSelectedColor()) + )); + }); + } + + static void buildCarriageIndexGui(ICarriageIndexSetting setting, GuiBuilderContext context) { + context.builder().addLine(ICarriageIndexSetting.GUI_LINE_CARRIAGE_INDEX_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.CARRIAGE_NUMBER.getAsSprite(16, 16))); + int w = 22; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 6, line.y() + 2, w, 18)) + .setRenderArrow(true) + .titled(TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.carriage_index")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.carriage_index.description")) + .withRange(0, 100) + .withShiftStep(4) + .setState(setting.getCarriageIndex()) + .calling((i) -> { + setting.setCarriageIndex(i.byteValue()); + }) + ; + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), ICarriageIndexSetting.textOverwriteCarriageIndex.getString(), setting.shouldOverwriteCarriageIndex(), (c) -> setting.setOverwriteCarriageIndex(c.isChecked())) { + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(context.container().getParentScreen(), this, List.of(ICarriageIndexSetting.textOverwriteCarriageIndexDescription), context.container().getParentScreen().width() / 3, graphics, mouseX, mouseY); + } + }); + }); + } + + static void buildBasicTextWidthGui(ICustomTextWidthSetting setting, GuiBuilderContext context) { + context.builder().addLine(ICustomTextWidthSetting.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.WIDTH.getAsSprite(16, 16))); + }); + } + + static void buildPlatformWidthGui(IPlatformWidthSetting setting, GuiBuilderContext context, boolean allowAuto) { + context.builder().addToLine(IPlatformWidthSetting.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - IPlatformWidthSetting.USED_LINE_SPACE) / 4 - 3; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18)) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description")) + .withRange(allowAuto ? -1 : 0, 65) + .withShiftStep(4) + .setState(setting.getPlatformWidth()) + .format((val) -> { + if (val >= 0) { + return TextUtils.text(String.valueOf(val) + "px"); + } + return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); + }) + .calling((i) -> { + setting.setPlatformWidth(i.byteValue()); + }) + ; + }); + } + + static void buildShowArrivalGui(IShowArrivalSetting setting, GuiBuilderContext context) { + context.builder().addLine(IShowArrivalSetting.GUI_LINE_SHOW_ARRIVAL_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TARGET.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), IShowArrivalSetting.textShowArrival.getString(), setting.showArrival(), (cb) -> setting.setShowArrival(cb.isChecked())) { + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(context.container().getParentScreen(), this, List.of(IShowArrivalSetting.textShowArrivalDescription), context.container().getParentScreen().width() / 3, graphics, mouseX, mouseY); + } + }); + }); + } + + static void buildShowExitGui(IShowExitDirectionSetting settings, GuiBuilderContext context) { + context.builder().addLine(IShowExitDirectionSetting.GUI_LINE_SHOW_ARRIVAL_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.EXIT.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), IShowExitDirectionSetting.textShowExit.getString(), settings.showExit(), (cb) -> settings.setShowExit(cb.isChecked()))); + }); + } + + static void buildShowLineColorGui(IShowLineColorSetting setting, GuiBuilderContext context) { + context.builder().addLine(IShowLineColorSetting.GUI_LINE_SHOW_LINE_COLOR_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.COLOR_PALETTE.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), IShowLineColorSetting.textShowLineColor.getString(), setting.showLineColor(), (cb) -> setting.setShowLineColor(cb.isChecked())) { + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(context.container().getParentScreen(), this, List.of(IShowLineColorSetting.textShowLineColorDescription), context.container().getParentScreen().width() / 3, graphics, mouseX, mouseY); + } + }); + }); + } + + static void buildShowConnectionGui(IShowNextConnections setting, GuiBuilderContext context) { + context.builder().addLine(IShowNextConnections.GUI_LINE_SHOW_CONNECTIONS_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.CONNECTIONS.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), IShowNextConnections.textShowConnections.getString(), setting.showConnections(), (cb) -> setting.setShowConnection(cb.isChecked()))); + }); + } + + static void buildShowTimeAndDateGui(IShowTimeAndDateSetting setting, GuiBuilderContext context) { + context.builder().addLine(IShowTimeAndDateSetting.GUI_LINE_SHOW_TIME_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TIME.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), IShowTimeAndDateSetting.textShowStats.getString(), setting.showTimeAndDate(), (cb) -> setting.setShowTimeAndDate(cb.isChecked()))); + }); + } + + static void buildShowStatsGui(IShowTrainStatsSetting setting, GuiBuilderContext context) { + context.builder().addLine(IShowTrainStatsSetting.GUI_LINE_SHOW_ARRIVAL_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TRAIN_INFO.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 4, line.y() + line.height() / 2 - 8, line.getRemainingWidth(), IShowTrainStatsSetting.textShowStats.getString(), setting.showStats(), (cb) -> setting.setShowStats(cb.isChecked()))); + }); + } + + static void buildTimeDisplayGui(ITimeDisplaySetting setting, GuiBuilderContext context) { + context.builder().addLine(ITimeDisplaySetting.GUI_LINE_TIME_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TIME.getAsSprite(16, 16))); + line.add(new DLCreateSelectionScrollInput(context.container().getParentScreen(), line.getCurrentX() + 6, line.y() + 2, 32, 18)) + .setRenderArrow(true) + .forOptions(Arrays.stream(ETimeDisplay.values()).map(x -> TextUtils.translate(x.getValueInfoTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) + .titled(TextUtils.translate("enum.createrailwaysnavigator.time_display")) + .addHint(TextUtils.translate("enum.createrailwaysnavigator.time_display.description")) + .format((val) -> { + return TextUtils.translate(ETimeDisplay.getById(val).getValueTranslationKey(CreateRailwaysNavigator.MOD_ID)); + }) + .setState(setting.getTimeDisplay().getId()) + .calling((i) -> { + setting.setTimeDisplay(ETimeDisplay.getById(i)); + }) + ; + }); + } + + static void buildTrainNameGui(ITrainNameWidthSetting setting, GuiBuilderContext context, boolean allowAuto, boolean allowMax) { + context.builder().addToLine(ITrainNameWidthSetting.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - ITrainNameWidthSetting.USED_LINE_SPACE) / 4 - 3; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18)) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description")) + .withRange(ITrainNameWidthSetting.MIN_VALUE - (allowAuto ? 1 : 0), ITrainNameWidthSetting.MAX_VALUE + (allowMax ? 1 : 0)) + .withShiftStep(5) + .setState(setting.getTrainNameWidth()) + .format((val) -> { + if (val < 0) { + return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); + } else if (val >= 100) { + return TextUtils.translate("gui.createrailwaysnavigator.common.max"); + } + return TextUtils.text(String.valueOf(val) + "px"); + }) + .calling((i) -> { + setting.setTrainNameWidth(i.byteValue()); + }) + ; + }); + } + + static void buildTrainTextGui(ITrainTextSetting setting, GuiBuilderContext context) { + context.builder().addLine(ITrainTextSetting.GUI_LINE_SHOW_ARRIVAL_NAME, (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TEXT.getAsSprite(16, 16))); + line.add(new DLCreateSelectionScrollInput(context.container().getParentScreen(), line.getCurrentX() + 6, line.y() + 2, line.getRemainingWidth() - 6, 18)) + .setRenderArrow(true) + .forOptions(Arrays.stream(ETrainTextComponents.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) + .titled(TextUtils.translate("enum.createrailwaysnavigator.train_text_components")) + .addHint(TextUtils.translate("enum.createrailwaysnavigator.train_text_components.description")) + .format((val) -> { + return TextUtils.translate(ETrainTextComponents.getById(val).getValueTranslationKey(CreateRailwaysNavigator.MOD_ID)); + }) + .setState(setting.getTrainTextComponents().getId()) + .calling((i) -> { + setting.setTrainTextComponents(ETrainTextComponents.getById(i)); + }) + ; + }); + } + + public static void buildPlatformDisplayFocusGui(PlatformDisplayFocusSettings setting, GuiBuilderContext context) { + context.builder().addToLine(PlatformDisplayFocusSettings.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - PlatformDisplayFocusSettings.USED_LINE_SPACE) / 4 - 3; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18)) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width_table")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description")) + .withRange(-1, 100) + .withShiftStep(5) + .setState(setting.getTrainNameWidth()) + .format((val) -> { + if (val < 0) { + return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); + } else if (val >= 100) { + return TextUtils.translate("gui.createrailwaysnavigator.common.max"); + } + return TextUtils.text(String.valueOf(val) + "px"); + }) + .calling((i) -> { + setting.setTrainNameWidth(i.byteValue()); + }) + ; + }); + context.builder().addToLine(PlatformDisplayFocusSettings.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - PlatformDisplayFocusSettings.USED_LINE_SPACE) / 4 - 3; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18)) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width_table")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description")) + .withRange(-1, 65) + .withShiftStep(4) + .setState(setting.getPlatformWidth()) + .format((val) -> { + if (val >= 0) { + return TextUtils.text(String.valueOf(val) + "px"); + } + return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); + }) + .calling((i) -> { + setting.setPlatformWidth(i.byteValue()); + }) + ; + }); + + + context.builder().addToLine(PlatformDisplayFocusSettings.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - PlatformDisplayFocusSettings.USED_LINE_SPACE) / 4 - 3; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18)) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width_next")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description")) + .withRange(-1, 100) + .withShiftStep(5) + .setState(setting.getTrainNameWidthNextStop()) + .format((val) -> { + if (val < 0) { + return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); + } else if (val >= 100) { + return TextUtils.translate("gui.createrailwaysnavigator.common.max"); + } + return TextUtils.text(String.valueOf(val) + "px"); + }) + .calling((i) -> { + setting.setTrainNameWidthNextStop(i.byteValue()); + }) + ; + }); + context.builder().addToLine(PlatformDisplayFocusSettings.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - PlatformDisplayFocusSettings.USED_LINE_SPACE) / 4 - 3; + line.add(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18)) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width_next")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description")) + .withRange(-1, 65) + .withShiftStep(4) + .setState(setting.getPlatformWidthNextStop()) + .format((val) -> { + if (val >= 0) { + return TextUtils.text(String.valueOf(val) + "px"); + } + return TextUtils.translate("gui.createrailwaysnavigator.common.auto"); + }) + .calling((i) -> { + setting.setPlatformWidthNextStop(i.byteValue()); + }) + ; + }); + } + + public static void buildDepartureBoardTableGui(DepartureBoardDisplayTableSettings setting, GuiBuilderContext context) { + MutableSingle stopovers = new MutableSingle(null); + MutableSingle info = new MutableSingle(null); + context.builder().addToLine(DepartureBoardDisplayTableSettings.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - DepartureBoardDisplayTableSettings.USED_LINE_SPACE) / 4 - 3; + stopovers.setFirst(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.stopovers_width")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.stopovers_width.description")) + .withRange(0, 101) + .withShiftStep(5) + .setState((int)(setting.getStopoversWidthPercentage() * 100)) + .format((val) -> { + return TextUtils.text(String.valueOf(val) + "%"); + }) + .calling((i) -> { + setting.setStopoversWidthPercentageInt(i.byteValue()); + DLUtils.doIfNotNull(info.getFirst(), x -> x.withRange(0, MathUtils.clamp(101 - i, 0, 101))); + }) + ); + line.add(stopovers.getFirst()); + if (stopovers.getFirst() != null && info.getFirst() != null) { + stopovers.getFirst().withRange(0, MathUtils.clamp(101 - info.getFirst().getState(), 0, 101)); + info.getFirst().withRange(0, MathUtils.clamp(101 - stopovers.getFirst().getState(), 0, 101)); + } + }); + context.builder().addToLine(DepartureBoardDisplayTableSettings.GUI_LINE_TEXT_SIZE_NAME, (line) -> { + int w = (line.getWidth() - DepartureBoardDisplayTableSettings.USED_LINE_SPACE) / 4 - 3; + info.setFirst(new DLCreateScrollInput(context.container().getParentScreen(), line.getCurrentX() + 4, line.y() + 2, w, 18) + .titled(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.info_width")) + .addHint(TextUtils.translate("gui.createrailwaysnavigator.display_source.advanced_display.info_width.description")) + .withRange(0, 101) + .withShiftStep(5) + .setState((int)(setting.getInfoWidthPercentage() * 100)) + .format((val) -> { + return TextUtils.text(String.valueOf(val) + "%"); + }) + .calling((i) -> { + setting.setInfoWidthPercentageInt(i.byteValue()); + DLUtils.doIfNotNull(stopovers.getFirst(), x -> x.withRange(0, MathUtils.clamp(101 - i, 0, 101))); + }) + ); + line.add(info.getFirst()); + if (stopovers.getFirst() != null && info.getFirst() != null) { + stopovers.getFirst().withRange(0, MathUtils.clamp(101 - info.getFirst().getState(), 0, 101)); + info.getFirst().withRange(0, MathUtils.clamp(101 - stopovers.getFirst().getState(), 0, 101)); + } + }); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICarriageIndexSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICarriageIndexSetting.java new file mode 100644 index 00000000..a77dd992 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICarriageIndexSetting.java @@ -0,0 +1,36 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface ICarriageIndexSetting { + + public static final String GUI_LINE_CARRIAGE_INDEX_NAME = "carriage_index"; + + public static final String NBT_CARRIAGE_INDEX = "CarriageIndexOffset"; + public static final String NBT_OVERWRITE_CARRIAGE_INDEX = "OverwriteCarriageIndex"; + + public static final MutableComponent textOverwriteCarriageIndex = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.overwrite_carriage_index"); + public static final MutableComponent textOverwriteCarriageIndexDescription = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.overwrite_carriage_index.description"); + + byte getCarriageIndex(); + boolean shouldOverwriteCarriageIndex(); + void setCarriageIndex(byte b); + void setOverwriteCarriageIndex(boolean b); + + @Environment(EnvType.CLIENT) + default void buildCarriageIndexGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildCarriageIndexGui(this, context); + } + + default void copyCarriageIndexSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof ICarriageIndexSetting o) { + setCarriageIndex(o.getCarriageIndex()); + setOverwriteCarriageIndex(o.shouldOverwriteCarriageIndex()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IColorSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IColorSetting.java new file mode 100644 index 00000000..7e27cdaa --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IColorSetting.java @@ -0,0 +1,39 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.MutableComponent; + +public interface IColorSetting { + + public static final String GUI_LINE_COLORS_NAME = "color"; + + public static final String NBT_FONT_COLOR = "FontColor"; + public static final String NBT_BACK_COLOR = "BackColor"; + + public static final MutableComponent textFontColor = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.font_color"); + public static final MutableComponent textBackColor = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.back_color"); + public static final MutableComponent textClickToEdit = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.click_to_edit").withStyle(ChatFormatting.DARK_GRAY).withStyle(ChatFormatting.ITALIC); + + int getFontColor(); + int getBackColor(); + void setFontColor(int color); + void setBackColor(int color); + + @Environment(EnvType.CLIENT) + default void buildColorGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildColorGui(this, context); + } + + default void copyColorSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IColorSetting o) { + setBackColor(o.getBackColor()); + setFontColor(o.getFontColor()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICustomTextWidthSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICustomTextWidthSetting.java new file mode 100644 index 00000000..b173a1ab --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ICustomTextWidthSetting.java @@ -0,0 +1,16 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +public interface ICustomTextWidthSetting { + public static final String GUI_LINE_TEXT_SIZE_NAME = "text_width"; + + public static final int USED_LINE_SPACE = 18 + 4; + + @Environment(EnvType.CLIENT) + default void buildBasicTextWidthGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildBasicTextWidthGui(this, context); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IPlatformWidthSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IPlatformWidthSetting.java new file mode 100644 index 00000000..03b6859a --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IPlatformWidthSetting.java @@ -0,0 +1,36 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +/** + * For data conversion: Indicates that this class adopts the original + * property {@code platformWidth} from the Advanced Displays. + * If the class should adopt this property, this interface must be + * implemented or the value will not be converted! + */ +public interface IPlatformWidthSetting extends ICustomTextWidthSetting { + + public static final String NBT_PLATFORM_WIDTH = "PlatformWidth"; + + byte getPlatformWidth(); + void setPlatformWidth(byte b); + + @Environment(EnvType.CLIENT) + default void buildPlatformWidthGui(GuiBuilderContext context, boolean allowAuto) { + buildBasicTextWidthGui(context); + GuiBuilderWrapper.buildPlatformWidthGui(this, context, allowAuto); + } + + default void copyPlatformWidthSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IPlatformWidthSetting o) { + setPlatformWidth(o.getPlatformWidth()); + } + } + + default boolean isAutoPlatformWidth() { + return getPlatformWidth() < 0; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowArrivalSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowArrivalSetting.java new file mode 100644 index 00000000..eba880cb --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowArrivalSetting.java @@ -0,0 +1,33 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface IShowArrivalSetting { + + public static final String GUI_LINE_SHOW_ARRIVAL_NAME = "show_arrival"; + + public static final String NBT_SHOW_ARRIVAL = "ShowArrival"; + + public static final MutableComponent textShowArrival = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_arrival"); + public static final MutableComponent textShowArrivalDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_arrival.description"); + + boolean showArrival(); + void setShowArrival(boolean b); + + @Environment(EnvType.CLIENT) + default void buildShowArrivalGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildShowArrivalGui(this, context); + } + + default void copyShowArrivalSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IShowArrivalSetting o) { + setShowArrival(o.showArrival()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowExitDirectionSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowExitDirectionSetting.java new file mode 100644 index 00000000..48fe7216 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowExitDirectionSetting.java @@ -0,0 +1,32 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface IShowExitDirectionSetting { + + public static final String GUI_LINE_SHOW_ARRIVAL_NAME = "show_exit"; + + public static final String NBT_SHOW_EXIT = "ShowExit"; + + public static final MutableComponent textShowExit = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_exit"); + + boolean showExit(); + void setShowExit(boolean b); + + @Environment(EnvType.CLIENT) + default void buildShowExitGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildShowExitGui(this, context); + } + + default void copyShowExitSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IShowExitDirectionSetting o) { + setShowExit(o.showExit()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowLineColorSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowLineColorSetting.java new file mode 100644 index 00000000..abfcfb95 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowLineColorSetting.java @@ -0,0 +1,33 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface IShowLineColorSetting { + + public static final String GUI_LINE_SHOW_LINE_COLOR_NAME = "show_line_color"; + + public static final String NBT_SHOW_LINE_COLOR = "ShowLineColor"; + + public static final MutableComponent textShowLineColor = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_line_color"); + public static final MutableComponent textShowLineColorDescription = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_line_color.description"); + + boolean showLineColor(); + void setShowLineColor(boolean b); + + @Environment(EnvType.CLIENT) + default void buildShowLineColorGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildShowLineColorGui(this, context); + } + + default void copyShowLineColorSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IShowLineColorSetting o) { + setShowLineColor(o.showLineColor()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowNextConnections.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowNextConnections.java new file mode 100644 index 00000000..a855a6eb --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowNextConnections.java @@ -0,0 +1,32 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface IShowNextConnections { + + public static final String GUI_LINE_SHOW_CONNECTIONS_NAME = "show_connections"; + + public static final String NBT_SHOW_CONNECTIONS = "ShowConnections"; + + public static final MutableComponent textShowConnections = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_connections"); + + boolean showConnections(); + void setShowConnection(boolean b); + + @Environment(EnvType.CLIENT) + default void buildShowConnectionGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildShowConnectionGui(this, context); + } + + default void copyShowConnectionSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IShowNextConnections o) { + setShowConnection(o.showConnections()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTimeAndDateSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTimeAndDateSetting.java new file mode 100644 index 00000000..f1c3d804 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTimeAndDateSetting.java @@ -0,0 +1,32 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface IShowTimeAndDateSetting { + + public static final String GUI_LINE_SHOW_TIME_NAME = "show_time"; + + public static final String NBT_SHOW_TIME_AND_DATE = "ShowTime"; + + public static final MutableComponent textShowStats = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_time_and_date"); + + boolean showTimeAndDate(); + void setShowTimeAndDate(boolean b); + + @Environment(EnvType.CLIENT) + default void buildShowTimeAndDateGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildShowTimeAndDateGui(this, context); + } + + default void copyShowTimeAndDateSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IShowTimeAndDateSetting o) { + setShowTimeAndDate(o.showTimeAndDate()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTrainStatsSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTrainStatsSetting.java new file mode 100644 index 00000000..f8819d47 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/IShowTrainStatsSetting.java @@ -0,0 +1,32 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.network.chat.MutableComponent; + +public interface IShowTrainStatsSetting { + + public static final String GUI_LINE_SHOW_ARRIVAL_NAME = "show_stats"; + + public static final String NBT_SHOW_STATS = "ShowStats"; + + public static final MutableComponent textShowStats = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".advanced_display_settings.show_stats"); + + boolean showStats(); + void setShowStats(boolean b); + + @Environment(EnvType.CLIENT) + default void buildShowStatsGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildShowStatsGui(this, context); + } + + default void copyShowStatsSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof IShowTrainStatsSetting o) { + setShowStats(o.showStats()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITimeDisplaySetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITimeDisplaySetting.java new file mode 100644 index 00000000..54b48bbc --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITimeDisplaySetting.java @@ -0,0 +1,33 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +/** + * For data conversion: Indicates that this class adopts the original + * property {@code timeDisplay} from the Advanced Displays. + * If the class should adopt this property, this interface must be + * implemented or the value will not be converted! + */ +public interface ITimeDisplaySetting extends ICustomTextWidthSetting { + + public static final String GUI_LINE_TIME_NAME = "time"; + public static final String NBT_TIME_DISPLAY = "TimeDisplay"; + + ETimeDisplay getTimeDisplay(); + void setTimeDisplay(ETimeDisplay display); + + @Environment(EnvType.CLIENT) + default void buildTimeDisplayGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildTimeDisplayGui(this, context); + } + + default void copyTimeDisplaySetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof ITimeDisplaySetting o) { + setTimeDisplay(o.getTimeDisplay()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainNameWidthSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainNameWidthSetting.java new file mode 100644 index 00000000..bc079b4d --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainNameWidthSetting.java @@ -0,0 +1,43 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +/** + * For data conversion: Indicates that this class adopts the original + * property {@code timeDisplay} from the Advanced Displays. + * If the class should adopt this property, this interface must be + * implemented or the value will not be converted! + */ +public interface ITrainNameWidthSetting extends ICustomTextWidthSetting { + + public static final int DEFAULT_TRAIN_NAME_WIDTH = 16; + public static final int MIN_VALUE = 0; + public static final int MAX_VALUE = 100; + public static final String NBT_TRAIN_NAME_WIDTH = "TrainNameWidth"; + + byte getTrainNameWidth(); + void setTrainNameWidth(byte b); + + @Environment(EnvType.CLIENT) + default void buildTrainNameGui(GuiBuilderContext context, boolean allowAuto, boolean allowMax) { + buildBasicTextWidthGui(context); + GuiBuilderWrapper.buildTrainNameGui(this, context, allowAuto, allowMax); + } + + default void copyTrainNameSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof ITrainNameWidthSetting o) { + setTrainNameWidth(o.getTrainNameWidth()); + } + } + + default boolean isAutoTrainNameWidth() { + return getTrainNameWidth() < MIN_VALUE; + } + + default boolean isFullTrainNameWidth() { + return getTrainNameWidth() >= MAX_VALUE; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainTextSetting.java b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainTextSetting.java new file mode 100644 index 00000000..2477f364 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/display/properties/components/ITrainTextSetting.java @@ -0,0 +1,76 @@ +package de.mrjulsen.crn.block.display.properties.components; + +import java.util.Arrays; + +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.util.StringRepresentable; + +public interface ITrainTextSetting { + + public static enum ETrainTextComponents implements StringRepresentable, ITranslatableEnum { + ALL((byte)0, "all"), + TRAIN_NAME((byte)1, "train_name"), + DESTINATION((byte)2, "destination"); + + final String name; + final byte id; + + ETrainTextComponents(byte id, String name) { + this.name = name; + this.id = id; + } + + public byte getId() { + return this.id; + } + + public static ETrainTextComponents getById(int id) { + return Arrays.stream(values()).filter(x -> x.getId() == (byte)id).findFirst().orElse(ETrainTextComponents.ALL); + } + + @Override + public String getSerializedName() { + return name; + } + + @Override + public String getEnumName() { + return "train_text_components"; + } + + @Override + public String getEnumValueName() { + return this.name; + } + + public boolean showTrainName() { + return this == ALL || this == TRAIN_NAME; + } + + public boolean showDestination() { + return this == ALL || this == DESTINATION; + } + } + + public static final String GUI_LINE_SHOW_ARRIVAL_NAME = "train_text"; + + public static final String NBT_TRAIN_TEXT = "TrainText"; + + ETrainTextComponents getTrainTextComponents(); + void setTrainTextComponents(ETrainTextComponents v); + + @Environment(EnvType.CLIENT) + default void buildTrainTextGui(GuiBuilderContext context) { + GuiBuilderWrapper.buildTrainTextGui(this, context); + } + + default void copyTrainTextSetting(IDisplaySettings oldSettings) { + if (oldSettings instanceof ITrainTextSetting o) { + setTrainTextComponents(o.getTrainTextComponents()); + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java b/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java index 2cb9b15b..7424b3db 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java +++ b/common/src/main/java/de/mrjulsen/crn/block/properties/EDisplayType.java @@ -9,7 +9,8 @@ public enum EDisplayType implements StringRepresentable, ITranslatableEnum { TRAIN_DESTINATION((byte)0, "train_destination", ModGuiIcons.TRAIN_DESTINATION, EDisplayTypeDataSource.TRAIN_INFORMATION), PASSENGER_INFORMATION((byte)1, "passenger_information", ModGuiIcons.PASSENGER_INFORMATION, EDisplayTypeDataSource.TRAIN_INFORMATION), - PLATFORM((byte)2, "platform", ModGuiIcons.PLATFORM_INFORMATION, EDisplayTypeDataSource.PLATFORM); + PLATFORM((byte)2, "platform", ModGuiIcons.PLATFORM_INFORMATION, EDisplayTypeDataSource.PLATFORM), + DEPARTURE_BOARD((byte)3, "departure_board", ModGuiIcons.PLATFORM_INFORMATION, EDisplayTypeDataSource.PLATFORM); private String name; private byte id; @@ -43,6 +44,10 @@ public static EDisplayType getTypeById(int id) { return Arrays.stream(values()).filter(x -> x.getId() == (byte)id).findFirst().orElse(EDisplayType.TRAIN_DESTINATION); } + public static EDisplayType getTypeByName(String name) { + return Arrays.stream(values()).filter(x -> x.name.equals(name)).findFirst().orElse(EDisplayType.TRAIN_DESTINATION); + } + @Override public String getSerializedName() { return name; diff --git a/common/src/main/java/de/mrjulsen/crn/block/properties/ETimeDisplay.java b/common/src/main/java/de/mrjulsen/crn/block/properties/ETimeDisplay.java new file mode 100644 index 00000000..56580e4f --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/block/properties/ETimeDisplay.java @@ -0,0 +1,47 @@ +package de.mrjulsen.crn.block.properties; + +import java.util.Arrays; + +import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; +import net.minecraft.util.StringRepresentable; + +public enum ETimeDisplay implements StringRepresentable, ITranslatableEnum { + ABS((byte)0, "abs"), + ETA((byte)1, "eta"); + + private byte id; + private String name; + + private ETimeDisplay(byte id, String name) { + this.id = id; + this.name = name; + } + + public byte getId() { + return id; + } + + public String getName() { + return name; + } + + public static ETimeDisplay getById(int id) { + return Arrays.stream(values()).filter(x -> x.getId() == id).findFirst().orElse(ABS); + } + + @Override + public String getEnumName() { + return "time_display"; + } + + @Override + public String getEnumValueName() { + return getName(); + } + + @Override + public String getSerializedName() { + return getName(); + } + +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java b/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java index fe56c601..c413ef5b 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java +++ b/common/src/main/java/de/mrjulsen/crn/client/AdvancedDisplaysRegistry.java @@ -10,92 +10,135 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.display.properties.BasicDisplaySettings; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; import de.mrjulsen.crn.block.properties.EDisplayType; -import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; +import de.mrjulsen.crn.registry.ModDisplayTypes; +import de.mrjulsen.crn.client.ber.variants.AbstractAdvancedDisplayRenderer; import de.mrjulsen.crn.client.ber.variants.BERError; -import de.mrjulsen.mcdragonlib.data.Pair; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; public final class AdvancedDisplaysRegistry { - public static record DisplayTypeResourceKey(EDisplayType category, ResourceLocation id) { - private static final String NBT_CATEGORY = "Category"; - private static final String NBT_ID = "Id"; + + public static record DisplayTypeResourceKey(EDisplayType category, String name) { + private static final String NBT_ID = "DisplayId"; + @Deprecated private static final String LEGACY_NBT_ID = "Id"; + @Deprecated private static final String LEGACY_NBT_CATEGORY = "Category"; + @Override public final boolean equals(Object arg) { - return arg instanceof DisplayTypeResourceKey o && category == o.category && id.equals(o.id); + return arg instanceof DisplayTypeResourceKey o && category() == o.category() && name().equals(o.name()); } + @Override public final int hashCode() { - return Objects.hash(category, id); + return Objects.hash(category(), name()); + } + + public void toNbt(CompoundTag nbt) { + nbt.putString(NBT_ID, getLocation().toString()); } - public CompoundTag toNbt() { - CompoundTag nbt = new CompoundTag(); - nbt.putByte(NBT_CATEGORY, category().getId()); - nbt.putString(NBT_ID, id().toString()); - return nbt; + + public ResourceLocation getLocation() { + return new ResourceLocation(CreateRailwaysNavigator.MOD_ID, category().getInfoTypeName() + "/" + name()); + } + + @Deprecated + public static DisplayTypeResourceKey legacy_fromNbt(CompoundTag nbt) { + return new DisplayTypeResourceKey(EDisplayType.getTypeById(nbt.getByte(LEGACY_NBT_CATEGORY)), new ResourceLocation(nbt.getString(LEGACY_NBT_ID)).getPath()); } + public static DisplayTypeResourceKey fromNbt(CompoundTag nbt) { - return new DisplayTypeResourceKey(EDisplayType.getTypeById(nbt.getByte(NBT_CATEGORY)), new ResourceLocation(nbt.getString(NBT_ID))); + String id = nbt.getString(NBT_ID); + String[] data = new ResourceLocation(id).getPath().split("/"); + return new DisplayTypeResourceKey(data.length > 0 ? EDisplayType.getTypeByName(data[0]) : ModDisplayTypes.TRAIN_DESTINATION_SIMPLE.category(), data.length > 1 ? data[1] : ModDisplayTypes.TRAIN_DESTINATION_SIMPLE.name()); } + public String getTranslationKey() { - return "display." + CreateRailwaysNavigator.MOD_ID + "." + category().getEnumValueName() + "." + id.getPath(); + return "display." + CreateRailwaysNavigator.MOD_ID + "." + category().getEnumValueName() + "." + name(); + } + + @Override + public final String toString() { + return "DisplayType[" + category().getEnumValueName() + "/" + name() + "]"; } } + + /** + * Contains all information about the registered display. + */ + protected static record DisplayRegistrationData>(Supplier customizationSettings, Supplier renderer, DisplayProperties properties) {} /** - * @param singleLined Whether the display can be connected vertically. + * Constant, server-side properties that define the display in more detail. + * @param singleLined Whether the display can be connected vertically or not. * @param platformDisplayTrainsCount For Platform Displays only! Specifies how many trains can be shown on the display, depending on the properties of the display. If used correctly, this reduces network traffic, as data about trains that do not fit on the display are not transferred from the server. */ - public static record DisplayTypeInfo(boolean singleLined, Function platformDisplayTrainsCount) {} + public static record DisplayProperties(boolean singleLined, Function platformDisplayTrainsCount) {} - private static final Map>, DisplayTypeInfo>>> displayTypes = new HashMap<>(); + //private static final Map>, DisplayProperties>>> displayTypes = new HashMap<>(); + private static final Map>> newDisplayTypes = new HashMap<>(); /** * Registers a new display type that can then be used in CRN. + * @param The display settings type used by the renderer. + * @param The type of the display renderer. * @param category The display category to which the type should be assigned. - * @param id The id of the display type. - * @param displayRenderer The reference of the Renderer class that renders the contents of the display. - * @param info Additional information about this display type - * @return + * @param name The name of the display type. Must be unique in each category! + * @param settings A class containing all customization options for this specific display type. + * @param renderer The reference of the renderer class that renders the contents of the display. + * @param properties Additional constant properties of the display type. + * @return The key of the registered display type. */ - public static DisplayTypeResourceKey registerDisplayType(EDisplayType category, ResourceLocation id, Supplier> displayRenderer, DisplayTypeInfo info) { - Map>, DisplayTypeInfo>> reg = displayTypes.computeIfAbsent(category, x -> new HashMap<>()); - if (reg.containsKey(id)) { - throw new IllegalArgumentException("A display type with the id '" + id + "' is already registered!"); + public static > DisplayTypeResourceKey register(EDisplayType category, String name, Supplier settings, Supplier renderer, DisplayProperties properties) { + DisplayTypeResourceKey key = new DisplayTypeResourceKey(category, name); + Map> reg = newDisplayTypes.computeIfAbsent(category, x -> new HashMap<>()); + + if (reg.containsKey(name)) { + throw new IllegalArgumentException("A display type with the id '" + key + "' is already registered!"); } - reg.put(id, Pair.of(displayRenderer, info)); - return new DisplayTypeResourceKey(category, id); + reg.put(name, new DisplayRegistrationData<>(settings, renderer, properties)); + return key; } public static boolean isRegietered(DisplayTypeResourceKey key) { - return key != null && displayTypes.containsKey(key.category()) && displayTypes.get(key.category()).containsKey(key.id()); + return key != null && newDisplayTypes.containsKey(key.category()) && newDisplayTypes.get(key.category()).containsKey(key.name()); } - public static IBERRenderSubtype getRenderer(DisplayTypeResourceKey key) { + @Environment(EnvType.CLIENT) + public static AbstractAdvancedDisplayRenderer createRenderer(DisplayTypeResourceKey key) { if (!isRegietered(key)) { return new BERError(); } - return displayTypes.get(key.category()).get(key.id()).getFirst().get(); + return newDisplayTypes.get(key.category()).get(key.name()).renderer().get(); + } + + public static IDisplaySettings createSettings(DisplayTypeResourceKey key) { + if (!isRegietered(key)) { + return new BasicDisplaySettings(); + } + return newDisplayTypes.get(key.category()).get(key.name()).customizationSettings().get(); } - public static DisplayTypeInfo getInfo(DisplayTypeResourceKey key) { + public static DisplayProperties getProperties(DisplayTypeResourceKey key) { if (!isRegietered(key)) { - return new DisplayTypeInfo(true, $ -> 0); + return new DisplayProperties(true, $ -> 0); } - return displayTypes.get(key.category()).get(key.id()).getSecond(); + return newDisplayTypes.get(key.category()).get(key.name()).properties(); } - public static Map getAllOfType(EDisplayType type) { - return displayTypes.get(type).entrySet().stream().collect(Collectors.toMap(a -> a.getKey(), b -> b.getValue().getSecond())); + public static Map getAllOfType(EDisplayType type) { + return newDisplayTypes.get(type).entrySet().stream().collect(Collectors.toMap(a -> a.getKey(), b -> b.getValue().properties())); } public static List getAllOfTypeAsKey(EDisplayType type) { - return displayTypes.get(type).entrySet().stream().map(x -> new DisplayTypeResourceKey(type, x.getKey())).toList(); + return newDisplayTypes.get(type).entrySet().stream().map(x -> new DisplayTypeResourceKey(type, x.getKey())).toList(); } - public static List getAllIdsOfType(EDisplayType type) { - return displayTypes.get(type).entrySet().stream().map(x -> x.getKey()).toList(); + public static List getAllNamesOfType(EDisplayType type) { + return newDisplayTypes.get(type).entrySet().stream().map(x -> x.getKey()).toList(); } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java b/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java index 7385ce21..35d48965 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ClientWrapper.java @@ -18,12 +18,14 @@ import de.mrjulsen.crn.client.gui.NavigatorToast; import de.mrjulsen.crn.client.gui.screen.AdvancedDisplaySettingsScreen; import de.mrjulsen.crn.client.gui.screen.NavigatorScreen; +import de.mrjulsen.crn.client.gui.screen.TrainSeparationSettingsScreen; import de.mrjulsen.crn.client.gui.screen.TrainDebugScreen; import de.mrjulsen.crn.client.gui.screen.TrainSectionSettingsScreen; import de.mrjulsen.crn.client.gui.widgets.ResizableButton; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; +import de.mrjulsen.crn.data.schedule.condition.TrainSeparationCondition; import de.mrjulsen.crn.data.schedule.instruction.ResetTimingsInstruction; import de.mrjulsen.crn.data.schedule.instruction.TravelSectionInstruction; import de.mrjulsen.crn.mixin.ModularGuiLineBuilderAccessor; @@ -53,7 +55,7 @@ public class ClientWrapper { - private static ELanguage currentLanguage; + private static CustomLanguage currentLanguage; private static Language currentClientLanguage; public static void showNavigatorGui() { @@ -73,14 +75,14 @@ public static void showAdvancedDisplaySettingsScreen(AdvancedDisplayBlockEntity DLScreen.setScreen(new AdvancedDisplaySettingsScreen(blockEntity)); } - public static void updateLanguage(ELanguage lang, boolean force) { + public static void updateLanguage(CustomLanguage lang, boolean force) { if (currentLanguage == lang && !force) { return; } - LanguageInfo info = lang == ELanguage.DEFAULT ? null : Minecraft.getInstance().getLanguageManager().getLanguage(lang.getCode()); + LanguageInfo info = lang == CustomLanguage.DEFAULT ? null : Minecraft.getInstance().getLanguageManager().getLanguage(lang.getCode()); currentLanguage = lang; - if (lang == ELanguage.DEFAULT || info == null) { + if (lang == CustomLanguage.DEFAULT || info == null) { currentClientLanguage = Language.getInstance(); CreateRailwaysNavigator.LOGGER.info("Updated custom language to: (Default)"); } else { @@ -122,7 +124,7 @@ public static void initScheduleSectionInstruction(TravelSectionInstruction instr ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; - ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 121, 16, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction." + instruction.getId().getPath() + ".configure"), + ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 121, 16, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction.configure"), (b) -> { if (Minecraft.getInstance().screen instanceof ScheduleScreen scheduleScreen) { ((ScheduleScreenAccessor)scheduleScreen).crn$getOnEditorClose().accept(true); @@ -195,4 +197,58 @@ public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float part }; accessor.crn$getTarget().add(Pair.of(btn, "help_btn")); } + + @SuppressWarnings("resource") + public static void initTimingAdjustmentGui(TrainSeparationCondition condition, ModularGuiLineBuilder builder) { + + /* + builder.addScrollInput(0, 26, (i, l) -> { + i.titled(Lang.translateDirect("generic.duration")) + .withShiftStep(15) + .withRange(0, 121); + i.lockedTooltipX = -15; + i.lockedTooltipY = 35; + }, "Value"); + + builder.addSelectionScrollInput(26, 15, (i, l) -> { + i.forOptions(TimeUnit.translatedOptions()) + .titled(Lang.translateDirect("generic.timeUnit")) + .format((idx) -> { + return TextUtils.text(switch (TimeUnit.values()[idx]) { + case MINUTES -> "m"; + case SECONDS -> "s"; + default -> "t"; + }); + }) + ; + }, "TimeUnit"); + + builder.addSelectionScrollInput(41, 80, (i, l) -> { + i.forOptions(Arrays.stream(ETrainFilter.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) + .titled(TextUtils.translate(ETrainFilter.ANY.getEnumTranslationKey(CreateRailwaysNavigator.MOD_ID))) + .addHint(TextUtils.translate(ETrainFilter.ANY.getEnumDescriptionTranslationKey(CreateRailwaysNavigator.MOD_ID))) + + ; + }, "TrainFilter"); + */ + + ModularGuiLineBuilderAccessor accessor = (ModularGuiLineBuilderAccessor)builder; + ResizableButton btn = new ResizableButton(accessor.crn$getX(), accessor.crn$getY() - 4, 121, 16, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.instruction.configure"), + (b) -> { + if (Minecraft.getInstance().screen instanceof ScheduleScreen scheduleScreen) { + ((ScheduleScreenAccessor)scheduleScreen).crn$getOnEditorClose().accept(true); + builder.customArea(0, 0).speechBubble(); + Minecraft.getInstance().setScreen(new TrainSeparationSettingsScreen(scheduleScreen, condition.getData())); + } + }) { + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + Graphics graphics = new Graphics(poseStack); + DynamicGuiRenderer.renderArea(graphics, x, y, width, height, AreaStyle.GRAY, isActive() ? (isFocused() || isMouseOver(mouseX, mouseY) ? ButtonState.SELECTED : ButtonState.BUTTON) : ButtonState.DISABLED); + int j = isActive() ? DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE : DragonLib.NATIVE_BUTTON_FONT_COLOR_DISABLED; + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x + width / 2, y + (height - 8) / 2, this.getMessage(), j, EAlignment.CENTER, true); + } + }; + accessor.crn$getTarget().add(Pair.of(btn, "config_btn")); + } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java b/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java index dc52bc28..63bec0c7 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/AdvancedDisplayRenderInstance.java @@ -9,6 +9,7 @@ import de.mrjulsen.crn.block.properties.ESide; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; +import de.mrjulsen.crn.client.ber.variants.AbstractAdvancedDisplayRenderer; import de.mrjulsen.mcdragonlib.client.ber.AbstractBlockEntityRenderInstance; import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.data.Pair; @@ -21,7 +22,7 @@ public class AdvancedDisplayRenderInstance extends AbstractBlockEntityRenderInstance { - public IBERRenderSubtype renderSubtype; + public AbstractAdvancedDisplayRenderer renderSubtype; private DisplayTypeResourceKey lastType; private int lastXSize = 0; @@ -32,7 +33,7 @@ public AdvancedDisplayRenderInstance(AdvancedDisplayBlockEntity blockEntity) { @Override public void render(BERGraphics graphics, float partialTick) { - if (!graphics.blockEntity().isController()) { + if (!graphics.blockEntity().isController() || renderSubtype == null) { return; } @@ -84,9 +85,9 @@ public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlo @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, Object data) { EUpdateReason reason = (EUpdateReason)data; - DisplayTypeResourceKey type = blockEntity.getDisplayTypeKey(); + DisplayTypeResourceKey type = blockEntity.getDisplayType(); if (lastType == null || !lastType.equals(type)) { - renderSubtype = AdvancedDisplaysRegistry.getRenderer(type); + renderSubtype = AdvancedDisplaysRegistry.createRenderer(type); } lastType = type; diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java b/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java index 1045c129..12ba953b 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/TrainStationClockRenderer.java @@ -31,12 +31,12 @@ public void render(BERGraphics graphics, float par BERUtils.renderTexture(DIAL_TEXTURE, graphics, !graphics.blockEntity().isGlowing(), -7, -7, -0.2f, 14, 14, 0, 0, 1, 1, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), (0xFF << 24) | (graphics.blockEntity().getColor()), graphics.blockEntity().isGlowing() ? LightTexture.FULL_BRIGHT : graphics.packedLight()); graphics.poseStack().pushPose(); - graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 12000))); + graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.daytimeShift(), 12000))); BERUtils.fillColor(graphics, -0.5f, -0.5f, 0, 6, 1, 0xFF191919, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); graphics.poseStack().popPose(); graphics.poseStack().pushPose(); - graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 1000))); + graphics.poseStack().mulPose(Vector3f.ZP.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.daytimeShift(), 1000))); BERUtils.fillColor(graphics, -0.5f, -0.5f, 0.1f, 7, 1, 0xFF222222, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); graphics.poseStack().popPose(); @@ -47,13 +47,13 @@ public void render(BERGraphics graphics, float par graphics.poseStack().popPose(); graphics.poseStack().pushPose(); - graphics.poseStack().mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 12000))); + graphics.poseStack().mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.daytimeShift(), 12000))); graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(180)); BERUtils.fillColor(graphics, -0.5f, -0.5f, 0, 6, 1, 0xFF191919, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); graphics.poseStack().popPose(); graphics.poseStack().pushPose(); - graphics.poseStack().mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.DAYTIME_SHIFT, 1000))); + graphics.poseStack().mulPose(Vector3f.ZN.rotationDegrees(-90 + ModUtils.clockHandDegrees(graphics.blockEntity().getLevel().getDayTime() + DragonLib.daytimeShift(), 1000))); graphics.poseStack().mulPose(Vector3f.YP.rotationDegrees(180)); BERUtils.fillColor(graphics, -0.5f, -0.5f, 0.1f, 7, 1, 0xFF222222, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); graphics.poseStack().popPose(); diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java new file mode 100644 index 00000000..62cab4a9 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java @@ -0,0 +1,22 @@ +package de.mrjulsen.crn.client.ber.variants; + +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; +import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; +import de.mrjulsen.crn.client.ber.IBERRenderSubtype; + +public interface AbstractAdvancedDisplayRenderer extends IBERRenderSubtype { + + public static final int DARK_FONT_COLOR = 0xFF111111; + public static final int LIGHT_FONT_COLOR = 0xFFEEEEEE; + + @SuppressWarnings("unchecked") + default T getDisplaySettings(AdvancedDisplayBlockEntity blockEntity) { + try { + return (T)blockEntity.getSettings(); + } catch (ClassCastException e) { + throw new IllegalArgumentException("Could not get display data of display at " + blockEntity.getBlockPos(), e); + } + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERDepartureBoardTable.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERDepartureBoardTable.java new file mode 100644 index 00000000..d76f1428 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERDepartureBoardTable.java @@ -0,0 +1,487 @@ +package de.mrjulsen.crn.client.ber.variants; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.block.display.properties.DepartureBoardDisplayTableSettings; +import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; +import de.mrjulsen.crn.client.lang.CustomLanguage; +import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; +import de.mrjulsen.crn.data.train.portable.StationDisplayData; +import de.mrjulsen.crn.util.ModUtils; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel; +import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.client.util.BERUtils; +import de.mrjulsen.mcdragonlib.util.ColorUtils; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.state.BlockState; + +public class BERDepartureBoardTable implements AbstractAdvancedDisplayRenderer { + + private static final String keyDeparture = "gui.createrailwaysnavigator.departure"; + private static final String keyTrain = "gui.createrailwaysnavigator.line"; + private static final String keyDestination = "gui.createrailwaysnavigator.destination"; + private static final String keyPlatform = "gui.createrailwaysnavigator.platform"; + private static final String keyVia = "gui.createrailwaysnavigator.via"; + private static final String keyTooSmall = "gui.createrailwaysnavigator.too_small"; + + private static final int MIN_SIZE = 4; + private static final float LINE_HEIGHT = 5.4f; + private static final float Y_OFFSET = LINE_HEIGHT; // Headline + private static final float SPACING = 2; + private static final float TIME_LABEL_MAX_WIDTH = 12; + private static final float REAL_TIME_LABEL_MAX_WIDTH = 12; + + private boolean showInfoLine = false; + private MutableComponent infoLineText = TextUtils.empty(); + private int maxLines = 0; + + private final BERLabel statusLabel = new BERLabel(TextUtils.empty()) + .setCentered(true) + .setScale(0.4f, 0.4f) + .setYScale(0.4f) + .setColor(0xFF111111) + .setBackground(0xFFFFFFFF, true) + .setScrollingSpeed(2) + ; + private final BERLabel tooSmallLabel = new BERLabel(TextUtils.translate(keyTooSmall)) // TODO + .setCentered(false) + .setScale(0.4f, 0.4f) + .setYScale(0.4f) + .setPos(3, 3) + ; + private BERLabel[][] lines = new BERLabel[0][]; + + private final BERLabel[] headlines; + { + headlines = new BERLabel[LineComponent.values().length]; + + headlines[LineComponent.TIME.i()] = new BERLabel() + .setText(CustomLanguage.translate(keyDeparture).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC)) + .setYScale(0.4f) + .setMaxWidth(12.5f, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setPos(0, 3) + .setMaxWidth(0, BoundsHitReaction.CUT_OFF) + ; + headlines[LineComponent.TRAIN_NAME.i()] = new BERLabel() + .setText(CustomLanguage.translate(keyTrain).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC)) + .setYScale(0.4f) + .setScrollingSpeed(2) + .setMaxWidth(14, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setPos(0, 3) + .setMaxWidth(0, BoundsHitReaction.CUT_OFF) + ; + + headlines[LineComponent.PLATFORM.i()] = new BERLabel() + .setText(CustomLanguage.translate(keyPlatform).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC)) + .setYScale(0.4f) + .setScale(0.4f, 0.2f) + .setPos(0, 3) + .setMaxWidth(0, BoundsHitReaction.CUT_OFF) + ; + + headlines[LineComponent.DESTINATION.i()] = new BERLabel() + .setText(CustomLanguage.translate(keyDestination).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC)) + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setPos(0, 3) + .setMaxWidth(0, BoundsHitReaction.CUT_OFF) + ; + headlines[LineComponent.STOPOVERS.i()] = new BERLabel() + .setText(CustomLanguage.translate(keyVia).withStyle(ChatFormatting.BOLD).withStyle(ChatFormatting.ITALIC)) + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setPos(0, 3) + .setMaxWidth(0, BoundsHitReaction.CUT_OFF) + ; + headlines[LineComponent.INFO.i()] = new BERLabel() + .setText(TextUtils.empty()) + .setPos(0, 3) + .setMaxWidth(0, BoundsHitReaction.CUT_OFF) + ; + } + + @Override + public void renderTick(float deltaTime) { + statusLabel.renderTick(); + DLUtils.doIfNotNull(lines, x -> { + for (int i = 0; i < x.length; i++) { + BERLabel[] line = x[i]; + if (line == null) continue; + for (int k = 0; k < line.length; k++) { + DLUtils.doIfNotNull(line[k], y -> y.renderTick()); + } + } + }); + for (int k = 0; k < headlines.length; k++) { + DLUtils.doIfNotNull(headlines[k], y -> y.renderTick()); + } + } + + @Override + public void render(BERGraphics graphics, float pPartialTicks, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { + + BERUtils.fillColor(graphics, 2, 1.5f + LINE_HEIGHT, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 4, 0.25f, (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); + if (graphics.blockEntity().getXSizeScaled() < MIN_SIZE) { + tooSmallLabel.render(graphics, light); + return; + } + + for (int k = 0; k < headlines.length; k++) { + DLUtils.doIfNotNull(headlines[k], y -> y.render(graphics, light)); + } + + for (int i = 0; i < lines.length && i < maxLines; i++) { + graphics.poseStack().pushPose(); + if (i % 2 == 1) { + BERUtils.fillColor(graphics, 2, 2 + Y_OFFSET + i * LINE_HEIGHT, 0, graphics.blockEntity().getXSizeScaled() * 16 - 4, LINE_HEIGHT, (0x40 << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING)); + graphics.poseStack().translate(0, 0, 0.05f); + } + for (int k = 0; k < lines[i].length; k++) { + DLUtils.doIfNotNull(lines[i][k], x -> x.render(graphics, light)); + } + graphics.poseStack().popPose(); + } + + if (showInfoLine) { + statusLabel.render(graphics, light); + } + } + + private Collection getStatusInfo(AdvancedDisplayBlockEntity blockEntity, StationDisplayData data) { + if (!data.getTrainData().hasStatusInfo() && !data.getStationData().isDepartureDelayed()) { + return List.of(); + } + Collection content = new ArrayList<>(); + if (data.getTrainData().isCancelled()) { + content.add(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_cancelled", data.getTrainData().getName())); + return content; + } + String delay = getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA ? ModUtils.timeRemainingString(data.getStationData().getDepartureTimeDeviation()) : String.valueOf(TimeUtils.formatToMinutes(data.getStationData().getDepartureTimeDeviation())); + MutableComponent delayComponent = CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_delayed", data.getTrainData().getName(), delay); + if (getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ABS) { + delayComponent.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delay_abs_suffix")); + } + content.add(delayComponent); + for (CompiledTrainStatus status : data.getTrainData().getStatus()) { + content.add(status.text()); + } + return content; + } + + @Override + public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { + List preds = blockEntity.getStops().stream().filter(x -> { + return (!x.isNextSectionExcluded() || getDisplaySettings(blockEntity).showArrival()) && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get()); + }).toList(); + + showInfoLine = !preds.isEmpty() && preds.get(0).getStationData().isDepartureDelayed() && preds.get(0).getTrainData().hasStatusInfo(); + if (showInfoLine) { + // Update status label + this.infoLineText = TextUtils.concat(TextUtils.text(" +++ "), preds.stream().limit(maxLines).filter(x -> x.getTrainData().hasStatusInfo() && x.getStationData().isDepartureDelayed()).flatMap(x -> { + return getStatusInfo(blockEntity, x).stream(); + }).toArray(Component[]::new)); + } else { + infoLineText = TextUtils.empty(); + } + + int defaultMaxLines = blockEntity.getYSizeScaled() * 3 - 2; + this.maxLines = defaultMaxLines - (showInfoLine ? 1 : 0); + int maxIndices = Math.max(0, Math.min(this.maxLines, preds.size())); + if (reason == EUpdateReason.LAYOUT_CHANGED || this.lines == null || lines.length != maxIndices) { + updateLayout(blockEntity, preds, maxIndices); + } + + for (int i = 0; i < this.lines.length && i < preds.size(); i++) { + StationDisplayData stop = preds.get(i); + updateContent(blockEntity, stop, i, false, 0, 0, 0); + } + + statusLabel + .setText(infoLineText) + .setPos(3, blockEntity.getYSizeScaled() * 16 - 12 * statusLabel.getYScale() - 2) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) + .setColor(ColorUtils.brightnessDependingFontColor(getDisplaySettings(blockEntity).getFontColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) + ; + } + + + private void updateLayout(AdvancedDisplayBlockEntity blockEntity, List preds, int maxIndices) { + DepartureBoardDisplayTableSettings settings = getDisplaySettings(blockEntity); + + if (blockEntity.getXSizeScaled() < MIN_SIZE) { + tooSmallLabel + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + return; + } + + boolean hasStopovers = blockEntity.getXSizeScaled() - 4 >= 4; + boolean hasInfo = blockEntity.getXSizeScaled() - 4 >= 7; + + // Init headline + BERLabel hTimeLabel = headlines[LineComponent.TIME.i()]; + hTimeLabel + .setPos(3, hTimeLabel.getY()) + .setMaxWidth(TIME_LABEL_MAX_WIDTH + (!isSmall(blockEntity) ? REAL_TIME_LABEL_MAX_WIDTH : 0) + SPACING, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + BERLabel hTrainLabel = headlines[LineComponent.TRAIN_NAME.i()]; + hTrainLabel + .setPos(hTimeLabel.getX() + hTimeLabel.getMaxWidth() + SPACING, hTrainLabel.getY()) + .setMaxWidth(settings.getTrainNameWidth(), BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + BERLabel hPlatformLabel = headlines[LineComponent.PLATFORM.i()]; + float hPlatformLabelWidth = settings.getPlatformWidth(); + hPlatformLabel + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - Math.min(hPlatformLabelWidth, hPlatformLabel.getTextWidth()), hPlatformLabel.getY()) + .setMaxWidth(hPlatformLabelWidth, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + + final float remainingSpace = blockEntity.getXSizeScaled() * 16 - 3 - hTrainLabel.getX() - hTrainLabel.getMaxWidth() - settings.getPlatformWidth() - SPACING; // No *2! + final float infoSpace = hasInfo ? (remainingSpace * settings.getInfoWidthPercentage()) - SPACING : 0; + final float stopoversSpace = hasStopovers ? (remainingSpace * settings.getStopoversWidthPercentage()) - SPACING : 0; + final float destinationSpace = remainingSpace - infoSpace - stopoversSpace - SPACING * 3; + + BERLabel hStopoversLabel = headlines[LineComponent.STOPOVERS.i()]; + hStopoversLabel + .setPos(hasStopovers ? hTrainLabel.getX() + hTrainLabel.getMaxWidth() + SPACING : 0, hStopoversLabel.getY()) + .setMaxWidth(hasStopovers ? stopoversSpace : 0, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + BERLabel hDestinationLabel = headlines[LineComponent.DESTINATION.i()]; + hDestinationLabel + .setPos(hasStopovers ? hStopoversLabel.getX() + hStopoversLabel.getMaxWidth() + SPACING : hTrainLabel.getX() + hTrainLabel.getMaxWidth() + SPACING, hDestinationLabel.getY()) + .setMaxWidth(Math.min(hDestinationLabel.getX() + (destinationSpace), blockEntity.getXSizeScaled() * 16 - 3 - hPlatformLabel.getTextWidth() - SPACING) - hDestinationLabel.getX(), BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + BERLabel hInfoLabel = headlines[LineComponent.INFO.i()]; + hInfoLabel + .setPos(hasInfo ? hDestinationLabel.getX() + hDestinationLabel.getMaxWidth() + SPACING : 0, hDestinationLabel.getY()) + .setMaxWidth(hasInfo ? Math.min(hInfoLabel.getX() + (infoSpace - SPACING), blockEntity.getXSizeScaled() * 16 - 3 - hInfoLabel.getX() - hPlatformLabel.getTextWidth() - SPACING) - hInfoLabel.getX() : 0, BoundsHitReaction.CUT_OFF) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + + + + this.lines = new BERLabel[maxIndices][]; + for (int i = 0; i < this.lines.length; i++) { + StationDisplayData stop = preds.get(i); + this.lines[i] = createLine(blockEntity, stop, i, hasStopovers, hasInfo); + updateContent(blockEntity, stop, i, true, stopoversSpace, infoSpace, destinationSpace); + } + statusLabel + .setBackground((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF), true) + .setColor(ColorUtils.brightnessDependingFontColor(settings.getFontColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) + ; + + } + + private void updateContent(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index, boolean layoutUpdate, float stopoversSize, float infoLineWidth, float destinationWidth) { + DepartureBoardDisplayTableSettings settings = getDisplaySettings(blockEntity); + boolean isLast = settings.showArrival() && stop.isLastStop(); + boolean showInfoLine = stop.getStationData().isDepartureDelayed() && stop.getTrainData().hasStatusInfo(); + + BERLabel[] components = lines[index]; + + BERLabel timeLabel = components[LineComponent.TIME.i()] + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), settings.getTimeDisplay() == ETimeDisplay.ETA))) + ; + BERLabel realTimeLabel = components[LineComponent.REAL_TIME.i()] + .setText(isSmall(blockEntity) ? + TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), settings.getTimeDisplay() == ETimeDisplay.ETA)) : + TextUtils.text(stop.getTrainData().isCancelled() ? + " \u274C " : // X + (stop.getStationData().isDepartureDelayed() ? + (ModUtils.formatTime(stop.getRealTime(), settings.getTimeDisplay() == ETimeDisplay.ETA)) : + ""))) // Nothing (not delayed) + .setColor(ColorUtils.brightnessDependingFontColor(settings.getFontColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) + ; + BERLabel trainLabel = components[LineComponent.TRAIN_NAME.i()] + .setText(TextUtils.text(stop.getTrainData().getName())) + ; + + if (settings.showLineColor() && stop.getTrainData().hasColor()) { + trainLabel + .setBackground((0xFF << 24) | (stop.getTrainData().getColor() & 0x00FFFFFF), false) + .setColor(ColorUtils.brightnessDependingFontColor(stop.getTrainData().getColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) + ; + } else { + trainLabel + .setBackground(0, false) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + } + + BERLabel destinationLabel = components[LineComponent.DESTINATION.i()] + .setText(isLast ? + CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", stop.getFirstStopName()) : + TextUtils.text(stop.getStationData().getDestination())) + ; + BERLabel stopoversLabel; + BERLabel infoLabel; + boolean hasTransfers = (stopoversLabel = components[LineComponent.STOPOVERS.i()]) != null; + boolean hasInfo = (infoLabel = components[LineComponent.INFO.i()]) != null; + if (hasTransfers) { + stopoversLabel + .setText(isLast ? + TextUtils.empty() : + TextUtils.concat(TextUtils.text(" \u25CF "), stop.getStopovers().stream().map(a -> (Component)TextUtils.text(a)).toList()) + ) + ; + } + if (hasInfo) { + infoLabel + .setText(showInfoLine ? TextUtils.concat(TextUtils.text(" +++ "), getStatusInfo(blockEntity, stop)) : TextUtils.empty()) + .setColor(ColorUtils.brightnessDependingFontColor(settings.getFontColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) + ; + } + BERLabel platformLabel = components[LineComponent.PLATFORM.i()] + .setText(blockEntity.isPlatformFixed() ? + TextUtils.empty() : + TextUtils.text(stop.getStationData().getStationInfo().platform())) + ; + + + if (layoutUpdate) { + platformLabel + .setMaxWidth(settings.getPlatformWidth(), BoundsHitReaction.SCALE_SCROLL) + ; + timeLabel + .setPos(headlines[LineComponent.TIME.i()].getX(), Y_OFFSET + 3 + index * LINE_HEIGHT) + ; + realTimeLabel + .setPos(timeLabel.getX() + timeLabel.getMaxWidth() + SPACING, Y_OFFSET + 3 + index * LINE_HEIGHT) + ; + trainLabel + .setPos(headlines[LineComponent.TRAIN_NAME.i()].getX(), Y_OFFSET + 3 + index * LINE_HEIGHT) + ; + destinationLabel + .setPos(headlines[LineComponent.DESTINATION.i()].getX(), Y_OFFSET + 3 + index * LINE_HEIGHT) + .setMaxWidth(destinationWidth, BoundsHitReaction.SCALE_SCROLL) + ; + platformLabel + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformLabel.getTextWidth(), Y_OFFSET + 3 + index * LINE_HEIGHT) + ; + if (hasTransfers) { + stopoversLabel + .setPos(hasTransfers ? headlines[LineComponent.STOPOVERS.i()].getX() : 0, Y_OFFSET + 3 + index * LINE_HEIGHT + 0.5f) + .setMaxWidth(hasTransfers ? stopoversSize : 0, BoundsHitReaction.SCALE_SCROLL) + ; + } + if (hasInfo) { + infoLabel + .setPos(showInfoLine ? headlines[LineComponent.INFO.i()].getX() : 0, Y_OFFSET + 3 + index * LINE_HEIGHT) + .setMaxWidth(showInfoLine ? infoLineWidth : 0, BoundsHitReaction.SCALE_SCROLL) + ; + } + } + } + + private BERLabel[] createLine(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index, boolean withStopovers, boolean withInfo) { + BERLabel[] components = new BERLabel[LineComponent.values().length]; + boolean isSmall = isSmall(blockEntity); + + components[LineComponent.TIME.i()] = new BERLabel() + .setYScale(0.4f) + .setMaxWidth(isSmall ? -2 : TIME_LABEL_MAX_WIDTH, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) + ; + components[LineComponent.REAL_TIME.i()] = new BERLabel() + .setYScale(0.4f) + .setMaxWidth(REAL_TIME_LABEL_MAX_WIDTH, BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) + ; + if (!isSmall) { + components[LineComponent.REAL_TIME.i()] + .setBackground((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF), false) + .setColor(0xFF111111) + ; + } + components[LineComponent.TRAIN_NAME.i()] = new BERLabel() + .setYScale(0.4f) + .setScrollingSpeed(2) + .setMaxWidth(getDisplaySettings(blockEntity).getTrainNameWidth(), BoundsHitReaction.SCALE_SCROLL) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) + ; + components[LineComponent.PLATFORM.i()] = new BERLabel() + .setYScale(0.4f) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) + ; + components[LineComponent.DESTINATION.i()] = new BERLabel() + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) + ; + + if (withStopovers) { + components[LineComponent.STOPOVERS.i()] = new BERLabel() + .setYScale(0.3f) + .setScrollingSpeed(2) + .setScale(0.3f, 0.2f) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) + ; + } + if (withInfo) { + components[LineComponent.INFO.i()] = new BERLabel() + .setYScale(0.4f) + .setScrollingSpeed(2) + .setScale(0.4f, 0.2f) + .setColor(0xFF111111) + .setBackground((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF), true) + ; + } + + return components; + } + + private boolean isSmall(AdvancedDisplayBlockEntity blockEntity) { + return blockEntity.getXSizeScaled() <= MIN_SIZE; + } + + private static enum LineComponent { + TIME(0), + REAL_TIME(1), + TRAIN_NAME(2), + DESTINATION(3), + PLATFORM(4), + STOPOVERS(5), + INFO(6); + + int index; + LineComponent(int index) { + this.index = index; + } + public int i() { + return index; + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java index 9a8af342..6da08faa 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERError.java @@ -2,8 +2,8 @@ import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.properties.BasicDisplaySettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.client.ber.BERLabel; import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; @@ -12,7 +12,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -public class BERError implements IBERRenderSubtype { +public class BERError implements AbstractAdvancedDisplayRenderer { private final BERLabel label = new BERLabel() .setPos(3, 3) diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java index 82449018..ae481b8b 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoInformative.java @@ -4,12 +4,12 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; -import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.block.display.properties.PassengerInformationDetailedSettings; import de.mrjulsen.crn.client.CRNGui; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.TrainExitSide; import de.mrjulsen.crn.data.train.portable.NextConnectionsDisplayData; @@ -33,7 +33,7 @@ import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.state.BlockState; -public class BERPassengerInfoInformative implements IBERRenderSubtype { +public class BERPassengerInfoInformative implements AbstractAdvancedDisplayRenderer { private static final ResourceLocation CARRIAGE_ICON = new ResourceLocation("create:textures/gui/assemble.png"); private static final ResourceLocation ICONS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/icons.png"); @@ -77,7 +77,7 @@ public class BERPassengerInfoInformative implements IBERRenderSubtype 1 && !nextStopAnnounced ? TextUtils.text(ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) + .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(ModUtils.formatTime(DragonLib.getCurrentWorldTime(), false)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) .setPos(blockEntity.getXSizeScaled() * 16 - 3 - timeLabel.getTextWidth() - (this.exitSide != TrainExitSide.UNKNOWN ? 4 : 0), 2.5f) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; } @@ -176,7 +176,7 @@ public void renderHeader(BERGraphics graphics, float uv255 * (227 + 10), uv255 * (19 + 10), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } @@ -205,7 +205,7 @@ public void renderHeader(BERGraphics graphics, float uv255 * (22 + 13), uv255 * (231 + 5), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } @@ -228,7 +228,7 @@ public void renderHeader(BERGraphics graphics, float uv255 * (ModGuiIcons.ARROW_RIGHT.getU() + ModGuiIcons.ICON_SIZE), uv255 * (ModGuiIcons.ARROW_RIGHT.getV() + ModGuiIcons.ICON_SIZE), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor()), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor()), light ); break; @@ -247,7 +247,7 @@ public void renderHeader(BERGraphics graphics, float uv255 * (ModGuiIcons.ARROW_LEFT.getU() + ModGuiIcons.ICON_SIZE), uv255 * (ModGuiIcons.ARROW_LEFT.getV() + ModGuiIcons.ICON_SIZE), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor()), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor()), light ); break; @@ -261,7 +261,7 @@ public void renderHeader(BERGraphics graphics, float public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { final float uv255 = 1f / 256f; renderHeader(graphics, partialTick, parent, light, backSide); - BERUtils.fillColor(graphics, 2.5f, 5.0f, 0.01f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); + BERUtils.fillColor(graphics, 2.5f, 5.0f, 0.01f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { return; @@ -279,7 +279,7 @@ public void render(BERGraphics graphics, float parti }); nextConnectionsTitleLabel.render(graphics, light); pageIndicatorLabel.render(graphics, light); - } else if (DragonLib.getCurrentWorldTime() % 500 < 200 && !graphics.blockEntity().getTrainData().isWaitingAtStation()) { + } else if (getDisplaySettings(graphics.blockEntity()).showStats() && DragonLib.getCurrentWorldTime() % 500 < 200 && !graphics.blockEntity().getTrainData().isWaitingAtStation()) { // render stats speedLabel.render(graphics, light); dateLabel.render(graphics, light); @@ -298,7 +298,7 @@ public void render(BERGraphics graphics, float parti uv255 * (22 + 13), uv255 * (231 + 5), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } else { @@ -327,7 +327,7 @@ public void render(BERGraphics graphics, float parti uv32 * (21 + 7), uv32 * (30 + 14), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } else if (idx >= MAX_LINES - 1) { @@ -345,7 +345,7 @@ public void render(BERGraphics graphics, float parti uv32 * (35 + 7), uv32 * (30 + 14), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } else { @@ -363,7 +363,7 @@ public void render(BERGraphics graphics, float parti uv32 * (28 + 7), uv32 * (30 + 14), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } @@ -382,9 +382,9 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB TrainDisplayData data = blockEntity.getTrainData(); boolean wasNextStopAnnounced = nextStopAnnounced; nextStopAnnounced = !data.isWaitingAtStation() && data.getNextStop().isPresent() && data.getNextStop().get().getRealTimeArrivalTime() - DragonLib.getCurrentWorldTime() < ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get(); - this.exitSide = !nextStopAnnounced && !data.isWaitingAtStation() ? TrainExitSide.UNKNOWN : (data.isWaitingAtStation() ? exitSide : blockEntity.relativeExitDirection.get()); + this.exitSide = (!nextStopAnnounced && !data.isWaitingAtStation()) || !getDisplaySettings(blockEntity).showExit() ? TrainExitSide.UNKNOWN : (data.isWaitingAtStation() ? exitSide : blockEntity.relativeExitDirection.get()); - if (blockEntity.getXSizeScaled() > 1 && nextStopAnnounced && !wasNextStopAnnounced && data.getNextStop().isPresent()) { + if (getDisplaySettings(blockEntity).showConnections() && blockEntity.getXSizeScaled() > 1 && nextStopAnnounced && !wasNextStopAnnounced && data.getNextStop().isPresent()) { DataAccessor.getFromServer(new NextConnectionsRequestData(data.getNextStop().get().getName(), data.getTrainData().getId()), ModAccessorTypes.GET_NEXT_CONNECTIONS_DISPLAY_DATA, (res) -> { nextConnections = res; updateLayout(blockEntity, data); @@ -399,36 +399,43 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB updateContent(blockEntity, data); } - private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayData data) { + private int getCarriageIndex(AdvancedDisplayBlockEntity blockEntity) { + PassengerInformationDetailedSettings settings = getDisplaySettings(blockEntity); + return (settings.shouldOverwriteCarriageIndex() ? 0 : blockEntity.getCarriageData().index() + 1) + settings.getCarriageIndex(); + } + + private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayData data) { + PassengerInformationDetailedSettings settings = getDisplaySettings(blockEntity); + int carriageIndex = getCarriageIndex(blockEntity); timeLabel - .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) + .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(ModUtils.formatTime(DragonLib.getCurrentWorldTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) .setPos(blockEntity.getXSizeScaled() * 16 - 3 - timeLabel.getTextWidth() - (this.exitSide != TrainExitSide.UNKNOWN ? 4 : 0), 2.5f) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; carriageLabel - .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) + .setText(blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? TextUtils.text(String.format("%02d", carriageIndex)).withStyle(ChatFormatting.BOLD) : TextUtils.empty()) .setPos(timeLabel.getX() - 4 - carriageLabel.getTextWidth(), 2.5f) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; trainLineLabel - .setText(nextStopAnnounced ? ELanguage.translate(keyNextStop, data.getNextStop().get().getName()) : TextUtils.text(data.getTrainData().getName()).withStyle(ChatFormatting.BOLD)) + .setText(nextStopAnnounced ? CustomLanguage.translate(keyNextStop, data.getNextStop().get().getName()) : TextUtils.text((settings.getTrainTextComponents().showTrainName() ? data.getTrainData().getName() + " " : "") + (settings.getTrainTextComponents().showDestination() ? data.getNextStop().get().getDestination() : "")).withStyle(ChatFormatting.BOLD)) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6 - (blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? timeLabel.getTextWidth() - 4 : 0) - (blockEntity.getXSizeScaled() > 1 && !nextStopAnnounced ? carriageLabel.getTextWidth() - 5 : 0) - (this.exitSide != TrainExitSide.UNKNOWN ? 4 : 0), BoundsHitReaction.SCALE_SCROLL) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; speedLabel .setText(ModUtils.calcSpeedString(data.getSpeed(), ModClientConfig.SPEED_UNIT.get()).withStyle(ChatFormatting.BOLD)) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; dateLabel - .setText(ELanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / Level.TICKS_PER_DAY, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(CustomLanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / Level.TICKS_PER_DAY, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; carriageInfoLabel - .setText(TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1))) + .setText(TextUtils.text(String.format("%02d", carriageIndex))) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; if (shouldRenderNextConnections() && !nextConnections.getConnections().isEmpty()) { @@ -437,11 +444,11 @@ private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayD pageIndicatorLabel .setText(TextUtils.text(generatePageIndexString(page, pages))) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; nextConnectionsTitleLabel .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.CUT_OFF) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; DLUtils.doIfNotNull(nextConnectionsLines, x -> { for (int i = 0; i < MAX_LINES - 1; i++) { @@ -463,23 +470,23 @@ private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayD a[LineComponent.PLATFORM.i()] .setText(TextUtils.text(stop.getStationInfo().platform())) .setPos(blockEntity.getXSizeScaled() * 16 - 3 - a[LineComponent.PLATFORM.i()].getTextWidth(), 7.5f + k * 1.7f) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; if (a[LineComponent.REAL_TIME.i()] != null) { a[LineComponent.SCHEDULED_TIME.i()] .setPos(3, 7.5f + k * 1.7f) - .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledDepartureTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; a[LineComponent.REAL_TIME.i()] .setPos(a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getTextWidth() + 1, 7.5f + k * 1.7f) - .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeDepartureTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) .setColor(stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) ; } else { a[LineComponent.SCHEDULED_TIME.i()] .setPos(3, 7.5f + k * 1.7f) - .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeDepartureTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) .setColor(stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) ; } @@ -488,13 +495,13 @@ private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayD .setPos(pX, 7.5f + k * 1.7f) .setText(TextUtils.text(stop.getTrainName())) .setMaxWidth(6, BoundsHitReaction.CUT_OFF) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; a[LineComponent.DESTINATION.i()] .setPos(pX + 7, 7.5f + k * 1.7f) .setText(TextUtils.text(stop.getDestination())) .setMaxWidth(a[LineComponent.PLATFORM.i()].getX() - 1 - pX - 7, BoundsHitReaction.SCALE_SCROLL) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; }); } @@ -510,17 +517,17 @@ private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayD TrainStopDisplayData stop = data.getStopsFromCurrentStation().get(k); if (a[LineComponent.REAL_TIME.i()] != null) { a[LineComponent.SCHEDULED_TIME.i()] - .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledArrivalTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledArrivalTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; a[LineComponent.REAL_TIME.i()] .setPos(a[LineComponent.SCHEDULED_TIME.i()].getX() + a[LineComponent.SCHEDULED_TIME.i()].getTextWidth() + 1, 6 + j * 2) - .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeArrivalTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeArrivalTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) .setColor(stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) ; } else { a[LineComponent.SCHEDULED_TIME.i()] - .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeArrivalTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(TextUtils.text(ModUtils.formatTime(stop.getRealTimeArrivalTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) .setColor(stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME) ; } @@ -529,7 +536,7 @@ private void updateContent(AdvancedDisplayBlockEntity blockEntity, TrainDisplayD .setPos(pX, 6 + j * 2) .setText(TextUtils.text(stop.getName()).withStyle(j >= linesCount - 1 ? ChatFormatting.BOLD : ChatFormatting.RESET)) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 3 - pX, BoundsHitReaction.SCALE_SCROLL) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; }); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java index 41d13542..54816dc0 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPassengerInfoSimple.java @@ -2,11 +2,10 @@ import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; -import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.block.display.properties.PassengerInformationScrollingTextSettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.TrainExitSide; import de.mrjulsen.crn.util.ModUtils; @@ -21,7 +20,9 @@ import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.state.BlockState; -public class BERPassengerInfoSimple implements IBERRenderSubtype { +public class BERPassengerInfoSimple implements AbstractAdvancedDisplayRenderer { + + public BERPassengerInfoSimple() {} private static final String keyNextStop = "gui.createrailwaysnavigator.route_overview.next_stop"; private static final String keyDate = "gui.createrailwaysnavigator.route_overview.date"; @@ -70,7 +71,7 @@ public void render(BERGraphics graphics, float parti uv * (ModGuiIcons.ARROW_RIGHT.getU() + ModGuiIcons.ICON_SIZE), uv * (ModGuiIcons.ARROW_RIGHT.getV() + ModGuiIcons.ICON_SIZE), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor()), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor()), light ); break; @@ -89,7 +90,7 @@ public void render(BERGraphics graphics, float parti uv * (ModGuiIcons.ARROW_LEFT.getU() + ModGuiIcons.ICON_SIZE), uv * (ModGuiIcons.ARROW_LEFT.getV() + ModGuiIcons.ICON_SIZE), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor()), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor()), light ); break; @@ -115,27 +116,43 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB return; } - this.exitSide = blockEntity.getTrainData().isWaitingAtStation() ? exitSide : blockEntity.relativeExitDirection.get(); + PassengerInformationScrollingTextSettings settings = getDisplaySettings(blockEntity); + + this.exitSide = settings.showExit() ? (blockEntity.getTrainData().isWaitingAtStation() ? exitSide : blockEntity.relativeExitDirection.get()) : TrainExitSide.UNKNOWN; if (!blockEntity.getTrainData().getNextStop().isPresent()) { - label.setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName())); + label.setText(settings.getTrainTextComponents().showTrainName() ? TextUtils.text(blockEntity.getTrainData().getTrainData().getName()) : TextUtils.empty()); } else if (blockEntity.getTrainData().isWaitingAtStation()) { label.setText(TextUtils.text(blockEntity.getTrainData().getNextStop().get().getName())); } else if (blockEntity.getTrainData().getNextStop().get().getRealTimeArrivalTime() - DragonLib.getCurrentWorldTime() < ModClientConfig.NEXT_STOP_ANNOUNCEMENT.get()) { - label.setText(ELanguage.translate(keyNextStop, blockEntity.getTrainData().getNextStop().get().getName())); + label.setText(CustomLanguage.translate(keyNextStop, blockEntity.getTrainData().getNextStop().get().getName())); } else { final int slides = 3; int slide = (int)(DragonLib.getCurrentWorldTime() % (TICKS_PER_SLIDE * slides)) / TICKS_PER_SLIDE; - switch (slide) { - case 0 -> label.setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName() + " " + blockEntity.getTrainData().getNextStop().get().getDestination())); - case 1 -> label.setText(ELanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / Level.TICKS_PER_DAY, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))); - case 2 -> label.setText(ModUtils.calcSpeedString(blockEntity.getTrainData().getSpeed(), ModClientConfig.SPEED_UNIT.get())); + if ((slide == 1 && !settings.showTimeAndDate()) || + (slide == 2 && !settings.showStats()) + ) { + slide++; + } + slide %= slides; + switch (slide) { + case 0 -> label.setText(TextUtils.text((settings.getTrainTextComponents().showTrainName() + ? blockEntity.getTrainData().getTrainData().getName() + " " + : "") + + (settings.getTrainTextComponents().showDestination() + ? blockEntity.getTrainData().getNextStop().get().getDestination() + : ""))); + case 1 -> label + .setText(CustomLanguage.translate(keyDate, blockEntity.getLevel().getDayTime() / Level.TICKS_PER_DAY, + ModUtils.formatTime(DragonLib.getCurrentWorldTime(), false))); + case 2 -> label.setText(ModUtils.calcSpeedString(blockEntity.getTrainData().getSpeed(), + ModClientConfig.SPEED_UNIT.get())); } this.exitSide = TrainExitSide.UNKNOWN; } label .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6 - (exitSide == TrainExitSide.UNKNOWN ? 0 : 10), BoundsHitReaction.SCROLL) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java index 3d560f3b..61b3d84f 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java @@ -7,10 +7,10 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; -import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.block.display.properties.PlatformDisplayTableSettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; import de.mrjulsen.crn.data.train.portable.StationDisplayData; @@ -19,6 +19,7 @@ import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.client.ber.BERLabel; import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; +import de.mrjulsen.mcdragonlib.util.ColorUtils; import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; @@ -28,7 +29,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -public class BERPlatformDetailed implements IBERRenderSubtype { +public class BERPlatformDetailed implements AbstractAdvancedDisplayRenderer { private static final String keyTime = "gui.createrailwaysnavigator.time"; @@ -70,7 +71,7 @@ public void renderTick(float deltaTime) { @Override public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { timeLabel - .setText(ELanguage.translate(keyTime, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(CustomLanguage.translate(keyTime, ModUtils.formatTime(DragonLib.getCurrentWorldTime(), false))) ; } @@ -105,10 +106,17 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB this.infoLineText = TextUtils.concat(TextUtils.text(" +++ "), preds.stream().limit(maxLines).filter(x -> x.getTrainData().hasStatusInfo() && x.getStationData().isDepartureDelayed()).flatMap(x -> { Collection content = new ArrayList<>(); if (x.getTrainData().isCancelled()) { - content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_cancelled", x.getTrainData().getName())); + content.add(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_cancelled", x.getTrainData().getName())); return content.stream(); } - content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_delayed", x.getTrainData().getName(), TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation()))); + String delay = getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA ? ModUtils.timeRemainingString(x.getStationData().getDepartureTimeDeviation()) : String.valueOf(TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation())); + + MutableComponent delayComponent = CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.information_about_delayed", x.getTrainData().getName(), delay); + if (getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ABS) { + delayComponent.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delay_abs_suffix")); + } + content.add(delayComponent); + for (CompiledTrainStatus status : x.getTrainData().getStatus()) { content.add(status.text()); } @@ -145,39 +153,52 @@ private void updateLayout(AdvancedDisplayBlockEntity blockEntity, List { +public class BERPlatformInformative implements AbstractAdvancedDisplayRenderer { //TODO PlatformWidth verwenden private static final String keyFollowingTrains = "gui.createrailwaysnavigator.following_trains"; @@ -40,14 +43,16 @@ public class BERPlatformInformative implements IBERRenderSubtype x.renderTick()); + DLUtils.doIfNotNull(platformLabel, x -> x.renderTick()); DLUtils.doIfNotNull(focusArea, x -> { for (int i = 0; i < x.length; i++) { DLUtils.doIfNotNull(x[i], y -> y.renderTick()); @@ -71,9 +76,9 @@ private boolean isExtendedDisplay(AdvancedDisplayBlockEntity blockEntity) { @Override public void render(BERGraphics graphics, float pPartialTicks, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { if (isExtendedDisplay(graphics.blockEntity())) { - BERUtils.fillColor(graphics, 2.5f, 15.5f, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); + BERUtils.fillColor(graphics, 2.5f, 15.5f, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); followingTrainsLabel - .setColor((0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF)) ; followingTrainsLabel.render(graphics, light); } @@ -93,9 +98,9 @@ public void render(BERGraphics graphics, float pPart } else if (i == LineComponent.TRAIN_NAME.i()) { graphics.poseStack().translate(-label.getX() + maxWidth - 3 - label.getTextWidth(), 0, 0); } else if (i == LineComponent.DESTINATION.i()) { - graphics.poseStack().translate(-label.getX() + 5 + a[LineComponent.PLATFORM.i()].getTextWidth(), 0, 0); + graphics.poseStack().translate(-label.getX() + 5 + platformLabel.getMaxWidth(), 0, 0); } else if (i == LineComponent.STOPOVERS.i()) { - graphics.poseStack().translate(-label.getX() + 5 + a[LineComponent.PLATFORM.i()].getTextWidth(), 0, 0); + graphics.poseStack().translate(-label.getX() + 5 + platformLabel.getMaxWidth(), 0, 0); } else if (i == LineComponent.PLATFORM.i()) { graphics.poseStack().translate(-label.getX() + 3, 0, 0); } @@ -115,14 +120,21 @@ public void render(BERGraphics graphics, float pPart } }); + graphics.poseStack().pushPose(); + if (statusLabel != null && !statusLabel.getText().getString().isBlank()) { graphics.poseStack().pushPose(); - if (backSide && focusArea != null && focusArea[LineComponent.PLATFORM.i()] != null) { - graphics.poseStack().translate(-statusLabel.getX() + 5 + focusArea[LineComponent.PLATFORM.i()].getTextWidth(), 0, 0); + if (backSide) { + graphics.poseStack().translate(-statusLabel.getX() + 5 + platformLabel.getMaxWidth(), 0, 0); } DLUtils.doIfNotNull(statusLabel, x -> x.render(graphics, light)); graphics.poseStack().popPose(); } + if (backSide && platformLabel != null) { + graphics.poseStack().translate(-graphics.blockEntity().getXSizeScaled() * 16 + 6 + platformLabel.getTextWidth(), 0, 0); + } + platformLabel.render(graphics, light); + graphics.poseStack().popPose(); } @Override @@ -133,6 +145,15 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB lines = null; focusArea = null; statusLabel = null; + + if (blockEntity.isPlatformFixed() && blockEntity.getStationInfo() != null) { + this.platformLabel + .setText(TextUtils.text(blockEntity.getStationInfo().platform()).withStyle(ChatFormatting.BOLD)) + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformLabel.getTextWidth(), 3); + ; + } else { + this.platformLabel.setText(TextUtils.empty()); + } return; } @@ -145,9 +166,15 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB // Update status label Collection content = new ArrayList<>(); if (preds.get(0).getTrainData().isCancelled()) { - content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled")); - } else { - content.add(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed", TimeUtils.formatToMinutes(preds.get(0).getStationData().getDepartureTimeDeviation()))); + content.add(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled")); + } else { + TrainStopDisplayData displayData = preds.get(0).getStationData(); + String delay = getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA ? ModUtils.timeRemainingString(displayData.getDepartureTimeDeviation()) : String.valueOf(TimeUtils.formatToMinutes(displayData.getDepartureTimeDeviation())); + MutableComponent delayComponent = CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed", delay); + if (getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ABS) { + delayComponent.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delay_abs_suffix")); + } + content.add(delayComponent); for (CompiledTrainStatus status : preds.get(0).getTrainData().getStatus()) { content.add(status.text()); } @@ -169,14 +196,14 @@ private void updateLayout(AdvancedDisplayBlockEntity blockEntity, List (Component)TextUtils.text(a)).toList())) .setPos(x, 6) .setMaxWidth(w, BoundsHitReaction.SCALE_SCROLL) @@ -330,25 +378,41 @@ private void updateFocusContent(AdvancedDisplayBlockEntity blockEntity, StationD .setText(infoLineText) .setPos(x, 2.5f) .setMaxWidth(w, BoundsHitReaction.SCALE_SCROLL) + .setColor(ColorUtils.brightnessDependingFontColor(getDisplaySettings(blockEntity).getFontColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) ; } private void updateTableContent(AdvancedDisplayBlockEntity blockEntity, StationDisplayData stop, int index) { - boolean isLast = stop.isLastStop(); + PlatformDisplayFocusSettings settings = getDisplaySettings(blockEntity); + boolean isLast = (settings.showArrival() && stop.isLastStop()) || stop.isNextSectionExcluded(); + BERLabel[] components = lines[index]; components[LineComponent.TIME.i()] - .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA))) + .setText(TextUtils.text(ModUtils.formatTime(stop.getScheduledTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA))) ; components[LineComponent.REAL_TIME.i()] .setText(TextUtils.text(stop.getTrainData().isCancelled() ? " \u274C " : // X (stop.getStationData().isDepartureDelayed() ? - (ModUtils.formatTime(isLast ? stop.getStationData().getRealTimeArrivalTime() : stop.getStationData().getRealTimeDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA)) : + (ModUtils.formatTime(isLast ? stop.getStationData().getRealTimeArrivalTime() : stop.getStationData().getRealTimeDepartureTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA)) : ""))) // Nothing (not delayed) + .setColor(ColorUtils.brightnessDependingFontColor(getDisplaySettings(blockEntity).getFontColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) ; - components[LineComponent.TRAIN_NAME.i()] + BERLabel trainNameLabel = components[LineComponent.TRAIN_NAME.i()] .setText(TextUtils.text(stop.getTrainData().getName())) + .setBackground(settings.showLineColor() && stop.getTrainData().hasColor() ? (0xFF << 24) | (stop.getTrainData().getColor() & 0x00FFFFFF) : 0, false) ; + if (settings.showLineColor() && stop.getTrainData().hasColor()) { + trainNameLabel + .setBackground((0xFF << 24) | (stop.getTrainData().getColor() & 0x00FFFFFF), false) + .setColor(ColorUtils.brightnessDependingFontColor(stop.getTrainData().getColor(), LIGHT_FONT_COLOR, DARK_FONT_COLOR)) + ; + } else { + trainNameLabel + .setBackground(0, false) + .setColor((0xFF << 24) | (settings.getFontColor() & 0x00FFFFFF)) + ; + } components[LineComponent.PLATFORM.i()] .setText(blockEntity.isPlatformFixed() ? TextUtils.empty() : @@ -356,7 +420,7 @@ private void updateTableContent(AdvancedDisplayBlockEntity blockEntity, StationD ; components[LineComponent.DESTINATION.i()] .setText(isLast ? - ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", stop.getFirstStopName()) : + CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".schedule_board.train_from", stop.getFirstStopName()) : TextUtils.text(stop.getStationData().getDestination())) ; @@ -365,18 +429,25 @@ private void updateTableContent(AdvancedDisplayBlockEntity blockEntity, StationD x += components[LineComponent.TIME.i()].getTextWidth() + 2; components[LineComponent.REAL_TIME.i()].setPos(x, 11 + 3 + index * LINE_HEIGHT); x += components[LineComponent.REAL_TIME.i()].getTextWidth() + 2 + (!components[LineComponent.REAL_TIME.i()].getText().getString().isEmpty() ? 2 : 0); - - BERLabel trainNameLabel = components[LineComponent.TRAIN_NAME.i()] + + float trainNameWidth = settings.isAutoTrainNameWidth() ? trainNameLabel.getTextWidth() : settings.getTrainNameWidth(); + trainNameLabel .setPos(x, 11 + 3 + index * LINE_HEIGHT) - .setMaxWidth(blockEntity.getTrainNameWidth(), BoundsHitReaction.SCALE_SCROLL) + .setMaxWidth(trainNameWidth, BoundsHitReaction.SCALE_SCROLL) ; - x += trainNameLabel.getMaxWidth() + 2; + x += trainNameLabel.getMaxWidth() + 2; + BERLabel platformLabel = components[LineComponent.PLATFORM.i()]; - float platformWidth = platformLabel.getTextWidth(); - platformLabel.setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformWidth, 11 + 3 + index * LINE_HEIGHT); - components[LineComponent.DESTINATION.i()].setPos(x, 11 + 3 + index * LINE_HEIGHT); - components[LineComponent.DESTINATION.i()].setMaxWidth(blockEntity.getXSizeScaled() * 16 - 3 - x - platformWidth - 3, BoundsHitReaction.SCALE_SCROLL); + float platformWidth = settings.isAutoPlatformWidth() ? platformLabel.getTextWidth() : settings.getPlatformWidth(); + platformLabel + .setPos(blockEntity.getXSizeScaled() * 16 - 3 - platformLabel.getTextWidth(), 11 + 3 + index * LINE_HEIGHT) + .setMaxWidth(platformWidth, BoundsHitReaction.SCALE_SCROLL) + ; + components[LineComponent.DESTINATION.i()] + .setPos(x, 11 + 3 + index * LINE_HEIGHT) + .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 3 - x - platformWidth - 3, BoundsHitReaction.SCALE_SCROLL) + ; } private static enum LineComponent { diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java index 9c12a23e..c607cbeb 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java @@ -6,10 +6,10 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; -import de.mrjulsen.crn.block.display.AdvancedDisplaySource.ETimeDisplay; +import de.mrjulsen.crn.block.properties.ETimeDisplay; +import de.mrjulsen.crn.block.display.properties.PlatformDisplayScrollingTextSettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.train.portable.StationDisplayData; import de.mrjulsen.crn.util.ModUtils; @@ -25,7 +25,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -public class BERPlatformSimple implements IBERRenderSubtype { +public class BERPlatformSimple implements AbstractAdvancedDisplayRenderer { private static final String keyTrainDeparture = "gui.createrailwaysnavigator.route_overview.notification.journey_begins"; private static final String keyTrainDepartureWithPlatform = "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform"; @@ -51,7 +51,7 @@ public void renderTick(float deltaTime) { @Override public void tick(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent) { List textContent = new ArrayList<>(texts); - textContent.add(0, ELanguage.translate(keyTime, TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.TICKS_PER_DAY + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get()))); + textContent.add(0, CustomLanguage.translate(keyTime, TimeUtils.parseTime((int)(blockEntity.getLevel().getDayTime() % DragonLib.ticksPerDay() + DragonLib.daytimeShift()), ModClientConfig.TIME_FORMAT.get()))); MutableComponent txt = TextUtils.concat(textContent); label .setText(txt) @@ -68,35 +68,32 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getScheduledArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); label - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6, BoundsHitReaction.SCALE_SCROLL) ; texts = new ArrayList<>(); - texts.addAll(preds.stream().map(x -> { - String timeString; - switch (blockEntity.getTimeDisplay()) { - case ETA: - timeString = ModUtils.timeRemainingString(x.getStationData().getScheduledDepartureTime()); - break; - default: - timeString = ModUtils.formatTime(x.getStationData().getScheduledDepartureTime(), blockEntity.getTimeDisplay() == ETimeDisplay.ETA); - break; - } - + texts.addAll(preds.stream().filter(x -> { + return !x.isNextSectionExcluded(); + }).map(x -> { + String timeString = ModUtils.formatTime(x.getStationData().getScheduledDepartureTime(), getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA); MutableComponent text = TextUtils.empty(); if (x.getStationData().getStationInfo().platform() == null || x.getStationData().getStationInfo().platform().isBlank()) { - text.append(ELanguage.translate(keyTrainDeparture, x.getTrainData().getName(), x.getStationData().getDestination(), timeString)); + text.append(CustomLanguage.translate(keyTrainDeparture, x.getTrainData().getName(), x.getStationData().getDestination(), timeString)); } else { - text.append(ELanguage.translate(keyTrainDepartureWithPlatform, x.getTrainData().getName(), x.getStationData().getDestination(), timeString, x.getStationData().getStationInfo().platform())); + text.append(CustomLanguage.translate(keyTrainDepartureWithPlatform, x.getTrainData().getName(), x.getStationData().getDestination(), timeString, x.getStationData().getStationInfo().platform())); } if (x.getTrainData().isCancelled()) { - text.append(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled2").getString()); + text.append(", ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled2").getString()); } else if (x.getStationData().isDepartureDelayed()) { - text.append(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed2", TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation())).getString()); + String delay = getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA ? ModUtils.timeRemainingString(x.getStationData().getDepartureTimeDeviation()) : String.valueOf(TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation())); + text.append(", ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed2", delay).getString()); + if (getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ABS) { + text.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delay_abs_suffix")); + } if (x.getTrainData().hasStatusInfo()) { - text.append(" ").append(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.reason").getString()).append(x.getTrainData().getStatus().get(0).text()); + text.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.reason").getString()).append(x.getTrainData().getStatus().get(0).text()); } } return text; diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java index 7505ad10..e49c8c44 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationDetailed.java @@ -3,9 +3,9 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.properties.TrainDestinationExtendedSettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.client.ber.BERLabel; import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; @@ -16,9 +16,9 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -public class BERTrainDestinationDetailed implements IBERRenderSubtype { +public class BERTrainDestinationDetailed implements AbstractAdvancedDisplayRenderer { - private final BERLabel outOfServiceLabel = new BERLabel(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service")) + private final BERLabel outOfServiceLabel = new BERLabel(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service")) .setPos(3, 6) .setScale(0.5f, 0.25f) .setYScale(0.5f) @@ -37,7 +37,7 @@ public class BERTrainDestinationDetailed implements IBERRenderSubtype (Component)TextUtils.text(x.getName())).toList())) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java index dd67781d..58e4dab6 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationInformative.java @@ -3,8 +3,8 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.properties.TrainDestinationDetailedSettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.client.ber.BERLabel; import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; @@ -18,7 +18,7 @@ import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.state.BlockState; -public class BERTrainDestinationInformative implements IBERRenderSubtype { +public class BERTrainDestinationInformative implements AbstractAdvancedDisplayRenderer { private static final ResourceLocation CARRIAGE_ICON = new ResourceLocation("create:textures/gui/assemble.png"); private static final ResourceLocation ICONS = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/icons.png"); @@ -61,8 +61,8 @@ public void renderTick(float deltaTime) { @Override public void render(BERGraphics graphics, float partialTick, AdvancedDisplayRenderInstance parent, int light, boolean backSide) { float uv = 1.0f / 256.0f; - BERUtils.fillColor(graphics, 2.5f, 5.0f, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); - BERUtils.renderTexture(CARRIAGE_ICON, graphics, false, graphics.blockEntity().getXSizeScaled() * 16 - 7 - carriageIndexLabel.getTextWidth(), 2.5f, 0, 3, 2, uv * 22, uv * 231, uv * 22 + uv * 13, uv * 231 + uv * 5, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING).getOpposite(), (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), light); + BERUtils.fillColor(graphics, 2.5f, 5.0f, 0.0f, graphics.blockEntity().getXSizeScaled() * 16 - 5, 0.25f, (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), light); + BERUtils.renderTexture(CARRIAGE_ICON, graphics, false, graphics.blockEntity().getXSizeScaled() * 16 - 7 - carriageIndexLabel.getTextWidth(), 2.5f, 0, 3, 2, uv * 22, uv * 231, uv * 22 + uv * 13, uv * 231 + uv * 5, graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING).getOpposite(), (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light); carriageIndexLabel.render(graphics, light); if (graphics.blockEntity().getTrainData() == null || graphics.blockEntity().getTrainData().isEmpty()) { @@ -88,7 +88,7 @@ public void render(BERGraphics graphics, float parti uv * (195 + 10), uv * (19 + 10), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); @@ -106,7 +106,7 @@ public void render(BERGraphics graphics, float parti uv * (211 + 10), uv * (19 + 10), graphics.blockEntity().getBlockState().getValue(HorizontalDirectionalBlock.FACING), - (0xFF << 24) | (graphics.blockEntity().getColor() & 0x00FFFFFF), + (0xFF << 24) | (getDisplaySettings(graphics.blockEntity()).getFontColor() & 0x00FFFFFF), light ); } @@ -120,34 +120,36 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB } private void updateContent(AdvancedDisplayBlockEntity blockEntity) { + TrainDestinationDetailedSettings settings = getDisplaySettings(blockEntity); + int index = (settings.shouldOverwriteCarriageIndex() ? 0 : blockEntity.getCarriageData().index() + 1) + settings.getCarriageIndex(); carriageIndexLabel - .setText(TextUtils.text(String.format("%02d", blockEntity.getCarriageData().index() + 1)).withStyle(ChatFormatting.BOLD)) + .setText(TextUtils.text(String.format("%02d", index)).withStyle(ChatFormatting.BOLD)) .setPos(blockEntity.getXSizeScaled() * 16 - 3 - carriageIndexLabel.getTextWidth(), 2.5f) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; trainLineLabel .setPos(3, 2.5f) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 6 - carriageIndexLabel.getTextWidth() - 5, BoundsHitReaction.SCALE_SCROLL) .setText(TextUtils.text(blockEntity.getTrainData().getTrainData().getName()).withStyle(ChatFormatting.BOLD)) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; fromLabel .setPos(6, 6) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 9, BoundsHitReaction.SCALE_SCROLL) .setText(TextUtils.text(!blockEntity.getTrainData().getAllStops().isEmpty() ? blockEntity.getTrainData().getAllStops().get(0).getName() : "")) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; stopoversLabel .setPos(6, 8.75f) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 9, BoundsHitReaction.SCALE_SCROLL) .setText(TextUtils.concat(TextUtils.text(" \u25CF "), blockEntity.getTrainData().getStopovers().stream().map(x -> (Component)TextUtils.text(x.getName())).toList())) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; destinationLabel .setPos(6, 11) .setMaxWidth(blockEntity.getXSizeScaled() * 16 - 9, BoundsHitReaction.SCALE_SCROLL) .setText(TextUtils.text(blockEntity.getTrainData().getNextStop().isPresent() ? blockEntity.getTrainData().getNextStop().get().getDestination() : "").withStyle(ChatFormatting.BOLD)) - .setColor((0xFF << 24) | (blockEntity.getColor() & 0x00FFFFFF)) + .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) ; } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java index 23881277..0083401e 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERTrainDestinationSimple.java @@ -3,9 +3,9 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity.EUpdateReason; +import de.mrjulsen.crn.block.display.properties.TrainDestinationCompactSettings; import de.mrjulsen.crn.client.ber.AdvancedDisplayRenderInstance; -import de.mrjulsen.crn.client.ber.IBERRenderSubtype; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.mcdragonlib.client.ber.BERGraphics; import de.mrjulsen.mcdragonlib.client.ber.BERLabel; import de.mrjulsen.mcdragonlib.client.ber.BERLabel.BoundsHitReaction; @@ -16,10 +16,10 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -public class BERTrainDestinationSimple implements IBERRenderSubtype { +public class BERTrainDestinationSimple implements AbstractAdvancedDisplayRenderer { - private final BERLabel outOfServiceLabel = new BERLabel(ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service")) + private final BERLabel outOfServiceLabel = new BERLabel(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service")) .setPos(3, 6) .setScale(0.5f, 0.25f) .setYScale(0.5f) @@ -30,7 +30,6 @@ public class BERTrainDestinationSimple implements IBERRenderSubtype { currentPage = new RouteOverviewPage(this.route); String terminus = x.part().getNextStop().getTerminusText(); - setSlidingText(ELanguage.translate(keyTrainDetails, x.part().getNextStop().getTrainDisplayName(), terminus == null || terminus.isEmpty() ? x.part().getNextStop().getScheduleTitle() : terminus)); + setSlidingText(CustomLanguage.translate(keyTrainDetails, x.part().getNextStop().getTrainDisplayName(), terminus == null || terminus.isEmpty() ? x.part().getNextStop().getScheduleTitle() : terminus)); }); route.listen(ClientRoute.EVENT_FIRST_STOP_STATION_CHANGED, this, x -> { - setSlidingText(x.trainStop().getRealTimeStationTag().info().platform().isEmpty() ? ELanguage.translate(keyJourneyBegins) : ELanguage.translate(keyJourneyBeginsWithPlatform, x.trainStop().getRealTimeStationTag().info().platform())); + setSlidingText(x.trainStop().getRealTimeStationTag().info().platform().isEmpty() ? CustomLanguage.translate(keyJourneyBegins) : CustomLanguage.translate(keyJourneyBeginsWithPlatform, x.trainStop().getRealTimeStationTag().info().platform())); }); route.listen(ClientRoute.EVENT_ARRIVAL_AT_ANY_STOP, this, x -> { setSlidingText(TextUtils.text(x.trainStop().getClientTag().tagName())); @@ -113,17 +114,17 @@ public RouteDetailsOverlay(Level level, ClientRoute route, int width, int height } }); route.listen(ClientRoute.EVENT_ANNOUNCE_STOPOVER, this, x -> { - setSlidingText(ELanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName())); + setSlidingText(CustomLanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName())); }); route.listen(ClientRoute.EVENT_ANNOUNCE_LAST_STOP, this, x -> { - setSlidingText(ELanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName())); + setSlidingText(CustomLanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName())); }); route.listen(ClientRoute.EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION, this, x -> { if (x.connection().isConnectionMissed()) { connectionMissed(); return; } - setSlidingText(ELanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName()).append(" *** ").append(getTransferSlidingText(x.connection()))); + setSlidingText(CustomLanguage.translate(keyNextStop, x.trainStop().getClientTag().tagName()).append(" *** ").append(getTransferSlidingText(x.connection()))); currentPage = new TransferPage(this.route, x.connection()); }); route.listen(ClientRoute.EVENT_PART_CHANGED, this, x -> { @@ -136,7 +137,7 @@ public RouteDetailsOverlay(Level level, ClientRoute route, int width, int height currentPage = new TransferPage(this.route, x.connection()); }); route.listen(ClientRoute.EVENT_ARRIVAL_AT_LAST_STOP, this, x -> { - setSlidingText(ELanguage.translate(keyAfterJourney, x.trainStop().getClientTag().tagName())); + setSlidingText(CustomLanguage.translate(keyAfterJourney, x.trainStop().getClientTag().tagName())); currentPage = new JourneyCompletedPage(this.route, () -> currentPage = new NextConnectionsPage(route, () -> {} /*InstanceManager::removeRouteOverlay*/)); route.close(); }); @@ -144,7 +145,7 @@ public RouteDetailsOverlay(Level level, ClientRoute route, int width, int height if (journeyCompleted) { return; } - setSlidingText(ELanguage.translate(keyAfterJourney, x.trainStop().getClientTag().tagName())); + setSlidingText(CustomLanguage.translate(keyAfterJourney, x.trainStop().getClientTag().tagName())); currentPage = new JourneyCompletedPage(this.route, () -> currentPage = new NextConnectionsPage(route, () -> {} /*InstanceManager::removeRouteOverlay*/)); route.close(); }); @@ -159,17 +160,17 @@ public RouteDetailsOverlay(Level level, ClientRoute route, int width, int height private Component getTransferSlidingText(TransferConnection connection) { StationInfo info = connection.getDepartureStation().getRealTimeStationTag().info(); String terminus = connection.getDepartureStation().getDisplayTitle(); - return (info == null || info.platform().isBlank() ? ELanguage.translate(keyTransfer, connection.getDepartureStation().getTrainDisplayName(), terminus) : ELanguage.translate(keyTransferWithPlatform, connection.getDepartureStation().getTrainDisplayName(), terminus, info.platform())); + return (info == null || info.platform().isBlank() ? CustomLanguage.translate(keyTransfer, connection.getDepartureStation().getTrainDisplayName(), terminus) : CustomLanguage.translate(keyTransferWithPlatform, connection.getDepartureStation().getTrainDisplayName(), terminus, info.platform())); } private void connectionMissed() { - setSlidingText(ELanguage.translate(keyConnectionMissedInfo)); + setSlidingText(CustomLanguage.translate(keyConnectionMissedInfo)); currentPage = new ConnectionMissedPage(this.route); route.close(); } private void trainCancelled(String trainName) { - setSlidingText(ELanguage.translate(keyTrainCancelledInfo, trainName)); + setSlidingText(CustomLanguage.translate(keyTrainCancelledInfo, trainName)); currentPage = new TrainCancelledInfo(this.route, trainName); route.close(); } @@ -269,9 +270,9 @@ private void renderInternal(Graphics graphics, int x, int y, int width, int heig GuiUtils.drawTexture(GUI, graphics, x, y, GUI_WIDTH, GUI_HEIGHT, 0, currentPage != null && currentPage.isImportant() ? 138 : 0, 256, 256); GuiUtils.drawString(graphics, font, x + 6, y + 4, title, 0x4F4F4F, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, font, x + 6, y + GUI_HEIGHT - 2 - font.lineHeight, TextUtils.translate(keyOptionsText, TextUtils.translate(InputConstants.getKey(Minecraft.ON_OSX ? InputConstants.KEY_LWIN : InputConstants.KEY_LCONTROL, 0).getName()).append(" + ").append(Component.keybind(keyKeybindOptions)).withStyle(ChatFormatting.BOLD)), 0x4F4F4F, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, x + 6, y + GUI_HEIGHT - 2 - font.lineHeight, TextUtils.translate(keyOptionsText, TextUtils.translate(InputConstants.getKey(Minecraft.ON_OSX ? InputConstants.KEY_LWIN : InputConstants.KEY_LCONTROL, 0).getName()).append(" + ").append(TextUtils.keybind(keyKeybindOptions)).withStyle(ChatFormatting.BOLD)), 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + String timeString = TimeUtils.parseTime((int)((level.getDayTime() + DragonLib.daytimeShift()) % DragonLib.ticksPerDay()), ModClientConfig.TIME_FORMAT.get()); GuiUtils.drawString(graphics, font, x + GUI_WIDTH - 4 - font.width(timeString), y + 4, timeString, 0x4F4F4F, EAlignment.LEFT, false); renderSlidingText(graphics, x, y + 2, transX, transY); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java index 3a62722c..5369c7b0 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/NextConnectionsPage.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.train.TrainStop; import de.mrjulsen.crn.registry.ModAccessorTypes; @@ -75,7 +75,7 @@ public void tick() { @Override public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { - GuiUtils.drawString(graphics, font, 5, 4, ELanguage.translate(keyNextConnections).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 5, 4, CustomLanguage.translate(keyNextConnections).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); int y = 16; final int spacing = 5; @@ -85,7 +85,7 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par TrainStop stop = nextConnections.get(i); String terminus = stop.getDisplayTitle(); GuiUtils.drawString(graphics, font, 5, y, TimeUtils.parseTime(stop.getScheduledDepartureTime(), ModClientConfig.TIME_FORMAT.get()), 0xFFDBDBDB, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, font, 5 + timeWidth + spacing, y, GuiUtils.ellipsisString(font, TextUtils.text(stop.getTrainName()), trainNameWidth), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 5 + timeWidth + spacing, y, GuiUtils.ellipsisString(font, TextUtils.text(stop.getTrainDisplayName()), trainNameWidth), 0xFFDBDBDB, EAlignment.LEFT, false); GuiUtils.drawString(graphics, font, width() - 5, y, stop.getRealTimeStationTag().info().platform(), 0xFFDBDBDB, EAlignment.RIGHT, false); int terminusWidth = width() - 10 + timeWidth + trainNameWidth + spacing * 3 - font.width(stop.getRealTimeStationTag().info().platform()); GuiUtils.drawString(graphics, font, 5 + timeWidth + trainNameWidth + spacing * 2, y, GuiUtils.ellipsisString(font, TextUtils.text(terminus), terminusWidth), 0xFFDBDBDB, EAlignment.LEFT, false); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java index 89c2899e..998e9fa0 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/RouteOverviewPage.java @@ -66,9 +66,9 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par public static void renderStation(Graphics graphics, int y, int width, Font font, TrainStop stop, RoutePathIcons icon, boolean isStart, boolean isMissed) { final int precision = ModClientConfig.REALTIME_PRECISION_THRESHOLD.get(); - GuiUtils.drawString(graphics, font, 7, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TextUtils.text(TimeUtils.parseTime((isStart ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime()) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(isMissed ? ChatFormatting.STRIKETHROUGH : ChatFormatting.RESET), isMissed ? Constants.COLOR_DELAYED : 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 7, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TextUtils.text(TimeUtils.parseTime((isStart ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime()) + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get())).withStyle(isMissed ? ChatFormatting.STRIKETHROUGH : ChatFormatting.RESET), isMissed ? Constants.COLOR_DELAYED : 0xFFDBDBDB, EAlignment.LEFT, false); if (stop.shouldRenderRealTime() && !isMissed) { - GuiUtils.drawString(graphics, font, 7 + 32, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TimeUtils.parseTime((isStart ? stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) : stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision)) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 7 + 32, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, TimeUtils.parseTime((isStart ? stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) : stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision)) + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get()), stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); } icon.getAsSprite().render(graphics, 10 + 64, y); GuiUtils.drawString(graphics, font, 17 + 64 + RoutePathIcons.SPRITE_WIDTH, y + ENTRY_HEIGHT - 2 - font.lineHeight / 2, GuiUtils.ellipsisString(font, TextUtils.text(stop.getClientTag().tagName()), width - (17 + 64 + RoutePathIcons.SPRITE_WIDTH) - font.width(stop.getRealTimeStationTag().info().platform()) - 10), 0xFFDBDBDB, EAlignment.LEFT, false); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java index 3ff0b2ea..69aeaab3 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/TransferPage.java @@ -2,7 +2,7 @@ import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.gui.overlay.pages.RouteOverviewPage.RoutePathIcons; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.StationTag.StationInfo; import de.mrjulsen.crn.data.navigation.ClientRoute; import de.mrjulsen.crn.data.navigation.TransferConnection; @@ -33,12 +33,12 @@ public TransferPage(ClientRoute route, TransferConnection connection) { StationInfo info = connection.getDepartureStation().getRealTimeStationTag().info(); this.messageLabel = MultiLineLabel.create(font, info.platform() == null || info.platform().isBlank() ? - ELanguage.translate(keyTransfer, - connection.getDepartureStation().getTrainName(), + CustomLanguage.translate(keyTransfer, + connection.getDepartureStation().getTrainDisplayName(), terminus ) : - ELanguage.translate(keyTransferWithPlatform, - connection.getDepartureStation().getTrainName(), + CustomLanguage.translate(keyTransferWithPlatform, + connection.getDepartureStation().getTrainDisplayName(), terminus, info.platform() ), width - (15 + ModGuiIcons.ICON_SIZE)); @@ -59,7 +59,7 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par // Title ModGuiIcons.WALK.render(graphics, 5, y + 3); long transferTime = connection.getDepartureStation().getRealTimeDepartureTime() - DragonLib.getCurrentWorldTime(); - GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, ELanguage.translate(keyScheduleTransfer).append(" ").append(transferTime > 0 ? TextUtils.text(TimeUtils.parseDurationShort((int)transferTime)) : ELanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, CustomLanguage.translate(keyScheduleTransfer).append(" ").append(transferTime > 0 ? TextUtils.text(TimeUtils.parseDurationShort((int)transferTime)) : CustomLanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); y += 5 + ModGuiIcons.ICON_SIZE; // Details diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java index b70b04ef..0f05376a 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/overlay/pages/WelcomePage.java @@ -3,7 +3,7 @@ import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.gui.overlay.pages.RouteOverviewPage.RoutePathIcons; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.train.ClientTrainStop; import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.crn.data.navigation.ClientRoute; @@ -40,7 +40,7 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par // Title ModGuiIcons.TIME.render(graphics, 5, y + 3); long time = route.getCurrentPart().departureIn(); - GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, ELanguage.translate(keyDepartureIn).append(" ").append(time > 0 ? TextUtils.text(TimeUtils.parseDurationShort((int)time)) : ELanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + 3 + ModGuiIcons.ICON_SIZE / 2 - font.lineHeight / 2, CustomLanguage.translate(keyDepartureIn).append(" ").append(time > 0 ? TextUtils.text(TimeUtils.parseDurationShort((int)time)) : CustomLanguage.translate(keyTimeNow)).withStyle(ChatFormatting.BOLD), 0xFFFFFFFF, EAlignment.LEFT, false); y += 5 + ModGuiIcons.ICON_SIZE; // Details @@ -62,7 +62,7 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par ModGuiIcons.INFO.render(graphics, 5, y + detailsLineHeight + font.lineHeight / 2 - ModGuiIcons.ICON_SIZE / 2); GuiUtils.drawString(graphics, font, 10 + ModGuiIcons.ICON_SIZE, y + detailsLineHeight, TextUtils.text(String.format("%s %s | %s", route.getTransferCount(), - ELanguage.translate(keyTransferCount).getString(), + CustomLanguage.translate(keyTransferCount).getString(), TimeUtils.parseDurationShort((int)route.travelTime()) )), 0xFFDBDBDB, EAlignment.LEFT, false); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java index b94e1931..85fbe32d 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AbstractNavigatorScreen.java @@ -61,7 +61,7 @@ public void renderNavigatorBackground(Graphics graphics, int mouseX, int mouseY, renderScreenBackground(graphics); CreateDynamicWidgets.renderWindow(graphics, guiLeft, guiTop, GUI_WIDTH, GUI_HEIGHT, ContainerColor.GRAY, primaryColoring, FooterSize.DEFAULT.size(), FooterSize.SMALL.size(), false); GuiUtils.drawString(graphics, font, guiLeft + 6, guiTop + 4, getTitle(), 0x4F4F4F, EAlignment.LEFT, false); - String timeString = TimeUtils.parseTime((int)((Minecraft.getInstance().level.getDayTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + String timeString = TimeUtils.parseTime((int)((Minecraft.getInstance().level.getDayTime() + DragonLib.daytimeShift()) % DragonLib.ticksPerDay()), ModClientConfig.TIME_FORMAT.get()); GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH - 6, guiTop + 4, TextUtils.text(timeString), 0x4F4F4F, EAlignment.RIGHT, false); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java index 7dc93f06..ae883215 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/AdvancedDisplaySettingsScreen.java @@ -2,59 +2,80 @@ import java.util.Arrays; import java.util.List; - import com.simibubi.create.content.trains.station.NoShadowFontWrapper; import com.simibubi.create.foundation.gui.AllIcons; import com.simibubi.create.foundation.gui.element.GuiGameElement; -import com.simibubi.create.foundation.gui.widget.AbstractSimiWidget; -import com.simibubi.create.foundation.gui.widget.Label; import com.simibubi.create.foundation.gui.widget.ScrollInput; -import com.simibubi.create.foundation.utility.Components; +import com.simibubi.create.foundation.gui.widget.SelectionScrollInput; import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.AbstractAdvancedSidedDisplayBlock; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.display.properties.AdvancedDisplaySettingsData; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; import de.mrjulsen.crn.block.properties.EDisplayType; import de.mrjulsen.crn.block.properties.ESide; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; +import de.mrjulsen.crn.client.CRNGui; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; import de.mrjulsen.crn.client.gui.ModGuiIcons; import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; -import de.mrjulsen.crn.client.gui.widgets.DLCreateLabel; import de.mrjulsen.crn.client.gui.widgets.DLCreateSelectionScrollInput; +import de.mrjulsen.crn.client.gui.widgets.IconSlotWidget; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.modular.GuiBuilderContext; +import de.mrjulsen.crn.client.gui.widgets.modular.ModularWidgetContainer; import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.network.packets.cts.AdvancedDisplayUpdatePacket; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLCheckBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.render.GuiIcons; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; import de.mrjulsen.mcdragonlib.core.EAlignment; import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.data.Clipboard; +import de.mrjulsen.mcdragonlib.util.DLUtils; import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; -import net.minecraft.network.chat.Style; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; public class AdvancedDisplaySettingsScreen extends DLScreen { + private static boolean advancedSettingsExpanded = false; + private static final MutableComponent title = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.title"); - private static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/advanced_display_settings.png"); private static final int GUI_WIDTH = 212; - private static final int GUI_HEIGHT = 123; private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; + private static final FooterSize headerSize = FooterSize.DEFAULT; + private static final FooterSize footerSize = FooterSize.SMALL; + private static final int BASIC_GUI_HEIGHT = headerSize.size() + footerSize.size() + 76 + 5 + DLIconButton.DEFAULT_BUTTON_HEIGHT; + private static final int EXTENDED_GUI_HEIGHT = 220; + + private static final int guiHeight() { + return advancedSettingsExpanded ? EXTENDED_GUI_HEIGHT : BASIC_GUI_HEIGHT; + } private final Font shadowlessFont; private final ItemStack renderedItem; @@ -64,13 +85,12 @@ public class AdvancedDisplaySettingsScreen extends DLScreen { private final BlockPos pos; private DisplayTypeResourceKey typeKey; private EDisplayType type; + private IDisplaySettings settings; private final boolean canBeDoubleSided; private boolean doubleSided; private ScrollInput infoTypeInput; - private Label infoTypeLabel; private ScrollInput displayTypeInput; - private Label displayTypeLabel; private DLCreateIconButton globalSettingsButton; private final MutableComponent tooltipGlobalSettings = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.global_settings.tooltip"); @@ -78,43 +98,227 @@ public class AdvancedDisplaySettingsScreen extends DLScreen { private final MutableComponent tooltipInfoType = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.info_type"); private final MutableComponent textDoubleSided = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.double_sided"); + @SuppressWarnings("resource") + private final MutableComponent textAdvancedSettings(int maxWidth) { + Font font = Minecraft.getInstance().font; + MutableComponent comp = TextUtils.translate("gui.createrailwaysnavigator.advanced_display_settings.advanced_settings").withStyle(ChatFormatting.BOLD); + MutableComponent ellipsisText = TextUtils.text("...").withStyle(ChatFormatting.BOLD); + final boolean tooWide = font.width(comp) + font.width(ellipsisText) > maxWidth; + return tooWide ? TextUtils.text(font.substrByWidth(comp, maxWidth - font.width(ellipsisText)).getString() + "...").withStyle(ChatFormatting.BOLD) : comp; + } + private int guiLeft, guiTop; + private GuiAreaDefinition workingArea; private DLCreateIconButton backButton; + private ModularWidgetContainer commonSettingsContainer; + private ModularWidgetContainer advancedSettingsContainer; - private final Cache> displayTypes = new Cache<>(() -> AdvancedDisplaysRegistry.getAllOfTypeAsKey(type)); + private final Cache> displayTypes = new Cache<>(() -> AdvancedDisplaysRegistry.getAllOfTypeAsKey(type), ECachingPriority.ALWAYS); + + private final AdvancedDisplayBlockEntity blockEntity; @SuppressWarnings("resource") public AdvancedDisplaySettingsScreen(AdvancedDisplayBlockEntity blockEntity) { super(title); + this.blockEntity = blockEntity; + this.settings = blockEntity.getSettings(); this.shadowlessFont = new NoShadowFontWrapper(Minecraft.getInstance().font); this.pos = blockEntity.getBlockPos(); this.level = blockEntity.getLevel(); - this.type = blockEntity.getDisplayTypeKey().category(); - this.typeKey = blockEntity.getDisplayTypeKey(); + this.type = blockEntity.getDisplayType().category(); + this.typeKey = blockEntity.getDisplayType(); this.renderedItem = new ItemStack(blockEntity.getBlockState().getBlock()); this.canBeDoubleSided = blockEntity.getBlockState().getBlock() instanceof AbstractAdvancedSidedDisplayBlock; this.doubleSided = !canBeDoubleSided || blockEntity.getBlockState().getValue(AbstractAdvancedSidedDisplayBlock.SIDE) == ESide.BOTH; } @Override - public void onClose() { - CreateRailwaysNavigator.net().CHANNEL.sendToServer(new AdvancedDisplayUpdatePacket(level, pos, typeKey, doubleSided)); + public void onClose() { + CreateRailwaysNavigator.net().CHANNEL.sendToServer(new AdvancedDisplayUpdatePacket(level, pos, typeKey, doubleSided, settings)); super.onClose(); } + private void reinit() { + this.clearWidgets(); + this.setFocused((GuiEventListener)null); + this.init(); + this.triggerImmediateNarration(false); + } + @Override protected void init() { - super.init(); + super.init(); + displayTypeInput = null; + infoTypeInput = null; + guiLeft = this.width / 2 - GUI_WIDTH / 2; - guiTop = this.height / 2 - GUI_HEIGHT / 2; + guiTop = this.height / 2 - guiHeight() / 2; + workingArea = new GuiAreaDefinition(guiLeft + 1, guiTop + headerSize.size(), GUI_WIDTH - 2, guiHeight() - headerSize.size() - footerSize.size()); + + // Content + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, 0, 0, 0, GuiAreaDefinition.empty()); + commonSettingsContainer = addRenderableWidget(new ModularWidgetContainer(this, workingArea.getX() + 2, workingArea.getY() + 1, workingArea.getWidth() - 4, 76, (w, builder) -> { + builder.addLine("type", (line) -> { + IconSlotWidget icon = line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, type.getIcon().getAsSprite(16, 16))); + displayTypeInput = line.add(new DLCreateSelectionScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, line.getRemainingWidth() - 6, 18) + .setRenderArrow(true) + .forOptions(Arrays.stream(EDisplayType.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) + .setState(type.getId()) + .titled(tooltipDisplayType) + .calling((i) -> { + type = EDisplayType.getTypeById(i); + icon.setIcon(type.getIcon().getAsSprite(16, 16)); + displayTypes.clear(); + displayTypeInput.addHint(TextUtils.translate(type.getValueInfoTranslationKey(CreateRailwaysNavigator.MOD_ID))); + + DLUtils.doIfNotNull((SelectionScrollInput)infoTypeInput, x -> { + x.setState(0); + x.forOptions(displayTypes.get().stream().map(a -> TextUtils.translate(a.getTranslationKey())).toList()); + x.onChanged(); + }); + }) + .addHint(TextUtils.translate(type.getValueInfoTranslationKey(CreateRailwaysNavigator.MOD_ID)))); + displayTypeInput.onChanged(); + }); + builder.addLine("variant", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.VERY_DETAILED.getAsSprite(16, 16))); + infoTypeInput = line.add(new DLCreateSelectionScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, line.getRemainingWidth() - 6, 18) + .setRenderArrow(true) + .forOptions(displayTypes.get().stream().map(x -> TextUtils.translate(x.getTranslationKey())).toList()) + .setState(displayTypes.get().indexOf(typeKey))) + .titled(tooltipInfoType) + .calling((i) -> { + typeKey = displayTypes.get().get(i); + IDisplaySettings oldSettings = settings; + settings = blockEntity.getDisplayType().equals(typeKey) ? blockEntity.getSettings() : AdvancedDisplaysRegistry.createSettings(typeKey); + settings.onChangeSettings(oldSettings); + DLUtils.doIfNotNull(advancedSettingsContainer, ModularWidgetContainer::build); + }) + ; + infoTypeInput.onChanged(); + }); + builder.addLine("double_sided", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.DOUBLE_SIDED.getAsSprite(16, 16))); + line.add(new DLCheckBox(line.getCurrentX() + 6, line.getY() + 2, line.getRemainingWidth() - 6, textDoubleSided.getString(), doubleSided, (box) -> { + this.doubleSided = box.isChecked(); + })).active = canBeDoubleSided; + }); + }, scrollBar, 18, 18, 4, 4)); + addRenderableWidget(scrollBar); + + // Advanced Settings + final DLScreen screen = this; + DLIconButton copyBtn = null; + DLIconButton pasteBtn = null; + DLIconButton resetBtn = null; + + if (advancedSettingsExpanded) { + ModernVerticalScrollBar advancedSettingsScrollBar = new ModernVerticalScrollBar(this, 0, 0, 0, GuiAreaDefinition.empty()); + advancedSettingsContainer = addRenderableWidget(new ModularWidgetContainer(this, workingArea.getX() + 2, commonSettingsContainer.y() + commonSettingsContainer.height() + 3 + 18, workingArea.getWidth() - 4, workingArea.getHeight() - 2 - commonSettingsContainer.height() - 3 - 18, (w, builder) -> { + settings.buildGui(new GuiBuilderContext(builder, w)); + }, advancedSettingsScrollBar, 18, 18, 0, 4)); + advancedSettingsScrollBar.setScrollArea(GuiAreaDefinition.of(advancedSettingsContainer)); + addRenderableWidget(advancedSettingsScrollBar); + + copyBtn = addRenderableWidget(new DLIconButton( + ButtonType.DEFAULT, + AreaStyle.FLAT, + ModGuiIcons.COPY.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE), + workingArea.getX() + workingArea.getWidth() - 2 - DLIconButton.DEFAULT_BUTTON_WIDTH, + commonSettingsContainer.y() + commonSettingsContainer.height() + 3, + TextUtils.empty(), + (b) -> { + Clipboard.put(AdvancedDisplaySettingsData.class, new AdvancedDisplaySettingsData(typeKey, settings, doubleSided)); + } + ) { + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(screen, this, List.of(Constants.TEXT_COPY), screen.width() / 3, graphics, mouseX, mouseY); + } + }); + copyBtn.setBackColor(0); + pasteBtn = addRenderableWidget(new DLIconButton( + ButtonType.DEFAULT, + AreaStyle.FLAT, + ModGuiIcons.PASTE.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE), + copyBtn.x() - DLIconButton.DEFAULT_BUTTON_WIDTH, + copyBtn.y(), + TextUtils.empty(), + (b) -> { + Clipboard.get(AdvancedDisplaySettingsData.class).ifPresent(x -> { + this.typeKey = x.getKey(); + this.type = x.getKey().category(); + this.settings = x.getSettings(); + this.doubleSided = x.isDoubleSided(); + reinit(); + }); + } + ) { + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(screen, this, List.of(Constants.TEXT_PASTE), screen.width() / 3, graphics, mouseX, mouseY); + } + }); + pasteBtn.setBackColor(0); + resetBtn = addRenderableWidget(new DLIconButton( + ButtonType.DEFAULT, + AreaStyle.FLAT, + ModGuiIcons.REFRESH.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE), + pasteBtn.x() - DLIconButton.DEFAULT_BUTTON_WIDTH, + pasteBtn.y(), + TextUtils.empty(), + (b) -> { + settings = AdvancedDisplaysRegistry.createSettings(typeKey); + DLUtils.doIfNotNull(advancedSettingsContainer, ModularWidgetContainer::build); + } + ) { + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderFrontLayer(graphics, mouseX, mouseY, partialTicks); + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(screen, this, List.of(Constants.TEXT_RESET), screen.width() / 3, graphics, mouseX, mouseY); + } + }); + resetBtn.setBackColor(0); + } - backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 179, guiTop + 99, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIRM)); + DLIconButton expandCollapseBtn = addRenderableWidget(new DLIconButton( + ButtonType.DEFAULT, + AreaStyle.FLAT, + (advancedSettingsExpanded ? GuiIcons.ARROW_DOWN : GuiIcons.ARROW_RIGHT).getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE), + workingArea.getX() + 2, + commonSettingsContainer.y() + commonSettingsContainer.height() + 3, + workingArea.getWidth() - 4 - (advancedSettingsExpanded ? copyBtn.width() + pasteBtn.width() + resetBtn.width() : 0), + DLIconButton.DEFAULT_BUTTON_HEIGHT, + TextUtils.empty(), + (b) -> { + advancedSettingsExpanded = !advancedSettingsExpanded; + reinit(); + } + )); + expandCollapseBtn.setMessage(textAdvancedSettings(expandCollapseBtn.width() - ModGuiIcons.ICON_SIZE - 6)); + expandCollapseBtn.setTextAlignment(EAlignment.LEFT); + expandCollapseBtn.setBackColor(0); + expandCollapseBtn.setFontColor(DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE); + + // Buttons + backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 7 - DEFAULT_ICON_BUTTON_WIDTH, guiTop + guiHeight() - 6 - DEFAULT_ICON_BUTTON_HEIGHT, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIRM)); backButton.withCallback(() -> { onClose(); }); - DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 179 - DEFAULT_ICON_BUTTON_WIDTH - 10, guiTop + 99, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { + DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 17 - DEFAULT_ICON_BUTTON_WIDTH * 2, guiTop + guiHeight() - 6 - DEFAULT_ICON_BUTTON_HEIGHT, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); @@ -123,32 +327,10 @@ public void onClick(double mouseX, double mouseY) { }); addTooltip(DLTooltip.of(Constants.TEXT_HELP).assignedTo(helpButton)); - displayTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 23 + 5, Components.immutableEmpty()).withShadow()); - displayTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 23, 138, 18) - .forOptions(Arrays.stream(EDisplayType.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) - .titled(tooltipDisplayType) - .writingTo(displayTypeLabel) - .calling((i) -> { - type = EDisplayType.getTypeById(i); - displayTypes.clear(); - createDisplayBrowser(); - displayTypeInput.addHint(displayTypeHint()); - }) - .addHint(displayTypeHint()) - .setState(type.getId())); - displayTypeInput.onChanged(); - - infoTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 45 + 5, Components.immutableEmpty()).withShadow()); - createDisplayBrowser(); - - addRenderableWidget(new DLCheckBox(guiLeft + 45, guiTop + 67 + 1, 138, textDoubleSided.getString(), doubleSided, (box) -> { - this.doubleSided = box.isChecked(); - })).active = canBeDoubleSided; - // Global Options Button if (minecraft.player.hasPermissions(ModCommonConfig.GLOBAL_SETTINGS_PERMISSION_LEVEL.get())) { final Screen instance = this; - globalSettingsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 7, guiTop + 99, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.SETTINGS.getAsCreateIcon()) { + globalSettingsButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + 7, guiTop + guiHeight() - 6 - DEFAULT_ICON_BUTTON_HEIGHT, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.SETTINGS.getAsCreateIcon()) { @Override public void onClick(double mouseX, double mouseY) { super.onClick(mouseX, mouseY); @@ -157,75 +339,31 @@ public void onClick(double mouseX, double mouseY) { }); addTooltip(DLTooltip.of(tooltipGlobalSettings).assignedTo(globalSettingsButton)); } - } - private void createDisplayBrowser() { - if (infoTypeInput != null) { - removeWidget(infoTypeInput); - } - infoTypeInput = new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 45, 138, 18) - .forOptions(displayTypes.get().stream().map(x -> TextUtils.translate(x.getTranslationKey())).toList()) - .titled(tooltipInfoType) - .writingTo(infoTypeLabel) - .calling((i) -> { - typeKey = displayTypes.get().get(i); - }) - .setState(displayTypes.get().indexOf(typeKey)); - infoTypeInput.onChanged(); - addRenderableWidget(infoTypeInput); - } - - private MutableComponent displayTypeHint() { - StringBuilder sb = new StringBuilder(); - font.getSplitter().splitLines(TextUtils.translate(typeKey.category().getValueInfoTranslationKey(CreateRailwaysNavigator.MOD_ID)), width() / 3, Style.EMPTY).forEach(x -> { - sb.append("\n" + x.getString()); - }); - return TextUtils.text(sb.toString()); } @Override public boolean isPauseScreen() { return false; } - - @Override - public void tick() { - super.tick(); - infoTypeInput.tick(); - displayTypeInput.tick(); - } @Override public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { renderScreenBackground(graphics); - GuiUtils.drawTexture(GUI, graphics, guiLeft, guiTop, 0, 0, GUI_WIDTH, GUI_HEIGHT); - GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 6, guiTop + 4, title, DragonLib.NATIVE_UI_FONT_COLOR, EAlignment.LEFT, false); - - GuiGameElement.of(renderedItem).at(guiLeft + GUI_WIDTH, guiTop + GUI_HEIGHT - 48, -200) - .scale(4f) - .render(graphics.poseStack()); + CreateDynamicWidgets.renderWindow(graphics, guiLeft, guiTop, GUI_WIDTH, guiHeight(), ContainerColor.PURPLE, BarColor.GOLD, BarColor.GRAY, headerSize.size(), footerSize.size(), false); + CreateDynamicWidgets.renderVerticalSeparator(graphics, guiLeft + GUI_WIDTH - 31, guiTop + guiHeight() - footerSize.size() + 2, footerSize.size() - 4, BarColor.GRAY); + GuiUtils.drawTexture(CRNGui.GUI, graphics, guiLeft + GUI_WIDTH - 3, guiTop + guiHeight() - footerSize.size() / 2 - 9, 11, 18, 0, 12, 11, 18, CRNGui.GUI_WIDTH, CRNGui.GUI_HEIGHT); - type.getIcon().render(graphics, guiLeft + 22, guiTop + 24); - ModGuiIcons.VERY_DETAILED.render(graphics, guiLeft + 22, guiTop + 46); - ModGuiIcons.DOUBLE_SIDED.render(graphics, guiLeft + 22, guiTop + 68); + int commonHeight = commonSettingsContainer.getHeight() + 4; + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX(), workingArea.getY() - 1, workingArea.getWidth(), commonHeight, ContainerColor.PURPLE); + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX(), workingArea.getY() - 2 + commonHeight, workingArea.getWidth(), workingArea.getHeight() - commonHeight + 3, ContainerColor.GRAY); + GuiUtils.drawString(graphics, shadowlessFont, guiLeft + 6, guiTop + 4, title, DragonLib.NATIVE_UI_FONT_COLOR, EAlignment.LEFT, false); super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); - } - @Override - public void renderFrontLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { - super.renderFrontLayer(graphics, pMouseX, pMouseY, pPartialTick); - for (Widget widget : renderables) { - if (widget instanceof AbstractSimiWidget simiWidget && simiWidget.isHoveredOrFocused() - && simiWidget.visible) { - List tooltip = simiWidget.getToolTip(); - if (tooltip.isEmpty()) - continue; - int ttx = simiWidget.lockedTooltipX == -1 ? pMouseX : simiWidget.lockedTooltipX + simiWidget.x; - int tty = simiWidget.lockedTooltipY == -1 ? pMouseY : simiWidget.lockedTooltipY + simiWidget.y; - renderComponentTooltip(graphics.poseStack(), tooltip, ttx, tty); - } - } + GuiGameElement.of(renderedItem).at(guiLeft + GUI_WIDTH + 11, guiTop + guiHeight() - 48, -200) + .scale(4f) + .render(graphics.poseStack()); } } \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java index 092a0b0d..2de81de9 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/GlobalSettingsScreen.java @@ -140,7 +140,7 @@ private void addBlacklistedStationsWidget(List datalist, DLVerticalScrol GuiAreaDefinition workspace = option.getContentSpace(); DataListContainer, String> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, (list) -> { - return list.iterator(); + return list.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).iterator(); }, (data, entryWidget) -> { entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { GlobalSettingsClient.removeStationFromBlacklist(entry, (res) -> { @@ -187,7 +187,7 @@ private void addBlacklistedTrainsWidget(List datalist, DLVerticalScrollB GuiAreaDefinition workspace = option.getContentSpace(); DataListContainer, String> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, (list) -> { - return list.iterator(); + return list.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).iterator(); }, (data, entryWidget) -> { entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { GlobalSettingsClient.removeTrainFromBlacklist(entry, (res) -> { @@ -234,7 +234,7 @@ private void addTrainGroupsWidget(List datalist, DLVerticalScrollBar GuiAreaDefinition workspace = option.getContentSpace(); DataListContainer, TrainGroup> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, (list) -> { - return list.iterator(); + return list.stream().sorted((a, b) -> a.getGroupName().compareToIgnoreCase(b.getGroupName())).iterator(); }, (data, entryWidget) -> { entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { GlobalSettingsClient.deleteTrainGroup(entry.getGroupName(), () -> { @@ -246,7 +246,7 @@ private void addTrainGroupsWidget(List datalist, DLVerticalScrollBar DLIconButton colorBtn = entryWidget.addButton(ModGuiIcons.COLOR_PALETTE.getAsSprite(16, 16), textColor, (btn, tg, entry, refreshAction) -> { final TrainGroup e = entry; - FlyoutColorPicker flyout = new FlyoutColorPicker<>(this, e.getColor(), this::addRenderableWidget, (w) -> { + FlyoutColorPicker flyout = new FlyoutColorPicker<>(this, e.getColor(), Constants.DEFAULT_TRAIN_TYPE_COLORS, 5, true, true, this::addRenderableWidget, (w) -> { GlobalSettingsClient.updateTrainGroupColor(e.getGroupName(), ((FlyoutColorPicker)w).getColorPicker().getSelectedColor(), () -> { GlobalSettingsClient.getTrainGroups((res) -> { refreshAction.accept(Optional.ofNullable(res)); @@ -299,7 +299,7 @@ private void addTrainLinesWidget(List datalist, DLVerticalScrollBar s GuiAreaDefinition workspace = option.getContentSpace(); DataListContainer, TrainLine> cont = new DataListContainer<>(option, workspace.getX(), workspace.getY(), workspace.getWidth(), datalist, (list) -> { - return list.iterator(); + return list.stream().sorted((a, b) -> a.getLineName().compareToIgnoreCase(b.getLineName())).iterator(); }, (data, entryWidget) -> { entryWidget.addDeleteButton((btn, tg, entry, refreshAction) -> { GlobalSettingsClient.deleteTrainLine(entry.getLineName(), () -> { @@ -311,7 +311,7 @@ private void addTrainLinesWidget(List datalist, DLVerticalScrollBar s DLIconButton colorBtn = entryWidget.addButton(ModGuiIcons.COLOR_PALETTE.getAsSprite(16, 16), textColor, (btn, tg, entry, refreshAction) -> { final TrainLine e = entry; - FlyoutColorPicker flyout = new FlyoutColorPicker<>(this, e.getColor(), this::addRenderableWidget, (w) -> { + FlyoutColorPicker flyout = new FlyoutColorPicker<>(this, e.getColor(), Constants.DEFAULT_TRAIN_TYPE_COLORS, 5, true, true, this::addRenderableWidget, (w) -> { GlobalSettingsClient.updateTrainLineColor(e.getLineName(), ((FlyoutColorPicker)w).getColorPicker().getSelectedColor(), () -> { GlobalSettingsClient.getTrainLines((res) -> { refreshAction.accept(Optional.ofNullable(res)); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java index d4d62821..5d9afcca 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/NavigatorScreen.java @@ -257,7 +257,7 @@ public void onClick(double mouseX, double mouseY) { DataAccessor.getFromServer(new NavigationData(stationFrom, stationTo, Minecraft.getInstance().player.getUUID()), ModAccessorTypes.NAVIGATE, (routeList) -> { routes.addAll(routeList); routeViewer.displayRoutes(ImmutableList.copyOf(routes)); - routeViewer.displayRoutes(routeList); + //routeViewer.displayRoutes(routeList); isLoadingRoutes = false; DataAccessor.getFromServer(null, ModAccessorTypes.ALL_TRAINS_INITIALIZED, (result) -> { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java index c376fc77..2a41c494 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/RouteDetailsScreen.java @@ -20,7 +20,7 @@ import de.mrjulsen.crn.client.gui.widgets.DLCreateIndicator; import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; import de.mrjulsen.crn.client.gui.widgets.RouteDetailsViewer; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.SavedRoutesManager; import de.mrjulsen.crn.data.navigation.ClientRoute; import de.mrjulsen.crn.event.ModCommonEvents; @@ -44,8 +44,9 @@ public class RouteDetailsScreen extends AbstractNavigatorScreen { - private final MutableComponent textDeparture = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.departure"); - private final MutableComponent timeNowText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".time.now"); + private final MutableComponent textDeparture = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.departure"); + private final MutableComponent textArrival = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.arrival"); + private final MutableComponent timeNowText = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".time.now"); private final MutableComponent tooltipSaveRoute = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.save_route.tooltip"); private final MutableComponent tooltipRemoveRoute = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.remove_route.tooltip"); private final MutableComponent tooltipShowPopup = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.show_popup.tooltip"); @@ -172,7 +173,7 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par if (!route.isAnyCancelled()) { if (route.getStart().isDeparted()) { - GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 19, "Ankunft in", 0xFFFFFF, EAlignment.CENTER, false); + GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 19, textArrival, 0xFFFFFF, EAlignment.CENTER, false); } else { GuiUtils.drawString(graphics, font, guiLeft + GUI_WIDTH / 2, guiTop + 19, textDeparture, 0xFFFFFF, EAlignment.CENTER, false); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneyScreen.java similarity index 89% rename from common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java rename to common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneyScreen.java index 23d26ab2..46af4de9 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneySreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainJourneyScreen.java @@ -21,14 +21,14 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.world.level.Level; -public class TrainJourneySreen extends AbstractNavigatorScreen { +public class TrainJourneyScreen extends AbstractNavigatorScreen { private final ClientRoute route; private final ClientRoutePart part; private RouteDetailsViewer viewer; - public TrainJourneySreen(Screen lastScreen, ClientRoute route, UUID trainId) { + public TrainJourneyScreen(Screen lastScreen, ClientRoute route, UUID trainId) { super(lastScreen, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.title"), BarColor.GOLD); this.route = route; this.part = route.getClientParts().stream().filter(x -> x.getTrainId().equals(trainId)).findFirst().orElse(route.getFirstClientPart()); @@ -66,7 +66,7 @@ public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float par int y = FooterSize.DEFAULT.size() - 1; CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, 32, ContainerColor.BLUE); GuiUtils.drawString(graphics, font, guiLeft + 8, guiTop + y + 7, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.date", (ModCommonEvents.getPhysicalLevel().dayTime() / Level.TICKS_PER_DAY)), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, font, guiLeft + 8, guiTop + y + 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.train", part.getFirstStop().getTrainName(), part.getFirstStop().getTrainId().toString().split("-")[0], part.getFirstStop().getDisplayTitle()), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, guiLeft + 8, guiTop + y + 18, TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.train", part.getFirstStop().getTrainDisplayName(), part.getFirstStop().getTrainId().toString().split("-")[0], part.getFirstStop().getDisplayTitle()), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); y += 32 - 1; CreateDynamicWidgets.renderContainer(graphics, guiLeft + 1, guiTop + y, GUI_WIDTH - 2, GUI_HEIGHT - y - FooterSize.SMALL.size() + 1, ContainerColor.GOLD); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java index ec310b75..7e8461ed 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSectionSettingsScreen.java @@ -148,7 +148,7 @@ public void onClick(double mouseX, double mouseY) { List groupsList = new ArrayList<>(trainGroups.stream().map(x -> TextUtils.text(x.getGroupName())).toList()); groupsList.add(0, textNone); displayTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 23 + 5, Components.immutableEmpty()).withShadow()); - displayTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 23, 138, 18) + displayTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(this, guiLeft + 45, guiTop + 23, 138, 18) .forOptions(groupsList) .titled(tooltipTrainGroup) .writingTo(displayTypeLabel) @@ -162,7 +162,7 @@ public void onClick(double mouseX, double mouseY) { List linesList = new ArrayList<>(trainLines.stream().map(x -> TextUtils.text(x.getLineName())).toList()); linesList.add(0, textNone); infoTypeLabel = addRenderableWidget(new DLCreateLabel(guiLeft + 45 + 5, guiTop + 45 + 5, Components.immutableEmpty()).withShadow()); - infoTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(guiLeft + 45, guiTop + 45, 138, 18) + infoTypeInput = addRenderableWidget(new DLCreateSelectionScrollInput(this, guiLeft + 45, guiTop + 45, 138, 18) .forOptions(linesList) .titled(tooltipTrainLine) .writingTo(infoTypeLabel) diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java new file mode 100644 index 00000000..ddebd7d5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java @@ -0,0 +1,178 @@ +package de.mrjulsen.crn.client.gui.screen; + +import java.util.Arrays; + +import com.simibubi.create.content.trains.schedule.condition.TimedWaitCondition.TimeUnit; +import com.simibubi.create.foundation.gui.AllIcons; +import com.simibubi.create.foundation.utility.Lang; + +import de.mrjulsen.crn.Constants; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.BarColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ContainerColor; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.FooterSize; +import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.gui.widgets.DLCreateIconButton; +import de.mrjulsen.crn.client.gui.widgets.DLCreateScrollInput; +import de.mrjulsen.crn.client.gui.widgets.DLCreateSelectionScrollInput; +import de.mrjulsen.crn.client.gui.widgets.IconSlotWidget; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.crn.client.gui.widgets.modular.ModularWidgetContainer; +import de.mrjulsen.crn.data.schedule.condition.TrainSeparationCondition; +import de.mrjulsen.crn.data.train.StationDepartureHistory.ETrainFilter; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.MutableComponent; + +public class TrainSeparationSettingsScreen extends DLScreen { + + private static final MutableComponent title = TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.condition.train_separation.settings"); + private static final int GUI_WIDTH = 212; + private static final int DEFAULT_ICON_BUTTON_WIDTH = 18; + private static final int DEFAULT_ICON_BUTTON_HEIGHT = 18; + private static final FooterSize headerSize = FooterSize.DEFAULT; + private static final FooterSize footerSize = FooterSize.SMALL; + private static final int LINES = 2; + private static final int GUI_HEIGHT = headerSize.size() + footerSize.size() + 22 * LINES + 12; + + private int guiLeft, guiTop; + private GuiAreaDefinition workingArea; + + private DLCreateIconButton backButton; + private ModularWidgetContainer commonSettingsContainer; + + private final Screen lastScreen; + private final CompoundTag nbt; + + //private String stationFilter; + private int time = 5; + private TimeUnit timeUnit = TimeUnit.SECONDS; + private ETrainFilter filter = ETrainFilter.ANY; + + public TrainSeparationSettingsScreen(Screen lastScreen, CompoundTag nbt) { + super(title); + this.lastScreen = lastScreen; + this.nbt = nbt; + this.time = nbt.getInt(TrainSeparationCondition.NBT_TIME); + this.timeUnit = TimeUnit.values()[nbt.getInt(TrainSeparationCondition.NBT_TIME_UNIT)]; + this.filter = ETrainFilter.getByIndex(nbt.getByte(TrainSeparationCondition.NBT_TRAIN_FILTER)); + } + + @Override + public void onClose() { + super.onClose(); + nbt.putInt(TrainSeparationCondition.NBT_TIME, time); + nbt.putInt(TrainSeparationCondition.NBT_TIME_UNIT, timeUnit.ordinal()); + nbt.putByte(TrainSeparationCondition.NBT_TRAIN_FILTER, filter.getIndex()); + Minecraft.getInstance().setScreen(lastScreen); + } + + @Override + protected void init() { + super.init(); + + guiLeft = this.width / 2 - GUI_WIDTH / 2; + guiTop = this.height / 2 - GUI_HEIGHT / 2; + workingArea = new GuiAreaDefinition(guiLeft + 1, guiTop + headerSize.size(), GUI_WIDTH - 2, GUI_HEIGHT - headerSize.size() - footerSize.size()); + + // Content + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(this, 0, 0, 0, GuiAreaDefinition.empty()); + commonSettingsContainer = addRenderableWidget(new ModularWidgetContainer(this, workingArea.getX() + 2, workingArea.getY() + 1, workingArea.getWidth() - 4, 22 * LINES + 10, (w, builder) -> { + /* + builder.addLine("text", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TEXT.getAsSprite(16, 16))); + line.add(new DLCreateTextBox(font, line.getCurrentX() + 6, line.getY() + 2, line.getRemainingWidth() - 6, TextUtils.empty())) + .setRenderArrow(true) + .setResponder((txt) -> { + this.stationFilter = txt; + }); + }); + */ + + builder.addLine("time", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TIME.getAsSprite(16, 16))); + line.add(new DLCreateScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, 30, 18) + .setRenderArrow(true) + .titled(Lang.translateDirect("generic.duration")) + .withShiftStep(15) + .withRange(0, 121) + .setState(time) + .calling(x -> { + time = x; + }) + ); + + line.add(new DLCreateSelectionScrollInput(this, line.getCurrentX() + 4, line.getY() + 2, 65, 18) + .forOptions(TimeUnit.translatedOptions()) + .setState(timeUnit.ordinal()) + .titled(Lang.translateDirect("generic.timeUnit")) + .setState(timeUnit.ordinal()) + .calling((i) -> { + timeUnit = TimeUnit.values()[i]; + }) + ); + }); + + builder.addLine("train_filter", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TRAIN.getAsSprite(16, 16))); + line.add(new DLCreateSelectionScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, line.getRemainingWidth() - 6, 18) + .setRenderArrow(true) + .forOptions(Arrays.stream(ETrainFilter.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) + .setState(filter.getIndex()) + .titled(TextUtils.translate(ETrainFilter.ANY.getEnumTranslationKey(CreateRailwaysNavigator.MOD_ID))) + .addHint(TextUtils.translate(ETrainFilter.ANY.getEnumDescriptionTranslationKey(CreateRailwaysNavigator.MOD_ID))) + .setState(filter.getIndex()) + .calling((i) -> { + filter = ETrainFilter.getByIndex(i); + }) + ); + }); + }, scrollBar, 18, 18, 4, 4)); + addRenderableWidget(scrollBar); + + // Buttons + backButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 7 - DEFAULT_ICON_BUTTON_WIDTH, guiTop + GUI_HEIGHT - 6 - DEFAULT_ICON_BUTTON_HEIGHT, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, AllIcons.I_CONFIRM)); + backButton.withCallback(() -> { + onClose(); + }); + + DLCreateIconButton helpButton = this.addRenderableWidget(new DLCreateIconButton(guiLeft + GUI_WIDTH - 17 - DEFAULT_ICON_BUTTON_WIDTH * 2, guiTop + GUI_HEIGHT - 6 - DEFAULT_ICON_BUTTON_HEIGHT, DEFAULT_ICON_BUTTON_WIDTH, DEFAULT_ICON_BUTTON_HEIGHT, ModGuiIcons.HELP.getAsCreateIcon()) { + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + Util.getPlatform().openUri(Constants.HELP_PAGE_TRAIN_SEPARATION); + } + }); + addTooltip(DLTooltip.of(Constants.TEXT_HELP).assignedTo(helpButton)); + + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float pPartialTick) { + renderScreenBackground(graphics); + CreateDynamicWidgets.renderWindow(graphics, guiLeft, guiTop, GUI_WIDTH, GUI_HEIGHT, ContainerColor.PURPLE, BarColor.GOLD, BarColor.GRAY, headerSize.size(), footerSize.size(), false); + CreateDynamicWidgets.renderVerticalSeparator(graphics, guiLeft + GUI_WIDTH - 31, guiTop + GUI_HEIGHT - footerSize.size() + 2, footerSize.size() - 4, BarColor.GRAY); + + int commonHeight = commonSettingsContainer.getHeight() + 4; + CreateDynamicWidgets.renderContainer(graphics, workingArea.getX(), workingArea.getY() - 1, workingArea.getWidth(), commonHeight, ContainerColor.PURPLE); + GuiUtils.drawString(graphics, font, guiLeft + 6, guiTop + 4, title, DragonLib.NATIVE_UI_FONT_COLOR, EAlignment.LEFT, false); + + super.renderMainLayer(graphics, pMouseX, pMouseY, pPartialTick); + } +} \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java index 92b7faeb..717d1a42 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractFlyoutWidget.java @@ -9,8 +9,8 @@ import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; @@ -22,7 +22,7 @@ import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarrationElementOutput; -public abstract class AbstractFlyoutWidget extends WidgetContainer { +public abstract class AbstractFlyoutWidget extends DLWidgetContainer { protected final DLScreen screen; protected final FlyoutPointer pointer; @@ -97,6 +97,7 @@ public void closeImmediately() { @Override public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { graphics.poseStack().pushPose(); + graphics.poseStack().translate(0, 0, 100); RenderSystem.enableBlend(); RenderSystem.defaultBlendFunc(); RenderSystem.enableDepthTest(); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java index 9e10c75a..13d25b96 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/AbstractNotificationPopup.java @@ -8,14 +8,14 @@ import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.narration.NarrationElementOutput; -public abstract class AbstractNotificationPopup extends WidgetContainer { +public abstract class AbstractNotificationPopup extends DLWidgetContainer { protected final DLScreen screen; protected final ColorShade shade; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java index 277dbf12..5723a413 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/CRNListBox.java @@ -5,17 +5,17 @@ import java.util.Map; import java.util.Set; import java.util.Map.Entry; -import java.util.function.Function; +import java.util.function.BiFunction; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class CRNListBox extends ScrollableWidgetContainer { +public class CRNListBox extends DLScrollableWidgetContainer { private final Screen parent; private final DLAbstractScrollBar scrollBar; @@ -29,7 +29,7 @@ public CRNListBox(Screen parent, int x, int y, int width, int height, DLAbstract scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -38,13 +38,14 @@ public Screen getParent() { return parent; } - public void displayData(List data, Function createItem) { + public void displayData(List data, BiFunction createItem) { clearWidgets(); values.clear(); contentHeight = 0; for (int i = 0; i < data.size(); i++) { T entry = data.get(i); - W widget = createItem.apply(entry); + W widget = createItem.apply(entry, i); + if (widget == null) continue; widget.set_x(x()); widget.set_width(width()); widget.set_y(y() + contentHeight); @@ -52,7 +53,7 @@ public void displayData(List data, Function createItem) { values.put(widget, entry); contentHeight += widget.height(); } - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); } public Set> getEntries() { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java index 94ac0b66..6fb421b7 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorPickerWidget.java @@ -8,7 +8,7 @@ import de.mrjulsen.mcdragonlib.client.gui.DLColorPickerScreen; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; @@ -17,36 +17,42 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class ColorPickerWidget extends WidgetContainer { +public class ColorPickerWidget extends DLWidgetContainer { private int selectedColor = 0; - public ColorPickerWidget(Screen parent, int px, int py, int[] sampleColors, int maxColorsPerLine, int preselectedColor, Consumer onPick) { + public ColorPickerWidget(Screen parent, int px, int py, int[] sampleColors, int maxColorsPerLine, int preselectedColor, boolean allowCustom, boolean allowNone, Consumer onPick) { super(px, py, 1, 1); this.selectedColor = preselectedColor; int lines = (int)Math.ceil((double)sampleColors.length / (double)maxColorsPerLine); - set_height(18 * 2 + lines * 13 - 1); + set_height(18 * ((allowCustom ? 1 : 0) + (allowNone ? 1 : 0)) + lines * 13 - 1); set_width(maxColorsPerLine * 13 - 1); - addRenderableWidget(new ColorBrowserButton(x(), y(), width(), () -> selectedColor, (btn) -> { - DLScreen.setScreen(new DLColorPickerScreen(parent, selectedColor, c -> { - selectedColor = c.toInt(); - onPick.accept(this); - }, true)); - })); + int currentY = 0; + if (allowCustom) { + addRenderableWidget(new ColorBrowserButton(x(), y() + currentY, width(), () -> selectedColor, (btn) -> { + DLScreen.setScreen(new DLColorPickerScreen(parent, selectedColor, c -> { + selectedColor = c.toInt(); + onPick.accept(this); + }, true)); + })); + currentY += 18; + } for (int i = 0, y = 0; y < lines && i < sampleColors.length; y++) { for (int x = 0; x < maxColorsPerLine && i < sampleColors.length; x++, i++) { final int j = i; - addRenderableWidget(new ColorButton(x() + x * 13, y() + 18 + y * 13, sampleColors[j], btn -> { + addRenderableWidget(new ColorButton(x() + x * 13, y() + currentY + y * 13, sampleColors[j], btn -> { selectedColor = sampleColors[j]; onPick.accept(this); })); } } - addRenderableWidget(new NoColorButton(x(), y() + height() - 16, width(), (btn) -> { - selectedColor = 0; - onPick.accept(this); - })); + if (allowNone) { + addRenderableWidget(new NoColorButton(x(), y() + height() - 16, width(), (btn) -> { + selectedColor = 0; + onPick.accept(this); + })); + } } public int getSelectedColor() { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorSlotWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorSlotWidget.java new file mode 100644 index 00000000..94ccce1b --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/ColorSlotWidget.java @@ -0,0 +1,83 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutColorPicker; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; +import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.ButtonState; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.network.chat.Component; + +public class ColorSlotWidget extends DLButton { + + public static final int SLOT_SIZE = 18; + + private final DLScreen parent; + private final List tooltip; + private final Supplier getScrollOffset; + private final Consumer onColorSelected; + private final int[] defaultColors; + private final boolean allowCustom; + private final boolean allowNone; + private int selectedColor; + + public ColorSlotWidget(DLScreen parent, int x, int y, int color, int[] defaultColors, boolean allowCustom, boolean allowNone, List tooltip, Supplier getScrollOffset, Consumer onColorSelected) { + super(x, y, SLOT_SIZE, SLOT_SIZE, TextUtils.empty()); + this.parent = parent; + this.getScrollOffset = getScrollOffset; + this.defaultColors = defaultColors; + this.allowCustom = allowCustom; + this.allowNone = allowNone; + this.onColorSelected = onColorSelected; + this.tooltip = tooltip; + this.selectedColor = color; + } + + @Override + public void onClick(double mouseX, double mouseY) { + FlyoutColorPicker flyout = new FlyoutColorPicker<>(parent, selectedColor, defaultColors, 8, allowCustom, allowNone, + (w) -> { + renderTooltip = false; + parent.addRenderableWidget(w); + }, + (w) -> { + renderTooltip = true; + this.selectedColor = ((FlyoutColorPicker)w).getColorPicker().getSelectedColor(); + DLUtils.doIfNotNull(onColorSelected, x -> x.accept(this)); + parent.removeWidget(w); + }); + flyout.setYOffset((int)-getScrollOffset.get()); + flyout.open(this); + } + + public int getSelectedColor() { + return selectedColor; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTick) { + DynamicGuiRenderer.renderArea(graphics, x(), y(), width(), height(), AreaStyle.GRAY, ButtonState.DOWN); + GuiUtils.fill(graphics, x() + 1, y() + 1, width() - 2, height() - 2, selectedColor); + if (isMouseSelected()) { + GuiUtils.fill(graphics, x() + 1, y() + 1, width() - 2, height() - 2, 0x40FFFFFF); + } + } + + private boolean renderTooltip = true; + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + if (!isMouseSelected() || !renderTooltip) { + return; + } + + GuiUtils.renderTooltip(parent, this, tooltip, parent.width() / 3, graphics, mouseX, mouseY); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateScrollInput.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateScrollInput.java new file mode 100644 index 00000000..6aec9276 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateScrollInput.java @@ -0,0 +1,168 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.foundation.gui.widget.ScrollInput; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenu; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + +public class DLCreateScrollInput extends ScrollInput implements IDragonLibWidget { + + private boolean mouseSelected; + protected boolean renderArrow; + protected Function> onTooltip; + + protected final Cache> cachedTooltip = new Cache<>(() -> { + List tooltips = new ArrayList<>(getToolTip()); + DLUtils.doIfNotNull(onTooltip, x -> tooltips.addAll(onTooltip.apply(this))); + return tooltips; + }); + + protected final DLScreen parent; + + public DLCreateScrollInput(DLScreen parent, int x, int y, int width, int height) { + super(x, y, width, height); + this.parent = parent;; + } + + public DLCreateScrollInput setRenderArrow(boolean b) { + this.renderArrow = b; + return this; + } + + public DLCreateScrollInput onRenderTooltip(Function> action) { + this.onTooltip = action; + cachedTooltip.clear(); + return this; + } + + public boolean shouldRenderArrow() { + return renderArrow; + } + + public Component getText() { + return formatter.apply(state); + } + + @Override + protected void updateTooltip() { + super.updateTooltip(); + cachedTooltip.clear(); + } + + @Override + public final void render(PoseStack ms, int mouseX, int mouseY, float partialTicks) { + renderMainLayer(new Graphics(ms), mouseX, mouseY, partialTicks); + } + + @SuppressWarnings("resource") + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + CreateDynamicWidgets.renderTextBox(graphics, x(), y(), width()); + if (shouldRenderArrow()) CreateDynamicWidgets.renderTextBoxArrow(graphics, x(), y()); + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x() + 5, y() + 5, getText(), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, true); + + } + + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + if (!isMouseSelected()) { + return; + } + GuiUtils.renderTooltip(parent, this, cachedTooltip.get(), parent.width() / 3, graphics, mouseX, mouseY); + } + + @Override + public void onFocusChangeEvent(boolean focus) {} + + @Override + public DLContextMenu getContextMenu() { + return null; + } + + @Override + public void setMenu(DLContextMenu menu) {} + + @Override + public boolean isMouseSelected() { + return mouseSelected; + } + + @Override + public void setMouseSelected(boolean selected) { + this.mouseSelected = selected; + } + + @Override + public int x() { + return x; + } + + @Override + public int y() { + return y; + } + + @Override + public void set_x(int x) { + this.x = x; + } + + @Override + public void set_y(int y) { + this.y = y; + } + + @Override + public void set_width(int w) { + this.width = w; + } + + @Override + public void set_height(int h) { + this.height = h; + } + + @Override + public void set_visible(boolean b) { + this.visible = b; + } + + @Override + public boolean visible() { + return visible; + } + + @Override + public void set_active(boolean b) { + this.active = b; + } + + @Override + public boolean active() { + return super.isActive(); + } + + @Override + public int width() { + return width; + } + + @Override + public int height() { + return height; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java index a9fd0c27..932821a1 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateSelectionScrollInput.java @@ -1,16 +1,113 @@ package de.mrjulsen.crn.client.gui.widgets; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.foundation.gui.widget.SelectionScrollInput; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget.FlyoutPointer; +import de.mrjulsen.crn.client.gui.widgets.flyouts.FlyoutScrollInput; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLContextMenu; import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; public class DLCreateSelectionScrollInput extends SelectionScrollInput implements IDragonLibWidget { private boolean mouseSelected; + protected boolean renderArrow; + protected Function> onTooltip; + + protected final Cache> cachedTooltip = new Cache<>(() -> { + List tooltips = new ArrayList<>(getToolTip()); + tooltips.add(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".common.click_to_search").withStyle(ChatFormatting.DARK_GRAY).withStyle(ChatFormatting.ITALIC)); + DLUtils.doIfNotNull(onTooltip, x -> tooltips.addAll(onTooltip.apply(this))); + return tooltips; + }); + + protected final DLScreen parent; + + public DLCreateSelectionScrollInput(DLScreen parent, int x, int y, int width, int height) { + super(x, y, width, height); + this.parent = parent;; + } + + public DLCreateSelectionScrollInput setRenderArrow(boolean b) { + this.renderArrow = b; + return this; + } - public DLCreateSelectionScrollInput(int xIn, int yIn, int widthIn, int heightIn) { - super(xIn, yIn, widthIn, heightIn); + public DLCreateSelectionScrollInput onRenderTooltip(Function> action) { + this.onTooltip = action; + cachedTooltip.clear(); + return this; + } + + public boolean shouldRenderArrow() { + return renderArrow; + } + + public Component getText() { + return formatter.apply(state); + } + + @Override + protected void updateTooltip() { + super.updateTooltip(); + cachedTooltip.clear(); + } + + public List getOptions() { + return options.stream().map(x -> (Component)x).toList(); + } + + @Override + public final void render(PoseStack ms, int mouseX, int mouseY, float partialTicks) { + renderMainLayer(new Graphics(ms), mouseX, mouseY, partialTicks); + } + + @SuppressWarnings("resource") + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + CreateDynamicWidgets.renderTextBox(graphics, x(), y(), width()); + if (shouldRenderArrow()) CreateDynamicWidgets.renderTextBoxArrow(graphics, x(), y()); + GuiUtils.drawString(graphics, Minecraft.getInstance().font, x() + 5, y() + 5, getText(), DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, true); + + } + + boolean renderTooltip = true; + @Override + public void renderFrontLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + if (!isMouseSelected() || !renderTooltip) { + return; + } + GuiUtils.renderTooltip(parent, this, cachedTooltip.get(), parent.width() / 3, graphics, mouseX, mouseY); + } + + @Override + public void onClick(double mouseX, double mouseY) { + new FlyoutScrollInput<>(parent, FlyoutPointer.UP, ColorShade.DARK, (w) -> { + renderTooltip = false; + parent.addRenderableWidget(w); + }, (w) -> { + renderTooltip = true; + parent.removeWidget(w); + }, this).open(this); + super.onClick(mouseX, mouseY); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateTextBox.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateTextBox.java new file mode 100644 index 00000000..25c29614 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLCreateTextBox.java @@ -0,0 +1,36 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; + +public class DLCreateTextBox extends DLEditBox { + + protected boolean renderArrow; + + public DLCreateTextBox(Font pFont, int pX, int pY, int pWidth, Component pMessage) { + super(pFont, pX + 5, pY + 5, pWidth - 10, 13, pMessage); + this.setBordered(false); + } + + public DLCreateTextBox setRenderArrow(boolean b) { + this.renderArrow = b; + return this; + } + + public boolean shouldRenderArrow() { + return renderArrow; + } + + @Override + public void renderButton(PoseStack ms, int mouseX, int mouseY, float partialTicks) { + Graphics graphics = new Graphics(ms); + CreateDynamicWidgets.renderTextBox(graphics, x() - 5, y() - 5, width() + 10); + if (shouldRenderArrow()) CreateDynamicWidgets.renderTextBoxArrow(graphics, x() - 5, y() - 5); + super.renderButton(ms, mouseX, mouseY, partialTicks); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLLabel.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLLabel.java new file mode 100644 index 00000000..3b1579b5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/DLLabel.java @@ -0,0 +1,116 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import de.mrjulsen.mcdragonlib.core.EAlignment; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; + +public class DLLabel extends DLRenderable { + + protected final Font font = Minecraft.getInstance().font; + protected boolean autoWidth; + protected boolean autoHeight; + private Component text = TextUtils.empty(); + private int tint = 0xFFFFFFFF; + private EAlignment alignment = EAlignment.CENTER; + private boolean drawShadow = true; + + + public DLLabel(int x, int y, int width, int height, Component text) { + super(x, y, width, height); + this.text = text; + } + + public DLLabel setAutoSize(boolean b) { + this.autoWidth = b; + this.autoHeight = b; + return this; + } + + public DLLabel setAutoWidth(boolean b) { + this.autoWidth = b; + return this; + } + + public DLLabel setAutoHeight(boolean b) { + this.autoHeight = b; + return this; + } + + public DLLabel setText(Component text) { + this.text = text; + updateSize(); + return this; + } + + public DLLabel setTint(int tint) { + this.tint = tint; + updateSize(); + return this; + } + + public DLLabel setAlignment(EAlignment alignment) { + this.alignment = alignment; + updateSize(); + return this; + } + + public DLLabel setDrawShadow(boolean b) { + this.drawShadow = b; + return this; + } + + public boolean isAutoWidth() { + return autoWidth; + } + + public boolean isAutoHeight() { + return autoHeight; + } + + public boolean isAutoSize() { + return isAutoWidth() || isAutoHeight(); + } + + public Component getText() { + return text; + } + + public int getTint() { + return tint; + } + + public EAlignment getAlignment() { + return alignment; + } + + public boolean isDrawShadow() { + return drawShadow; + } + + protected void updateSize() { + if (this.autoWidth) this.set_width(font.width(text)); + if (this.autoHeight) this.set_height(font.lineHeight); + } + + private final Cache textCache = new Cache<>(() -> { + final boolean tooWide = font.width(getText()) > width(); + return tooWide ? TextUtils.text(font.substrByWidth(getText(), width()).getString() + "...") : getText(); + }); + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + EAlignment align = getAlignment(); + int x = x() + switch (align) { + case RIGHT -> width(); + case CENTER -> width() / 2; + default -> 0; + }; + GuiUtils.drawString(graphics, font, x, y() + height() / 2 - font.lineHeight / 2, textCache.get(), getTint(), getAlignment(), isDrawShadow()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IconSlotWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IconSlotWidget.java new file mode 100644 index 00000000..c05b94b9 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/IconSlotWidget.java @@ -0,0 +1,31 @@ +package de.mrjulsen.crn.client.gui.widgets; + +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import de.mrjulsen.mcdragonlib.client.render.Sprite; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.util.DLUtils; + +public class IconSlotWidget extends DLRenderable { + + private Sprite icon; + + public IconSlotWidget(int x, int y, Sprite icon) { + super(x, y, 18, 18); + this.icon = icon; + } + + public void setIcon(Sprite icon) { + this.icon = icon; + } + + public Sprite getIcon() { + return icon; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + CreateDynamicWidgets.renderIconSlot(graphics, x(), y(), width(), height()); + DLUtils.doIfNotNull(icon, x -> x.render(graphics, x() + (width() / 2 - icon.getWidth() / 2), y() + (height() / 2 - icon.getHeight() / 2))); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java index 0bef955b..787b0139 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteDetailsViewer.java @@ -15,13 +15,13 @@ import de.mrjulsen.crn.data.navigation.RoutePart; import de.mrjulsen.crn.data.navigation.TransferConnection; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class RouteDetailsViewer extends ScrollableWidgetContainer { +public class RouteDetailsViewer extends DLScrollableWidgetContainer { private final DLAbstractScrollBar scrollBar; private int contentHeight = 0; @@ -40,7 +40,7 @@ public RouteDetailsViewer(Screen parent, int x, int y, int width, int height, DL scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -59,6 +59,7 @@ public void displayRouteInternal(ClientRoute route, List parts, Queue connections = new ConcurrentLinkedQueue<>(route.getConnections()); for (int i = 0; i < parts.size(); i++) { ClientRoutePart part = parts.get(i); + RoutePartWidget widget = new RoutePartWidget(parent, x(), y() + contentHeight, width(), route, part); widget.setShowTrainDetails(showTrainDetails); widget.setCanExpandCollapse(canExpandCollapse); @@ -78,7 +79,7 @@ public void displayRouteInternal(ClientRoute route, List parts, } contentHeight += 10; - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java index 0c13edad..3eb522d5 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteViewer.java @@ -4,13 +4,13 @@ import de.mrjulsen.crn.data.navigation.ClientRoute; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class RouteViewer extends ScrollableWidgetContainer { +public class RouteViewer extends DLScrollableWidgetContainer { private final Screen parent; private final DLAbstractScrollBar scrollBar; @@ -23,7 +23,7 @@ public RouteViewer(Screen parent, int x, int y, int width, int height, DLAbstrac scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -41,7 +41,7 @@ public void displayRoutes(List routes) { contentHeight += (RouteWidget.HEIGHT + 3); } contentHeight += 2; - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java index 8ba80431..c4da9541 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/RouteWidget.java @@ -9,7 +9,7 @@ import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; import de.mrjulsen.crn.client.gui.screen.RouteDetailsScreen; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.data.SavedRoutesManager; import de.mrjulsen.crn.data.navigation.ClientRoute; @@ -41,9 +41,9 @@ public class RouteWidget extends DLButton { private final ClientRoute route; - private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); - private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); - private final MutableComponent trainCanceled = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); + private final MutableComponent transferText = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); + private final MutableComponent connectionInPast = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); + private final MutableComponent trainCanceled = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); private final MutableComponent textShowDetails = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.show_details"); private final MutableComponent textSave = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.save"); private final MutableComponent textShare = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.share"); @@ -84,8 +84,8 @@ public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float p ImmutableList parts = route.getParts(); Font shadowlessFont = new NoShadowFontWrapper(minecraft.font); - String timeStart = TimeUtils.parseTime((int)((route.getStart().getScheduledDepartureTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); - String timeEnd = TimeUtils.parseTime((int)((route.getEnd().getScheduledArrivalTime() + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY), ModClientConfig.TIME_FORMAT.get()); + String timeStart = TimeUtils.parseTime((int)((route.getStart().getScheduledDepartureTime() + DragonLib.daytimeShift()) % DragonLib.ticksPerDay()), ModClientConfig.TIME_FORMAT.get()); + String timeEnd = TimeUtils.parseTime((int)((route.getEnd().getScheduledArrivalTime() + DragonLib.daytimeShift()) % DragonLib.ticksPerDay()), ModClientConfig.TIME_FORMAT.get()); String dash = " - "; MutableComponent summary = TextUtils.text(String.format("%s%s%s | %s %s | %s", timeStart, @@ -128,10 +128,10 @@ public void renderMainLayer(Graphics graphics, int pMouseX, int pMouseY, float p GuiUtils.drawString(graphics, font, (int)((x + 6) / scale), (int)((y + 43) / scale), TextUtils.text(route.getStart().getClientTag().tagName()), 0xDBDBDB, EAlignment.LEFT, false); GuiUtils.drawString(graphics, font, (int)((x + WIDTH - 6) / scale) - textW, (int)((y + 43) / scale), TextUtils.text(endStationName), 0xDBDBDB, EAlignment.LEFT, false); if (route.getStart().shouldRenderRealTime()) { - GuiUtils.drawString(graphics, font, (int)((x + 6 + font.width(timeStart) * localScale / 2.0f) / scale) - font.width(timeStart) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)((route.getStart().getScheduledDepartureTime() + (route.getStart().getDepartureTimeDeviation() / precision * precision)) % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), route.getStart().isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, (int)((x + 6 + font.width(timeStart) * localScale / 2.0f) / scale) - font.width(timeStart) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)((route.getStart().getScheduledDepartureTime() + (route.getStart().getDepartureTimeDeviation() / precision * precision)) % 24000 + DragonLib.daytimeShift()), ModClientConfig.TIME_FORMAT.get())), route.getStart().isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); } if (route.getEnd().shouldRenderRealTime()) { - GuiUtils.drawString(graphics, font, (int)((x + 6 + font.width(timeEnd) * localScale * 1.5f + (font.width(dash)) * localScale) / scale) - font.width(timeEnd) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)((route.getEnd().getScheduledArrivalTime() + (route.getEnd().getArrivalTimeDeviation() / precision * precision)) % 24000 + DragonLib.DAYTIME_SHIFT), ModClientConfig.TIME_FORMAT.get())), route.getEnd().isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, (int)((x + 6 + font.width(timeEnd) * localScale * 1.5f + (font.width(dash)) * localScale) / scale) - font.width(timeEnd) / 2, (int)((y + 15) / scale), TextUtils.text(TimeUtils.parseTime((int)((route.getEnd().getScheduledArrivalTime() + (route.getEnd().getArrivalTimeDeviation() / precision * precision)) % 24000 + DragonLib.daytimeShift()), ModClientConfig.TIME_FORMAT.get())), route.getEnd().isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); } if (route.isAnyCancelled()) { diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java index 55618ca5..194584b4 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRouteWidget.java @@ -5,7 +5,7 @@ import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; import de.mrjulsen.crn.client.gui.screen.RouteDetailsScreen; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.ISaveableNavigatorData; import de.mrjulsen.crn.data.SavedRoutesManager; import de.mrjulsen.crn.data.ISaveableNavigatorData.SaveableNavigatorDataLine; @@ -37,9 +37,9 @@ public class SavedRouteWidget extends DLButton { private final ISaveableNavigatorData data; - private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); - private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); - private final MutableComponent trainCanceled = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); + private final MutableComponent transferText = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.transfer"); + private final MutableComponent connectionInPast = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); + private final MutableComponent trainCanceled = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); private final MutableComponent textShowDetails = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_route_widget.show_details"); private final MutableComponent textRemove = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_widget.remove"); private final MutableComponent textShare = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_route_widget.share"); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java index 7c20b4f3..fd023c77 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/SavedRoutesViewer.java @@ -9,7 +9,7 @@ import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.core.EAlignment; @@ -20,7 +20,7 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; -public class SavedRoutesViewer extends ScrollableWidgetContainer { +public class SavedRoutesViewer extends DLScrollableWidgetContainer { private final Screen parent; private final DLAbstractScrollBar scrollBar; @@ -35,7 +35,7 @@ public SavedRoutesViewer(Screen parent, int x, int y, int width, int height, DLA scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -67,7 +67,7 @@ public void displayRoutes(List data) { if (lastData == null || lastData.dayOrderValue() != d.dayOrderValue()) { Component text; long worldTime = DragonLib.getCurrentWorldTime(); - long dayDiff = d.dayOrderValue() - (worldTime + DragonLib.DAYTIME_SHIFT) / DragonLib.TICKS_PER_DAY; + long dayDiff = d.dayOrderValue() - (worldTime + DragonLib.daytimeShift()) / DragonLib.ticksPerDay(); if (d.timeOrderValue() < worldTime) text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.in_the_past"); else if (dayDiff == 0) text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.today"); else if (dayDiff == 1) text = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".saved_routes.tomorrow"); @@ -83,7 +83,7 @@ public void displayRoutes(List data) { } contentHeight += 10; - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java index 14995f92..22febe15 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesViewer.java @@ -6,7 +6,7 @@ import de.mrjulsen.crn.registry.ModAccessorTypes.DepartureRoutesData; import de.mrjulsen.crn.data.navigation.ClientRoute; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.core.EAlignment; @@ -17,7 +17,7 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class StationDeparturesViewer extends ScrollableWidgetContainer { +public class StationDeparturesViewer extends DLScrollableWidgetContainer { private final Screen parent; private final DLAbstractScrollBar scrollBar; @@ -30,7 +30,7 @@ public StationDeparturesViewer(Screen parent, int x, int y, int width, int heigh scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -55,7 +55,7 @@ public void displayRoutes(String stationTagName, UserSettings settings) { contentHeight += (widget.height() + 3); } contentHeight += 7; - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); }); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java index 2f3317cc..5cf74d01 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/StationDeparturesWidget.java @@ -6,8 +6,8 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; -import de.mrjulsen.crn.client.gui.screen.TrainJourneySreen; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.gui.screen.TrainJourneyScreen; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.crn.data.navigation.ClientRoute; import de.mrjulsen.mcdragonlib.client.gui.DLScreen; @@ -34,15 +34,15 @@ public class StationDeparturesWidget extends DLButton implements AutoCloseable { public static final float DEFAULT_SCALE = 0.75f; - private final MutableComponent connectionInPast = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); - private final MutableComponent trainCanceled = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); + private final MutableComponent connectionInPast = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".navigator.route_entry.connection_in_past"); + private final MutableComponent trainCanceled = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.stop_cancelled"); private final ClientRoute route; private final boolean arrival; public StationDeparturesWidget(Screen parent, StationDeparturesViewer viewer, int x, int y, int width, ClientRoute route, boolean arrival) { super(x, y, width, 32, TextUtils.empty(), (b) -> { - DLScreen.setScreen(new TrainJourneySreen(parent, route, route.getStart().getTrainId())); + DLScreen.setScreen(new TrainJourneyScreen(parent, route, route.getStart().getTrainId())); }); this.route = route; this.arrival = arrival; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java index 94c138e0..4e605122 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/TrainDebugViewer.java @@ -4,7 +4,7 @@ import de.mrjulsen.crn.registry.ModAccessorTypes; import de.mrjulsen.crn.debug.TrainDebugData; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.core.EAlignment; @@ -13,7 +13,7 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class TrainDebugViewer extends ScrollableWidgetContainer { +public class TrainDebugViewer extends DLScrollableWidgetContainer { private final Screen parent; private final DLAbstractScrollBar scrollBar; @@ -26,7 +26,7 @@ public TrainDebugViewer(Screen parent, int x, int y, int width, int height, DLAb scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -35,11 +35,6 @@ public Screen getParent() { return parent; } - @Override - protected void clearWidgets() { - super.clearWidgets(); - } - public void reload() { clearWidgets(); contentHeight = 0; @@ -52,7 +47,7 @@ public void reload() { contentHeight += (widget.height() + 3); } contentHeight += 7; - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); }); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java index 8e30a997..5d79b50d 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/create/CreateTimeSelectionWidget.java @@ -5,7 +5,7 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.core.EAlignment; @@ -16,7 +16,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; -public class CreateTimeSelectionWidget extends WidgetContainer { +public class CreateTimeSelectionWidget extends DLWidgetContainer { public static final int WIDHT = 66; public static final int HEIGHT = 18; @@ -33,7 +33,7 @@ public CreateTimeSelectionWidget(int x, int y, int max) { transferTimeInput = addRenderableWidget(new ScrollInput(x() + 3, y(), width() - 6, height()) .withRange(0, max) - .withStepFunction(a -> a.shift ? 1000 : 500) + .withStepFunction(a -> a.shift ? (int)DragonLib.ticksPerIngameHour() : (int)(DragonLib.ticksPerIngameHour() / 2)) .titled(transferTimeBoxText.copy()) .calling((i) -> { if (transferTimeInput == null) return; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java index 4a1fe2a0..93f3fee3 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutAdvancedSearchsettingsWidget.java @@ -55,9 +55,4 @@ public void renderFlyoutContent(Graphics graphics, int mouseX, int mouseY, float super.renderFlyoutContent(graphics, mouseX, mouseY, partialTicks, contentArea); GuiUtils.drawString(graphics, font, contentArea.getX() + 8, contentArea.getY() + 8, textTrainGroups, DragonLib.NATIVE_BUTTON_FONT_COLOR_ACTIVE, EAlignment.LEFT, false); } - - @Override - public void close() { - super.close(); - } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java index a544c841..dc9f6257 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutColorPicker.java @@ -2,7 +2,6 @@ import java.util.function.Consumer; -import de.mrjulsen.crn.Constants; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; import de.mrjulsen.crn.client.gui.widgets.ColorPickerWidget; @@ -15,9 +14,9 @@ public class FlyoutColorPicker addRenderableWidgetFunc, Consumer removeWidgetFunc) { + public FlyoutColorPicker(DLScreen screen, int initialColor, int[] defaultColors, int colorsPerLine, boolean allowCustom, boolean allowNone, Consumer addRenderableWidgetFunc, Consumer removeWidgetFunc) { super(screen, 100, 50, FlyoutPointer.RIGHT, ColorShade.LIGHT, addRenderableWidgetFunc, removeWidgetFunc); - colorPicker = addRenderableWidget(new ColorPickerWidget(screen, x() + 10, y() + 10, Constants.DEFAULT_TRAIN_TYPE_COLORS, 5, initialColor, (picker) -> { + colorPicker = addRenderableWidget(new ColorPickerWidget(screen, x() + 10, y() + 10, defaultColors, colorsPerLine, initialColor, allowCustom, allowNone, (picker) -> { closeImmediately(); })); set_width(colorPicker.width() + 20); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java index efb50ddc..42c81832 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutDepartureInWidget.java @@ -42,7 +42,7 @@ public FlyoutDepartureInWidget(DLScreen screen, FlyoutPointer pointer, ColorShad this.settings = settings; this.getUserSetting = getUserSetting; - this.timeSelection = addRenderableWidget(new CreateTimeSelectionWidget(getContentArea().getX() + 8, getContentArea().getY() + 24, DragonLib.TICKS_PER_DAY * 10)); + this.timeSelection = addRenderableWidget(new CreateTimeSelectionWidget(getContentArea().getX() + 8, getContentArea().getY() + 24, (int)Math.min(DragonLib.ticksPerDay() * 10, Integer.MAX_VALUE))); DLIconButton resetBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), getContentArea().getX() + getContentArea().getWidth() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, getContentArea().getY() + 2, TextUtils.empty(), (b) -> { getUserSetting.get().setToDefault(); timeSelection.setValue(getUserSetting.get().getValue()); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutScrollInput.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutScrollInput.java new file mode 100644 index 00000000..b537ecaa --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutScrollInput.java @@ -0,0 +1,72 @@ +package de.mrjulsen.crn.client.gui.widgets.flyouts; + +import java.util.Locale; +import java.util.function.Consumer; +import de.mrjulsen.crn.client.gui.CreateDynamicWidgets.ColorShade; +import de.mrjulsen.crn.client.gui.widgets.AbstractFlyoutWidget; +import de.mrjulsen.crn.client.gui.widgets.CRNListBox; +import de.mrjulsen.crn.client.gui.widgets.DLCreateSelectionScrollInput; +import de.mrjulsen.crn.client.gui.widgets.FlatCheckBox; +import de.mrjulsen.crn.client.gui.widgets.ModernVerticalScrollBar; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; +import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; +import de.mrjulsen.mcdragonlib.client.util.GuiAreaDefinition; +import de.mrjulsen.mcdragonlib.util.DLUtils; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.minecraft.client.gui.components.Widget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.Component; + +public class FlyoutScrollInput extends AbstractFlyoutWidget { + private final DLEditBox searchBox; + private final CRNListBox content; + private final DLCreateSelectionScrollInput input; + + public FlyoutScrollInput(DLScreen screen, FlyoutPointer pointer, ColorShade pointerShade, Consumer addRenderableWidgetFunc, Consumer removeWidgetFunc, DLCreateSelectionScrollInput input) { + super(screen, 1, 120, pointer, pointerShade, addRenderableWidgetFunc, removeWidgetFunc); + this.input = input; + set_width(input.width() + 10); + + searchBox = addRenderableWidget(new DLEditBox(font, getContentArea().getX() + 3, getContentArea().getY() + 3, getContentArea().getWidth() - 6, 16, TextUtils.empty())); + searchBox.withHint(DragonLib.TEXT_SEARCH); + searchBox.setResponder((text) -> reload(null)); + + final int searchBoxHeight = searchBox.x() + searchBox.height() - 1; + + ModernVerticalScrollBar scrollBar = new ModernVerticalScrollBar(screen, getContentArea().getX() + getContentArea().getWidth() - 7, searchBoxHeight + 2, getContentArea().getHeight() - searchBoxHeight + 1, GuiAreaDefinition.of(screen)); + this.content = addRenderableWidget(new CRNListBox<>(screen, getContentArea().getX() + 2, searchBoxHeight + 2, getContentArea().getWidth() - 4, getContentArea().getHeight() - searchBoxHeight + 1, scrollBar)); + + addRenderableWidget(scrollBar); + } + + @Override + public void open(IDragonLibWidget parent) { + reload(() -> super.open(parent)); + } + + private void reload(Runnable andThen) { + content.displayData(input.getOptions(), (c, i) -> { + if (!c.getString().toLowerCase(Locale.ROOT).contains(searchBox.getValue().toLowerCase(Locale.ROOT))) { + return null; + } + + return new FlatCheckBox(0, 0, 0, c.getString(), input.getState() == i, + (b) -> { + content.getEntries().forEach(x -> { + boolean checked = x.getKey() == b; + x.getKey().setChecked(checked, false); + }); + if (b.isChecked()) { + input.setState(i); + input.onChanged(); + closeImmediately(); + } + } + ); + }); + DLUtils.doIfNotNull(andThen, x -> x.run()); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java index b7ca3265..e7d12cb8 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTrainGroupsWidget.java @@ -78,7 +78,7 @@ public void open(IDragonLibWidget parent) { private void reload(Runnable andThen) { DataAccessor.getFromServer(null, ModAccessorTypes.GET_ALL_TRAIN_GROUPS, (groups) -> { - trainGroups.displayData(new ArrayList<>(groups.stream().sorted((a, b) -> a.getGroupName().compareToIgnoreCase(b.getGroupName())).toList()), (group) -> { + trainGroups.displayData(new ArrayList<>(groups.stream().sorted((a, b) -> a.getGroupName().compareToIgnoreCase(b.getGroupName())).toList()), (group, i) -> { FlatCheckBox cb = new FlatCheckBox(0, 0, 0, group.getGroupName(), getUserSetting.get().getValue().stream().noneMatch(x -> x.equals(group.getGroupName())), (b) -> {}); return cb; }); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java index 01f6eb44..b35e1a85 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/flyouts/FlyoutTransferTimeWidget.java @@ -42,7 +42,7 @@ public FlyoutTransferTimeWidget(DLScreen screen, FlyoutPointer pointer, ColorSha this.settings = settings; this.getUserSetting = getUserSetting; - this.timeSelection = addRenderableWidget(new CreateTimeSelectionWidget(getContentArea().getX() + 8, getContentArea().getY() + 24, DragonLib.TICKS_PER_DAY)); + this.timeSelection = addRenderableWidget(new CreateTimeSelectionWidget(getContentArea().getX() + 8, getContentArea().getY() + 24, (int)DragonLib.ticksPerDay())); DLIconButton resetBtn = addRenderableWidget(new DLIconButton(ButtonType.DEFAULT, AreaStyle.FLAT, ModGuiIcons.REFRESH.getAsSprite(16, 16), getContentArea().getX() + getContentArea().getWidth() - DLIconButton.DEFAULT_BUTTON_WIDTH - 2, getContentArea().getY() + 2, TextUtils.empty(), (b) -> { getUserSetting.get().setToDefault(); timeSelection.setValue(getUserSetting.get().getValue()); diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/GuiBuilderContext.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/GuiBuilderContext.java new file mode 100644 index 00000000..c2e451bd --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/GuiBuilderContext.java @@ -0,0 +1,19 @@ +package de.mrjulsen.crn.client.gui.widgets.modular; + +public class GuiBuilderContext { + private final ModularWidgetBuilder builder; + private final ModularWidgetContainer container; + + public GuiBuilderContext(ModularWidgetBuilder builder, ModularWidgetContainer container) { + this.builder = builder; + this.container = container; + } + + public ModularWidgetBuilder builder() { + return builder; + } + + public ModularWidgetContainer container() { + return container; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetBuilder.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetBuilder.java new file mode 100644 index 00000000..e639290e --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetBuilder.java @@ -0,0 +1,63 @@ +package de.mrjulsen.crn.client.gui.widgets.modular; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +@Environment(EnvType.CLIENT) +public class ModularWidgetBuilder { + + private final ModularWidgetContainer container; + private final Map> lineBuilders = new LinkedHashMap<>(); + + public ModularWidgetBuilder(ModularWidgetContainer container) { + this.container = container; + } + + public void addLine(String name, Consumer lineBuilder) { + if (lineBuilders.containsKey(name)) { + return; + } + lineBuilders.put(name, lineBuilder); + } + + public int getCurrentLinesCount() { + return lineBuilders.size(); + } + + public boolean hasLine(String name) { + return lineBuilders.containsKey(name); + } + + public Consumer getLine(String name) { + return lineBuilders.get(name); + } + + public void addToLine(String name, Consumer additional) { + lineBuilders.replace(name, lineBuilders.get(name).andThen(additional)); + } + + public void build() { + int currentY = 0; + for (Consumer c : lineBuilders.values()) { + ModularWidgetLine line = new ModularWidgetLine(0, 0, container.width()); + line.set_width(container.width() - container.getPaddingLeft() - container.getPaddingRight()); + c.accept(line); + currentY += container.addLine(line, currentY); + } + + DLAbstractScrollBar scrollBar = container.getScrollbar(); + scrollBar.set_x(container.x() + container.width() - scrollBar.width()); + scrollBar.set_y(container.y()); + scrollBar.set_height(container.height()); + scrollBar.setAutoScrollerSize(true); + scrollBar.setScreenSize(container.height()); + scrollBar.setMaxScroll(currentY + container.getPaddingBottom()); + scrollBar.withOnValueChanged((sb) -> container.setYScrollOffset(sb.getScrollValue())); + scrollBar.setStepSize(10); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetContainer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetContainer.java new file mode 100644 index 00000000..579f0393 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetContainer.java @@ -0,0 +1,112 @@ +package de.mrjulsen.crn.client.gui.widgets.modular; + +import java.util.function.BiConsumer; + +import de.mrjulsen.mcdragonlib.client.gui.DLScreen; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.util.Graphics; +import de.mrjulsen.mcdragonlib.client.util.GuiUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.narration.NarrationElementOutput; + +@Environment(EnvType.CLIENT) +public class ModularWidgetContainer extends DLScrollableWidgetContainer { + + public static final int DEFAULT_PADDING = 10; + + private final DLScreen screen; + private final DLAbstractScrollBar scrollBar; + private final BiConsumer builder; + + private int paddingLeft; + private int paddingRight; + private int paddingTop; + private int paddingBottom; + + public ModularWidgetContainer(DLScreen screen, int x, int y, int width, int height, BiConsumer builder, DLAbstractScrollBar scrollBar) { + this(screen, x, y, width, height, builder, scrollBar, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING); + } + + public ModularWidgetContainer(DLScreen screen, int x, int y, int width, int height, BiConsumer builder, DLAbstractScrollBar scrollBar, int paddingLeft, int paddingRight, int paddingTop, int paddingBottom) { + super(x, y, width, height); + this.screen = screen; + this.scrollBar = scrollBar; + this.builder = builder; + this.paddingLeft = paddingLeft; + this.paddingRight = paddingRight; + this.paddingTop = paddingTop; + this.paddingBottom = paddingBottom; + build(); + } + + public void build() { + clearWidgets(); + ModularWidgetBuilder mb = new ModularWidgetBuilder(this); + builder.accept(this, mb); + mb.build(); + } + + int addLine(ModularWidgetLine line, int yOffset) { + line.set_x(x() + paddingLeft); + line.set_y(y() + paddingTop + yOffset); + line.set_width(width() - paddingLeft - paddingRight); + addRenderableWidget(line); + return line.height(); + } + + public DLScreen getParentScreen() { + return screen; + } + + public DLAbstractScrollBar getScrollbar() { + return scrollBar; + } + + public int getPaddingLeft() { + return paddingLeft; + } + + public int getPaddingRight() { + return paddingRight; + } + + public int getPaddingTop() { + return paddingTop; + } + + public int getPaddingBottom() { + return paddingBottom; + } + + @Override + public void renderMainLayer(Graphics graphics, int mouseX, int mouseY, float partialTicks) { + super.renderMainLayer(graphics, mouseX, mouseY, partialTicks); + if (scrollBar.getScrollValue() > 0) { + GuiUtils.fillGradient(graphics, x(), y(), 0, width(), 10, 0x77000000, 0x00000000); + } + if (scrollBar.getScrollValue() < scrollBar.getMaxScroll()) { + GuiUtils.fillGradient(graphics, x(), y() + height() - 10, 0, width(), 10, 0x00000000, 0x77000000); + } + } + + @Override + public void set_width(int w) { + throw new IllegalStateException("Changing the width is not supported."); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) { } + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetLine.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetLine.java new file mode 100644 index 00000000..c84a20aa --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/modular/ModularWidgetLine.java @@ -0,0 +1,67 @@ +package de.mrjulsen.crn.client.gui.widgets.modular; + +import java.util.ArrayList; +import java.util.Collection; + +import com.simibubi.create.foundation.gui.widget.ScrollInput; + +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; + +public class ModularWidgetLine extends DLWidgetContainer { + + protected static final int HEIGHT = 22; + + private int currentX = 0; + private Collection scrollInputs = new ArrayList<>(); + + public ModularWidgetLine(int x, int y, int width) { + super(x, y, width, HEIGHT); + } + + public int getCurrentX() { + return currentX; + } + + public int getRemainingWidth() { + return width() - currentX; + } + + public T add(T w) { + currentX += w.x - currentX + w.getWidth(); + if (w instanceof ScrollInput i) { + scrollInputs.add(i); + } + return this.addRenderableWidget(w); + } + + public T add(T w) { + currentX += w.x() - currentX + w.width(); + return this.addRenderableOnly(w); + } + + @Override + public void tick() { + super.tick(); + for (ScrollInput i : scrollInputs) { + i.tick(); + } + } + + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public boolean consumeScrolling(double mouseX, double mouseY) { + return false; + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java index 82c18591..c302d402 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/AbstractDataListEntry.java @@ -12,8 +12,8 @@ import de.mrjulsen.mcdragonlib.client.gui.widgets.DLButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; import de.mrjulsen.mcdragonlib.client.render.Sprite; import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; @@ -26,7 +26,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; -public abstract class AbstractDataListEntry> extends WidgetContainer { +public abstract class AbstractDataListEntry> extends DLWidgetContainer { protected final DataListContainer parent; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java index 26075911..f02cb819 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DLOptionsList.java @@ -6,9 +6,9 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractScrollBar; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLScrollableWidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; -import de.mrjulsen.mcdragonlib.client.gui.widgets.ScrollableWidgetContainer; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.core.EAlignment; @@ -20,7 +20,7 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; -public class DLOptionsList extends ScrollableWidgetContainer { +public class DLOptionsList extends DLScrollableWidgetContainer { private final DLAbstractScrollBar scrollBar; private int contentHeight = 0; @@ -34,7 +34,7 @@ public DLOptionsList(Screen parent, int x, int y, int width, int height, DLAbstr scrollBar.setAutoScrollerSize(true); scrollBar.setScreenSize(height()); - scrollBar.updateMaxScroll(0); + scrollBar.setMaxScroll(0); scrollBar.withOnValueChanged((sb) -> setYScrollOffset(sb.getScrollValue())); scrollBar.setStepSize(10); } @@ -48,7 +48,7 @@ public int getContentWidth() { return width() - 20; } - public OptionEntry addOption(Function, T> contentContainer, Component text, Component description, BiConsumer, OptionEntryHeader> onHeaderClick, Function onTitleEdited) { + public OptionEntry addOption(Function, T> contentContainer, Component text, Component description, BiConsumer, OptionEntryHeader> onHeaderClick, Function onTitleEdited) { OptionEntry entry = new OptionEntry(parent, this, 0, y(), width() - 20, contentContainer, text, description, x -> rearrangeContent(), onHeaderClick, onTitleEdited); addRenderableWidget(entry); return entry; @@ -83,7 +83,7 @@ public void rearrangeContent() { } } contentHeight += 10; - scrollBar.updateMaxScroll(contentHeight); + scrollBar.setMaxScroll(contentHeight); if (!scrollBar.canScroll()) { scrollBar.scrollTo(0); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java index 42e4e0e5..93e0818f 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/DataListContainer.java @@ -17,7 +17,7 @@ import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; @@ -26,7 +26,7 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; -public class DataListContainer extends WidgetContainer { +public class DataListContainer extends DLWidgetContainer { private static final int BORDER_WIDTH = 2; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java index 8412ca17..22caa76d 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/NewEntryWidget.java @@ -13,7 +13,7 @@ import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; import de.mrjulsen.mcdragonlib.client.util.DLWidgetsCollection; import de.mrjulsen.mcdragonlib.client.util.Graphics; @@ -27,7 +27,7 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.MutableComponent; -public class NewEntryWidget extends WidgetContainer { +public class NewEntryWidget extends DLWidgetContainer { private final DLEditBox nameBox; private final DLIconButton addBtn; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java index 8af99032..81260067 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/options/OptionEntry.java @@ -18,7 +18,7 @@ import de.mrjulsen.mcdragonlib.client.gui.widgets.DLEditBox; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLTooltip; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; import de.mrjulsen.mcdragonlib.client.render.Sprite; import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; @@ -33,7 +33,7 @@ import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.Style; -public class OptionEntry extends WidgetContainer { +public class OptionEntry extends DLWidgetContainer { public static void expandOrCollapse(OptionEntry entry) { if (entry.isExpanded()) diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java index f9fb08a7..aac3c089 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RouteDetailsTransferWidget.java @@ -2,7 +2,7 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.gui.ModGuiIcons; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.navigation.TransferConnection; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLRenderable; import de.mrjulsen.mcdragonlib.client.util.Graphics; @@ -17,15 +17,15 @@ public class RouteDetailsTransferWidget extends DLRenderable { - private final MutableComponent transferText = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.transfer"); + private final MutableComponent transferText = CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_details.transfer"); protected static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/widgets.png"); protected static final int GUI_TEXTURE_WIDTH = 256; protected static final int GUI_TEXTURE_HEIGHT = 256; protected static final int ENTRY_WIDTH = 225; - private final MutableComponent textConnectionEndangered = ELanguage.translate("gui.createrailwaysnavigator.route_overview.connection_endangered").withStyle(ChatFormatting.GOLD).withStyle(ChatFormatting.BOLD); - private final MutableComponent textConnectionMissed = ELanguage.translate("gui.createrailwaysnavigator.route_overview.connection_missed").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD); + private final MutableComponent textConnectionEndangered = CustomLanguage.translate("gui.createrailwaysnavigator.route_overview.connection_endangered").withStyle(ChatFormatting.GOLD).withStyle(ChatFormatting.BOLD); + private final MutableComponent textConnectionMissed = CustomLanguage.translate("gui.createrailwaysnavigator.route_overview.connection_missed").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD); private final TransferConnection connection; diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java index 14117750..e2ffd613 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartEntryWidget.java @@ -74,19 +74,19 @@ protected void renderData(Graphics graphics, int y) { if (this.type == TrainStopType.TRANSIT) { graphics.poseStack().translate((x() + 28) / scale, (y + 2) / scale, 0); - GuiUtils.drawString(graphics, font, 00, 00, TextUtils.text(TimeUtils.parseTime(stop.getScheduledArrivalTime() + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, font, 00, 12, TextUtils.text(TimeUtils.parseTime(stop.getScheduledDepartureTime() + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 00, 00, TextUtils.text(TimeUtils.parseTime(stop.getScheduledArrivalTime() + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 00, 12, TextUtils.text(TimeUtils.parseTime(stop.getScheduledDepartureTime() + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); if (stop.shouldRenderRealTime() && !part.isCancelled() && valid) { - GuiUtils.drawString(graphics, font, 30, 00, TimeUtils.parseTime(stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); - GuiUtils.drawString(graphics, font, 30, 12, TimeUtils.parseTime(stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 30, 00, TimeUtils.parseTime(stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision) + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get()), stop.isArrivalDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 30, 12, TimeUtils.parseTime(stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get()), stop.isDepartureDelayed() ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); } } else { graphics.poseStack().translate((x() + 28) / scale, (y + 6) / scale, 0); - GuiUtils.drawString(graphics, font, 00, 00, TextUtils.text(TimeUtils.parseTime((type == TrainStopType.START ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime()) + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 00, 00, TextUtils.text(TimeUtils.parseTime((type == TrainStopType.START ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime()) + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get())).withStyle(valid ? ChatFormatting.RESET : ChatFormatting.STRIKETHROUGH), valid ? 0xFFFFFFFF : Constants.COLOR_DELAYED, EAlignment.LEFT, false); if (stop.shouldRenderRealTime() && !part.isCancelled() && valid) { long realTime = type == TrainStopType.START ? stop.getScheduledDepartureTime() + (stop.getDepartureTimeDeviation() / precision * precision) : stop.getScheduledArrivalTime() + (stop.getArrivalTimeDeviation() / precision * precision); - GuiUtils.drawString(graphics, font, 30, 00, TimeUtils.parseTime(realTime + DragonLib.DAYTIME_SHIFT, ModClientConfig.TIME_FORMAT.get()), (type == TrainStopType.START ? stop.isDepartureDelayed() : stop.isArrivalDelayed()) ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, 30, 00, TimeUtils.parseTime(realTime + DragonLib.daytimeShift(), ModClientConfig.TIME_FORMAT.get()), (type == TrainStopType.START ? stop.isDepartureDelayed() : stop.isArrivalDelayed()) ? Constants.COLOR_DELAYED : Constants.COLOR_ON_TIME, EAlignment.LEFT, false); } } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java index 40b6c797..d0c5cf84 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartTrainDetailsWidget.java @@ -9,7 +9,7 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.client.gui.CreateDynamicWidgets; -import de.mrjulsen.crn.client.gui.screen.TrainJourneySreen; +import de.mrjulsen.crn.client.gui.screen.TrainJourneyScreen; import de.mrjulsen.crn.client.gui.widgets.routedetails.RoutePartWidget.RoutePartDetailsActionBuilder; import de.mrjulsen.crn.data.train.ClientTrainStop; import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; @@ -20,7 +20,7 @@ import de.mrjulsen.mcdragonlib.DragonLib; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLAbstractImageButton.ButtonType; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.render.DynamicGuiRenderer.AreaStyle; import de.mrjulsen.mcdragonlib.client.render.GuiIcons; import de.mrjulsen.mcdragonlib.client.render.Sprite; @@ -37,7 +37,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -public class RoutePartTrainDetailsWidget extends WidgetContainer implements Closeable { +public class RoutePartTrainDetailsWidget extends DLWidgetContainer implements Closeable { protected static final ResourceLocation GUI = new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "textures/gui/widgets.png"); protected static final int GUI_TEXTURE_WIDTH = 256; @@ -77,7 +77,7 @@ public RoutePartTrainDetailsWidget(Screen parent, RoutePartWidget container, Cli currentHeight = DEFAULT_HEIGHT; updateStatus(); - addAction(new RoutePartDetailsActionBuilder(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.title"), Sprite.empty(), (b) -> Minecraft.getInstance().setScreen(new TrainJourneySreen(parent, route, part.getTrainId())))); + addAction(new RoutePartDetailsActionBuilder(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".journey_info.title"), Sprite.empty(), (b) -> Minecraft.getInstance().setScreen(new TrainJourneyScreen(parent, route, part.getTrainId())))); CRNEventsManager.getEventOptional(RouteDetailsActionsEvent.class).ifPresent(x -> x.run(route, part, container.isExpanded()).forEach(this::addAction)); if (!part.getStopovers().isEmpty()) { addAction(new RoutePartDetailsActionBuilder(container.isExpanded() ? Constants.TOOLTIP_COLLAPSE : Constants.TOOLTIP_EXPAND, (container.isExpanded() ? GuiIcons.ARROW_UP : GuiIcons.ARROW_DOWN).getAsSprite(16, 16), (b) -> container.setExpanded(!container.isExpanded()))); @@ -126,7 +126,7 @@ protected void renderData(Graphics graphics, int y) { graphics.poseStack().scale(scale, scale, scale); Component trainName = TextUtils.text(part.getLastStop().getTrainDisplayName()).withStyle(ChatFormatting.BOLD); CreateDynamicWidgets.renderTextHighlighted(graphics, (int)((x() + 80 + 24) / scale), (int)((y + 4) / scale), font, trainName, part.getLastStop().getTrainDisplayColor()); - GuiUtils.drawString(graphics, font, (int)((x() + 80 + 24) / scale) + font.width(trainName) + 10, (int)((y + 6) / scale), GuiUtils.ellipsisString(font, TextUtils.text(String.format("%s (%s)", stop.getTrainName(), stop.getTrainId().toString().split("-")[0])), (int)((maxWidth - font.width(trainName) - 15) / scale)), 0xFFDBDBDB, EAlignment.LEFT, false); + GuiUtils.drawString(graphics, font, (int)((x() + 80 + 24) / scale) + font.width(trainName) + 10, (int)((y + 6) / scale), GuiUtils.ellipsisString(font, TextUtils.text(String.format("%s (%s)", stop.getTrainDisplayName(), stop.getTrainId().toString().split("-")[0])), (int)((maxWidth - font.width(trainName) - 15) / scale)), 0xFFDBDBDB, EAlignment.LEFT, false); GuiUtils.drawString(graphics, font, (int)((x() + 80 + 24) / scale), (int)((y + 18) / scale), GuiUtils.ellipsisString(font, TextUtils.text(stop.getDisplayTitle()), (int)((maxWidth - 24) / scale)), 0xFFDBDBDB, EAlignment.LEFT, false); graphics.poseStack().scale(mul, mul, mul); graphics.poseStack().popPose(); @@ -174,7 +174,6 @@ public int getDy() { @Override public void close() { - part.stopListeningAll(this); } @Override diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java index 5daab598..35a4cf98 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/widgets/routedetails/RoutePartWidget.java @@ -1,7 +1,5 @@ package de.mrjulsen.crn.client.gui.widgets.routedetails; -import java.io.Closeable; -import java.io.IOException; import java.util.List; import java.util.function.Consumer; @@ -10,8 +8,8 @@ import de.mrjulsen.crn.data.navigation.ClientRoute; import de.mrjulsen.crn.data.navigation.ClientRoutePart; import de.mrjulsen.mcdragonlib.client.gui.widgets.DLIconButton; +import de.mrjulsen.mcdragonlib.client.gui.widgets.DLWidgetContainer; import de.mrjulsen.mcdragonlib.client.gui.widgets.IDragonLibWidget; -import de.mrjulsen.mcdragonlib.client.gui.widgets.WidgetContainer; import de.mrjulsen.mcdragonlib.client.render.Sprite; import de.mrjulsen.mcdragonlib.client.util.DLWidgetsCollection; import de.mrjulsen.mcdragonlib.client.util.Graphics; @@ -22,7 +20,7 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; -public class RoutePartWidget extends WidgetContainer { +public class RoutePartWidget extends DLWidgetContainer { private final ClientRoutePart part; private final ClientRoute route; @@ -49,11 +47,6 @@ public RoutePartWidget(Screen parent, int x, int y, int width, ClientRoute route } public void initGui() { - children().stream().filter(x -> x instanceof Closeable).forEach(x -> { - try { - ((Closeable)x).close(); - } catch (IOException e) {} - }); clearWidgets(); stackLayoutY = 0; stationWidgets.clear(); diff --git a/common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java b/common/src/main/java/de/mrjulsen/crn/client/lang/CustomLanguage.java similarity index 83% rename from common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java rename to common/src/main/java/de/mrjulsen/crn/client/lang/CustomLanguage.java index 3bc57dab..23a5f343 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/lang/ELanguage.java +++ b/common/src/main/java/de/mrjulsen/crn/client/lang/CustomLanguage.java @@ -7,7 +7,7 @@ import net.minecraft.network.chat.MutableComponent; import net.minecraft.util.StringRepresentable; -public enum ELanguage implements StringRepresentable { +public enum CustomLanguage implements StringRepresentable { DEFAULT("defaut", "def"), ENGLISH("english", "en_us"), GERMAN("german", "de_de"), @@ -21,13 +21,17 @@ public enum ELanguage implements StringRepresentable { FRENCH("french", "fr_fr"), KOREAN("korean", "ko_kr"), SWEDISH("swedish", "sv_se"), - PORTUGUESE("portuguese", "pt_pt"); + PORTUGUESE("portuguese", "pt_pt"), + BASQUE("basque", "eu_es"), + ITALIAN("italian", "it_it"), + JAPANESE("japanese", "ja_jp"), + PORTUGUESE_brazilian("portuguese_brazilian", "pt_br"), + UKRAINIAN("ukrainian", "uk_ua"); private String name; private String code; - - private ELanguage(String name, String code) { + private CustomLanguage(String name, String code) { this.name = name; this.code = code; } @@ -40,7 +44,7 @@ public String getCode() { return code; } - public static ELanguage getByCode(String code) { + public static CustomLanguage getByCode(String code) { return Arrays.stream(values()).filter(x -> x.getCode().equals(code)).findFirst().orElse(DEFAULT); } diff --git a/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java b/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java index dffbc443..3e7e4022 100644 --- a/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java +++ b/common/src/main/java/de/mrjulsen/crn/cmd/DebugCommand.java @@ -9,6 +9,8 @@ import de.mrjulsen.crn.registry.ModAccessorTypes; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import dev.architectury.platform.Platform; +import dev.architectury.utils.Env; import net.minecraft.Util; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; @@ -87,9 +89,14 @@ private static int reset(CommandSourceStack cmd) throws CommandSyntaxException { } private static int showTrainObservationOverlay(CommandSourceStack cmd) throws CommandSyntaxException { - cmd.sendSuccess(TextUtils.text("Visibility of the train debug overlay has been toggled."), false); - DebugOverlay.toggle(); - return 1; + if (Platform.getEnvironment() == Env.CLIENT) { + cmd.sendSuccess(TextUtils.text("Visibility of the train debug overlay has been toggled."), false); + DebugOverlay.toggle(); + return 1; + } else { + cmd.sendFailure(TextUtils.text("Cannot open the train debug overlay in multiplayer.")); + } + return 0; } private static int showTrainDebugScreen(CommandSourceStack cmd) throws CommandSyntaxException { diff --git a/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java b/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java index d347c20e..a6ca273b 100644 --- a/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java +++ b/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java @@ -1,7 +1,7 @@ package de.mrjulsen.crn.config; import de.mrjulsen.crn.client.gui.overlay.OverlayPosition; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.util.ESpeedUnit; import de.mrjulsen.mcdragonlib.util.TimeUtils.TimeFormat; import net.minecraftforge.common.ForgeConfigSpec; @@ -18,7 +18,7 @@ public class ModClientConfig { public static final ForgeConfigSpec.ConfigValue ROUTE_NOTIFICATIONS; public static final ForgeConfigSpec.ConfigValue ROUTE_OVERLAY_POSITION; public static final ForgeConfigSpec.ConfigValue TIME_FORMAT; - public static final ForgeConfigSpec.ConfigValue LANGUAGE; + public static final ForgeConfigSpec.ConfigValue LANGUAGE; public static final ForgeConfigSpec.ConfigValue SPEED_UNIT; public static final double MIN_SCALE = 0.25f; @@ -42,7 +42,7 @@ public class ModClientConfig { .defineEnum("route_overlay.position", OverlayPosition.TOP_LEFT); LANGUAGE = BUILDER.comment("The language that should be used for announcements of the navigator. Can be different from the game's language settings. (Default: Default)") - .defineEnum("language", ELanguage.DEFAULT); + .defineEnum("language", CustomLanguage.DEFAULT); SPEED_UNIT = BUILDER.comment("The unit to be used to represent speed. (Default: KMH)") .defineEnum("speed_unit", ESpeedUnit.KMH); TIME_FORMAT = BUILDER.comment("Display Time Format. (Default: Hours 24)") diff --git a/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java b/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java index 150ae845..1df957cc 100644 --- a/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java +++ b/common/src/main/java/de/mrjulsen/crn/config/ModCommonConfig.java @@ -14,7 +14,9 @@ public class ModCommonConfig { public static final ForgeConfigSpec.ConfigValue TRANSFER_COST; public static final ForgeConfigSpec.ConfigValue TOTAL_DURATION_DEVIATION_THRESHOLD; public static final ForgeConfigSpec.ConfigValue CUSTOM_TRANSIT_TIME_CALCULATION; + public static final ForgeConfigSpec.ConfigValue USE_CREATE_TRANSIT_TIMES_ON_INIT; public static final ForgeConfigSpec.ConfigValue EXCLUDE_TRAINS; + public static final ForgeConfigSpec.ConfigValue ADVANCED_LOGGING; static { BUILDER.push(CreateRailwaysNavigator.MOD_ID + "_common_config"); @@ -32,6 +34,8 @@ public class ModCommonConfig { CUSTOM_TRANSIT_TIME_CALCULATION = BUILDER.comment("When activated, CRN calculates the transit times of the trains and does not use the calculations from Create. CRN is much more accurate, while Create calculates an average. (Default: ON)") .define("train_data_calculation.custom_transit_time_calculation", true); + USE_CREATE_TRANSIT_TIMES_ON_INIT = BUILDER.comment("When activated, CRN uses the transit times provided by Create (if available) when initializing. When turned off, the initialization may take longer. (Default: ON)") + .define("train_data_calculation.use_create_transit_times_on_init", true); TOTAL_DURATION_BUFFER_SIZE = BUILDER.comment(new String[] {"[in Cycles]", "How often the calculated time for a route section between two stations must deviate from the current reference value before the reference value is updated. (Default: 3)"}) .defineInRange("train_data_calculation.total_duration_deviation_buffer_size", 3, 1, 16); TOTAL_DURATION_DEVIATION_THRESHOLD = BUILDER.comment(new String[] {"[in Ticks]", "Deviations of the calculated time for a route section between two stations from the reference value that are smaller than the threshold value are not taken into account. (Default: 50)"}) @@ -41,6 +45,9 @@ public class ModCommonConfig { AUTO_RESET_TIMINGS = BUILDER.comment(new String[] {"[In Cycles]", "(ONLY WORKS FOR TRAINS WITH DYNAMIC DELAYS! Trains without dynamic delays do this every new schedule section by default.)", " ", "Every X cycles the scheduled times are updated to the current real-time data. (Default: 2; Disabled: 0)"}) .defineInRange("train_data_calculation.auto_reset_timings", 2, 0, Integer.MAX_VALUE); + ADVANCED_LOGGING = BUILDER.comment(new String[] {"Prints more details to the console to better observe the behavior of CRN. Only relevant for debugging."}) + .define("debug.advanced_logging", false); + BUILDER.pop(); SPEC = BUILDER.build(); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java b/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java index 781efc63..9a486d82 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/ISaveableNavigatorData.java @@ -17,7 +17,7 @@ public interface ISaveableNavigatorData { /** The value (usually the time at which the corresponding entry is relevant) by which the items are sorted and grouped. */ long timeOrderValue(); default long dayOrderValue() { - return (timeOrderValue() + DragonLib.DAYTIME_SHIFT) / DragonLib.TICKS_PER_DAY; + return (timeOrderValue() + DragonLib.daytimeShift()) / DragonLib.ticksPerDay(); } /** Custom value used for grouping with custom label. Default: {@code null} (grouped by time) */ default Pair customGroup() { diff --git a/common/src/main/java/de/mrjulsen/crn/data/StationTag.java b/common/src/main/java/de/mrjulsen/crn/data/StationTag.java index 40afc206..d88b963b 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/StationTag.java +++ b/common/src/main/java/de/mrjulsen/crn/data/StationTag.java @@ -191,7 +191,12 @@ public void addAll(Map stations) { */ public boolean contains(String stationName) { String regex = stationName.isBlank() ? stationName : "\\Q" + stationName.replace("*", "\\E.*\\Q"); - return stations.keySet().stream().anyMatch(x -> x.matches(regex)); + for (String name : stations.keySet()) { + if (name.matches(regex)) { + return true; + } + } + return false; } public Set getAllStationNames() { @@ -226,7 +231,17 @@ public String toString() { @Override public boolean equals(Object obj) { if (obj instanceof StationTag alias) { - return getTagName().equals(alias.getTagName()) && getAllStationNames().size() == alias.getAllStationNames().size() && getAllStationNames().stream().allMatch(x -> alias.contains(x)); + if (!getTagName().equals(alias.getTagName())) return false; + Set stationNames = getAllStationNames(); + Set otherStationNames = alias.getAllStationNames(); + if (stationNames.size() != otherStationNames.size()) return false; + + for (String name : stationNames) { + if (!otherStationNames.contains(name)) { + return false; + } + } + return true; } return false; } diff --git a/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java b/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java index 2e07e66b..62dec251 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java +++ b/common/src/main/java/de/mrjulsen/crn/data/TrainInfo.java @@ -1,8 +1,10 @@ package de.mrjulsen.crn.data; +import javax.annotation.Nullable; + import net.minecraft.nbt.CompoundTag; -public record TrainInfo(TrainLine line, TrainGroup group) { +public record TrainInfo(@Nullable TrainLine line, @Nullable TrainGroup group) { private static final String NBT_TRAIN_GROUP = "Group"; private static final String NBT_TRAIN_LINE = "Line"; diff --git a/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java b/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java index e4a8a550..a8e346a3 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java +++ b/common/src/main/java/de/mrjulsen/crn/data/UserSettings.java @@ -15,6 +15,7 @@ import java.util.stream.Collectors; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.storage.GlobalSettings; import de.mrjulsen.crn.event.ModCommonEvents; import de.mrjulsen.crn.exceptions.RuntimeSideException; @@ -136,7 +137,7 @@ public final synchronized void save() throws RuntimeSideException { CompoundTag nbt = this.toNbt(); try { NbtIo.writeCompressed(nbt, new File(ModCommonEvents.getCurrentServer().get().getWorldPath(new LevelResource("data/" + FILENAME + getOwnerId() + ".nbt")).toString())); - CreateRailwaysNavigator.LOGGER.info("Saved user settings."); + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Saved user settings."); } catch (IOException e) { CreateRailwaysNavigator.LOGGER.error("Unable to save user settings.", e); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java index 4feba82a..7918695a 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java @@ -1,6 +1,5 @@ package de.mrjulsen.crn.data.navigation; -import java.util.Collection; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; @@ -14,7 +13,8 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.ClientWrapper; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.SavedRoutesManager; import de.mrjulsen.crn.data.train.ClientTrainStop; import de.mrjulsen.crn.data.train.RoutePartProgressState; @@ -143,7 +143,6 @@ public static record QueuedAnnouncementEvent(Runnable callback, ClientRoutePart private static final String keyNotificationConnectionCanceled = "gui.createrailwaysnavigator.route_overview.journey_interrupted"; private final Map>> listeners = new HashMap<>(); - private final Map> queuedNotifications = new HashMap<>(); private final long id = System.nanoTime(); private final Map listenerIds = new HashMap<>(); @@ -181,8 +180,10 @@ public ClientRoute(List parts, boolean realTimeTracker) { super(parts, realTimeTracker); this.currentPart = getFirstClientPart(); + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Created new " + this); + if (!realTimeTracker) return; - getClientParts().stream().forEach(x -> listenerIds.put(ClientTrainListener.register(x.getSessionId(), x.getTrainId(), x::update), x)); + getClientParts().forEach(x -> listenerIds.put(ClientTrainListener.register(x.getSessionId(), x.getTrainId(), x::update), x)); CRNEventsManager.getEvent(DefaultTrainDataRefreshEvent.class).register(CreateRailwaysNavigator.MOD_ID + "_" + id, this::update); addListener(); @@ -231,10 +232,10 @@ public ClientRoute(List parts, boolean realTimeTracker) { if (currentPartIndex > 0) return; sendNotification( - ELanguage.translate(keyNotificationJourneyBeginsTitle, getEnd().getClientTag().tagName()), + CustomLanguage.translate(keyNotificationJourneyBeginsTitle, getEnd().getClientTag().tagName()), getStart().getRealTimeStationTag().info().isPlatformKnown() ? - ELanguage.translate(keyNotificationJourneyBeginsWithPlatform, getStart().getTrainDisplayName(), getStart().getDisplayTitle(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false), getStart().getRealTimeStationTag().info().platform()) : - ELanguage.translate(keyNotificationJourneyBegins, getStart().getTrainDisplayName(), getStart().getDisplayTitle(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false)) + CustomLanguage.translate(keyNotificationJourneyBeginsWithPlatform, getStart().getTrainDisplayName(), getStart().getDisplayTitle(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false), getStart().getRealTimeStationTag().info().platform()) : + CustomLanguage.translate(keyNotificationJourneyBegins, getStart().getTrainDisplayName(), getStart().getDisplayTitle(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false)) ); queuedAnnouncements.add(new QueuedAnnouncementEvent(() -> { @@ -331,14 +332,14 @@ public ClientRoute(List parts, boolean realTimeTracker) { part.listen(ClientRoutePart.EVENT_SCHEDULE_CHANGED, this, x -> { if (scheduleChangedSent) return; - sendNotification(ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.notification.schedule_changed.title"), ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.notification.schedule_changed")); + sendNotification(CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.notification.schedule_changed.title"), CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.notification.schedule_changed")); notifyListeners(EVENT_SCHEDULE_CHANGED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); scheduleChangedSent = true; }); part.listen(ClientRoutePart.EVENT_TRAIN_CANCELLED, this, x -> { if (cancelledSent) return; - sendNotification(ELanguage.translate(keyNotificationConnectionCanceledTitle), ELanguage.translate(keyNotificationConnectionCanceled, x.part().getFirstStop().getTrainDisplayName())); + sendNotification(CustomLanguage.translate(keyNotificationConnectionCanceledTitle), CustomLanguage.translate(keyNotificationConnectionCanceled, x.part().getFirstStop().getTrainDisplayName())); notifyListeners(EVENT_ANY_TRAIN_CANCELLED, new ListenerNotificationData(this, x.part(), x.trainStop(), null)); cancelledSent = true; }); @@ -400,7 +401,7 @@ public ClientRoute(List parts, boolean realTimeTracker) { getLastClientPart().listen(ClientRoutePart.EVENT_DEPARTURE_FROM_LAST_STOP, this, x -> { if (currentPartIndex < parts.size() - 1) return; this.progressState = RouteProgressState.AFTER; - sendNotification(ELanguage.translate(keyNotificationJourneyCompletedTitle), ELanguage.translate(keyNotificationJourneyCompleted)); + sendNotification(CustomLanguage.translate(keyNotificationJourneyCompletedTitle), CustomLanguage.translate(keyNotificationJourneyCompleted)); if (!savedRouteRemoved) { savedRouteRemoved = true; SavedRoutesManager.removeRoute(this); @@ -461,7 +462,7 @@ public ClientRoute(List parts, boolean realTimeTracker) { listen(EVENT_ANY_STATION_CHANGED, this, (p) -> { if (stationChangedSent) return; - sendNotification(ELanguage.translate(keyNotificationPlatformChangedTitle), ELanguage.translate(keyNotificationPlatformChanged, + sendNotification(CustomLanguage.translate(keyNotificationPlatformChangedTitle), CustomLanguage.translate(keyNotificationPlatformChanged, p.trainStop().getTrainDisplayName(), p.trainStop().getRealTimeStationTag().info().platform() )); @@ -475,11 +476,11 @@ public ClientRoute(List parts, boolean realTimeTracker) { }); listen(EVENT_ANNOUNCE_TRANSFER_ARRIVAL_STATION, this, (p) -> { - sendNotification(ELanguage.translate(keyNotificationTransferTitle), getStart().getRealTimeStationTag().info().isPlatformKnown() ? ELanguage.translate(keyNotificationTransferWithPlatform, + sendNotification(CustomLanguage.translate(keyNotificationTransferTitle), getStart().getRealTimeStationTag().info().isPlatformKnown() ? CustomLanguage.translate(keyNotificationTransferWithPlatform, p.connection().getDepartureStation().getTrainDisplayName(), p.connection().getDepartureStation().getDisplayTitle(), p.connection().getDepartureStation().getRealTimeStationTag().info().platform() - ) : ELanguage.translate(keyNotificationTransfer, + ) : CustomLanguage.translate(keyNotificationTransfer, p.connection().getDepartureStation().getTrainDisplayName(), p.connection().getDepartureStation().getDisplayTitle() ) @@ -496,8 +497,8 @@ private void sendNotification(Component title, Component description) { private void queueDelayNotification(ClientTrainStop stop, boolean start) { if (shouldShowNotifications()) { ClientWrapper.sendCRNNotification( - ELanguage.translate(keyNotificationTrainDelayedTitle, stop.getTrainDisplayName(), TimeUtils.parseDurationShort((int)(start ? stop.getDepartureTimeDeviation() : stop.getArrivalTimeDeviation()))), - ELanguage.translate(keyNotificationTrainDelayed, + CustomLanguage.translate(keyNotificationTrainDelayedTitle, stop.getTrainDisplayName(), TimeUtils.parseDurationShort((int)(start ? stop.getDepartureTimeDeviation() : stop.getArrivalTimeDeviation()))), + CustomLanguage.translate(keyNotificationTrainDelayed, ModUtils.formatTime(start ? stop.getRoundedRealTimeDepartureTime() : stop.getRoundedRealTimeArrivalTime(), false), ModUtils.formatTime(start ? stop.getScheduledDepartureTime() : stop.getScheduledArrivalTime(), false), stop.getClientTag().tagName() @@ -507,13 +508,13 @@ private void queueDelayNotification(ClientTrainStop stop, boolean start) { private void queueConnectionEndangeredNotification(TransferConnection connection) { if (shouldShowNotifications()) { - ClientWrapper.sendCRNNotification(ELanguage.translate(keyNotificationConnectionEndangeredTitle), ELanguage.translate(keyNotificationConnectionEndangered, connection.getDepartureStation().getTrainDisplayName(), connection.getDepartureStation().getDisplayTitle())); + ClientWrapper.sendCRNNotification(CustomLanguage.translate(keyNotificationConnectionEndangeredTitle), CustomLanguage.translate(keyNotificationConnectionEndangered, connection.getDepartureStation().getTrainDisplayName(), connection.getDepartureStation().getDisplayTitle())); } } private void queueConnectionMissedNotification(TransferConnection connection) { if (shouldShowNotifications()) { - ClientWrapper.sendCRNNotification(ELanguage.translate(keyNotificationConnectionMissedTitle), ELanguage.translate(keyNotificationConnectionMissed, connection.getDepartureStation().getTrainDisplayName(), connection.getDepartureStation().getDisplayTitle())); + ClientWrapper.sendCRNNotification(CustomLanguage.translate(keyNotificationConnectionMissedTitle), CustomLanguage.translate(keyNotificationConnectionMissed, connection.getDepartureStation().getTrainDisplayName(), connection.getDepartureStation().getDisplayTitle())); } } @@ -599,15 +600,10 @@ public void update() { } } - getConnections().stream().forEach(x -> x.update()); + for (TransferConnection connection : getConnections()) { + connection.update(); + } - // process notifications - queuedNotifications.entrySet().forEach(x -> { - if (x.getValue().isEmpty()) { - return; - } - }); - queuedNotifications.clear(); isCancelled.clear(); resetSpamBlockers(); } @@ -633,20 +629,32 @@ public void close() { public void closeAll() { listenersCount = 0; - listenerIds.entrySet().stream().forEach(x -> ClientTrainListener.unregister(x.getValue().getTrainId(), x.getKey())); - getClientParts().stream().forEach(x -> { - x.stopListeningAll(this); - x.close(); - }); - getConnections().stream().forEach(x -> { - x.stopListeningAll(this); - x.close(); - }); + synchronized (listenerIds) { + for (Map.Entry e : listenerIds.entrySet()) { + ClientTrainListener.unregister(e.getValue().getTrainId(), e.getKey()); + } + } + List clientParts = getClientParts(); + synchronized (clientParts) { + for (ClientRoutePart part : clientParts) { + part.stopListeningAll(this); + part.close(); + } + } + List connections = getConnections(); + synchronized (connections) { + for (TransferConnection connection : connections) { + connection.stopListeningAll(this); + connection.close(); + } + } stopListeningAll(this); CRNEventsManager.getEvent(DefaultTrainDataRefreshEvent.class).unregister(CreateRailwaysNavigator.MOD_ID + "_" + id); clearEvents(); isClosed = true; CreateRailwaysNavigator.LOGGER.info("Route listener closed."); + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Closed " + this); + } public static ClientRoute fromNbt(CompoundTag nbt, boolean realTimeTracker) { @@ -661,12 +669,6 @@ public long timeOrderValue() { return getStart().getScheduledDepartureTime(); } - private static enum NotificationType { - DELAY, - CONNECTION_ENDANGERED, - CONNECTION_MISSED - } - public boolean isClosed() { return isClosed; } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java index ec2d8296..5b928a50 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java @@ -24,14 +24,16 @@ import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; public class ClientRoutePart extends RoutePart implements ITrainListenerClient, IListenable { public static record ListenerNotificationData(ClientRoutePart part, ClientTrainStop trainStop) {} public static record QueuedAnnouncementEvent(Runnable callback, ClientTrainStop trainStop) {} - public static final String EVENT_UPDATE = "update"; + public static final String EVENT_UPDATE = "update2"; public static final String EVENT_ANNOUNCE_START = "announce_start"; public static final String EVENT_ARRIVAL_AT_START = "arrival_at_start"; public static final String EVENT_DEPARTURE_FROM_START = "departure_at_start"; @@ -155,7 +157,9 @@ public ClientRoutePart(UUID sessionId, UUID trainId, List routeStops, notifyListeners(EVENT_LAST_STOP_STATION_CHANGED, new ListenerNotificationData(this, x)); }); - getAllClientStops().stream().forEach(x -> { + + + getAllClientStops().forEach(x -> { x.listen(ClientTrainStop.EVENT_SCHEDULE_CHANGED, this, a -> { notifyListeners(EVENT_SCHEDULE_CHANGED, new ListenerNotificationData(this, a)); }); @@ -223,19 +227,25 @@ public void update(TrainRealTimeData data) { MutableSingle shouldRenderStatus = new MutableSingle<>(false); - getAllClientStops().stream().forEach(x -> { - if (data.stationData().containsKey(x.getScheduleIndex())) { - x.update(data.stationData().get(x.getScheduleIndex())); - if (x.shouldRenderRealTime()) { - shouldRenderStatus.setFirst(true); + List allStops = getAllClientStops(); + synchronized (allStops) { + for (ClientTrainStop stop : allStops) { + if (data.stationData().containsKey(stop.getScheduleIndex())) { + stop.update(data.stationData().get(stop.getScheduleIndex())); + if (stop.shouldRenderRealTime()) { + shouldRenderStatus.setFirst(true); + } } } - }); - getAllJourneyClientStops().stream().forEach(x -> { - if (data.stationData().containsKey(x.getScheduleIndex())) { - x.update(data.stationData().get(x.getScheduleIndex())); + } + List allJourneyStops = getAllJourneyClientStops(); + synchronized (allJourneyStops) { + for (ClientTrainStop stop : allJourneyStops) { + if (data.stationData().containsKey(stop.getScheduleIndex())) { + stop.update(data.stationData().get(stop.getScheduleIndex())); + } } - }); + } if (shouldRenderStatus.getFirst() || data.cancelled()) { status.addAll(data.statusInfo()); @@ -279,30 +289,62 @@ public static RoutePart fromNbt(CompoundTag nbt) { @Override public void close() { - getAllClientStops().stream().forEach(x -> { - x.stopListeningAll(this); - x.close(); - }); - getAllJourneyClientStops().stream().forEach(x -> { - x.stopListeningAll(this); - x.close(); - }); + List allStops = getAllClientStops(); + synchronized (allStops) { + for (ClientTrainStop stop : allStops) { + stop.stopListeningAll(this); + stop.close(); + } + } + List allJourneyStops = getAllJourneyClientStops(); + synchronized (allJourneyStops) { + for (ClientTrainStop stop : allJourneyStops) { + stop.stopListeningAll(this); + stop.close(); + } + } stopListeningAll(this); - } + CreateRailwaysNavigator.LOGGER.info("CLOSED " + this); + } - public static record TrainRealTimeData(UUID sessionId, Map stationData, Set statusInfo, boolean cancelled) { + public static class TrainRealTimeData { + private static final String NBT_SESSION_ID = "SessionId"; private static final String NBT_STATUS_INFOS = "Status"; private static final String NBT_CANCELLED = "Cancelled"; + private final UUID sessionId; + private final Map stationData; + private final Set statusLocations; + private final Set status; + private final boolean cancelled; + + private TrainRealTimeData(UUID sessionId, Map stationData, Set statusLocations, Set status, boolean cancelled) { + this.sessionId = sessionId; + this.stationData = stationData; + this.statusLocations = statusLocations; + this.status = status; + this.cancelled = cancelled; + } + + public static TrainRealTimeData createServer(UUID sessionId, Map stationData, Set statusLocations, boolean cancelled) { + return new TrainRealTimeData(sessionId, stationData, statusLocations, null, cancelled); + } + + private static TrainRealTimeData createClient(UUID sessionId, Map stationData, Set status, boolean cancelled) { + return new TrainRealTimeData(sessionId, stationData, null, status, cancelled); + } + public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); nbt.putUUID(NBT_SESSION_ID, sessionId); - ListTag status = new ListTag(); - status.addAll(statusInfo().stream().map(x -> x.toNbt()).toList()); - nbt.put(NBT_STATUS_INFOS, status); + ListTag statusList = new ListTag(); + for (ResourceLocation s : statusLocations) { + statusList.add(StringTag.valueOf(s.toString())); + } + nbt.put(NBT_STATUS_INFOS, statusList); nbt.putBoolean(NBT_CANCELLED, cancelled); for (Map.Entry e : stationData.entrySet()) { @@ -312,12 +354,28 @@ public CompoundTag toNbt() { } public static TrainRealTimeData fromNbt(CompoundTag nbt) { - return new TrainRealTimeData( + return createClient( nbt.getUUID(NBT_SESSION_ID), nbt.getAllKeys().stream().filter(x -> { try { Integer.parseInt(x); return true; } catch (Exception e) { return false; } }).collect(Collectors.toMap(x -> Integer.parseInt(x), x -> TrainStopRealTimeData.fromNbt(nbt.getCompound(x)))), - nbt.getList(NBT_STATUS_INFOS, Tag.TAG_COMPOUND).stream().map(x -> CompiledTrainStatus.fromNbt((CompoundTag)x)).collect(Collectors.toSet()), + nbt.getList(NBT_STATUS_INFOS, Tag.TAG_STRING).stream().map(x -> CompiledTrainStatus.load(new ResourceLocation(((StringTag)x).getAsString()))).collect(Collectors.toSet()), nbt.getBoolean(NBT_CANCELLED) ); } + + public UUID sessionId() { + return sessionId; + } + + public Map stationData() { + return stationData; + } + + public Set statusInfo() { + return status; + } + + public boolean cancelled() { + return cancelled; + } } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java index 7f291924..85743311 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientTrainListener.java @@ -1,11 +1,12 @@ package de.mrjulsen.crn.data.navigation; -import java.util.ArrayList; +import java.util.Iterator; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import de.mrjulsen.crn.data.navigation.ClientRoutePart.TrainRealTimeData; import de.mrjulsen.crn.registry.ModAccessorTypes; import de.mrjulsen.mcdragonlib.data.Pair; import de.mrjulsen.mcdragonlib.util.DLUtils; @@ -45,20 +46,24 @@ public static void unregister(UUID trainId, UUID callbackId) { } public static void tick(Runnable andThen) { - callbacks.entrySet().stream().forEach(x -> { - if (x.getValue().isEmpty()) { - callbacks.remove(x.getKey()); - return; - } - - final Map>> listeners = x.getValue(); - DataAccessor.getFromServer(x.getKey(), ModAccessorTypes.UPDATE_REALTIME, res -> { - if (res != null) { - new ArrayList<>(listeners.values()).stream().forEach(a -> a.getSecond().accept(res)); + synchronized (callbacks) { + Iterator>>>> iterator = callbacks.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry>>> entry = iterator.next(); + if (entry.getValue().isEmpty()) { + iterator.remove(); + continue; } - DLUtils.doIfNotNull(andThen, a -> a.run()); - }); - }); + + final Map>> listeners = entry.getValue(); + DataAccessor.getFromServer(entry.getKey(), ModAccessorTypes.UPDATE_REALTIME, res -> { + if (res != null) { + listeners.values().forEach(a -> a.getSecond().accept(res)); + } + DLUtils.doIfNotNull(andThen, a -> a.run()); + }); + } + } } public static void clear() { diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java index 9320b6bf..8497f60d 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java @@ -138,7 +138,7 @@ protected void addTrain(Train train, TrainData data) { protected boolean isPredictionAllowed(TrainPrediction prediction) { TrainTravelSection section = prediction.getSection(); boolean usable = section.isUsable() || (section.isFirstStop(prediction) && section.previousSection().isUsable() && section.previousSection().shouldIncludeNextStationOfNextSection()); - return !globalSettings().isStationBlacklisted(prediction.getStationName()) && (prediction.getSection().getTrainGroup() == null || !userSettings.navigationExcludedTrainGroups.getValue().contains(prediction.getSection().getTrainGroup().getGroupName())) && usable; + return !globalSettings().isStationBlacklisted(prediction.getStationName()) && (prediction.getSection().getTrainGroup().map(x -> !userSettings.navigationExcludedTrainGroups.getValue().contains(x.getGroupName())).orElse(true)) && usable; } protected Node addNode(TrainPrediction prediction) { diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java index 759ad40c..dc130b78 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/Node.java @@ -79,9 +79,12 @@ public void setPreviousNode(Node previousNode) { public void setNextNode(Node nextNode) { this.nextNode = nextNode; - nextNode.getConnections().stream().filter(x -> x.target() == this).forEach(x -> { - nextConnections.add(new EdgeConnection(nextNode, x.edge().invert())); - }); + for (EdgeConnection connection : nextNode.getConnections()) { + if (connection.target() != this) { + continue; + } + nextConnections.add(new EdgeConnection(nextNode, connection.edge().invert())); + } } public Node getNextNode() { diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java index 678e4d62..2896330e 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/Route.java @@ -42,7 +42,12 @@ public List getConnections() { } public Optional getConnectionWith(TrainStop stop) { - return getConnections().stream().filter(x -> x.getArrivalStation() == stop || x.getDepartureStation() == stop).findFirst(); + for (TransferConnection connection : getConnections()) { + if (connection.getArrivalStation() == stop || connection.getDepartureStation() == stop) { + return Optional.ofNullable(connection); + } + } + return Optional.empty(); } public RoutePart getFirstPart() { @@ -109,7 +114,9 @@ public String toString() { public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); ListTag list = new ListTag(); - list.addAll(parts.stream().map(x -> x.toNbt()).toList()); + for (RoutePart part : parts) { + list.add(part.toNbt()); + } nbt.put(NBT_PARTS, list); return nbt; } @@ -126,7 +133,7 @@ public List getOverviewData() { List lines = new ArrayList<>(); lines.add(new SaveableNavigatorDataLine(TextUtils.text(ModUtils.formatTime(getStart().getScheduledDepartureTime(), false) + " " + getStart().getClientTag().tagName()), ModGuiIcons.ROUTE_START.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); lines.add(new SaveableNavigatorDataLine(TextUtils.text(ModUtils.formatTime(getEnd().getScheduledArrivalTime(), false) + " " + getEnd().getClientTag().tagName()), ModGuiIcons.ROUTE_END.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); - lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.date", (getStart().getScheduledDepartureTime() + DragonLib.DAYTIME_SHIFT) / DragonLib.TICKS_PER_DAY, ModUtils.formatTime(getStart().getScheduledDepartureTime(), false)).append(" | ").append(TimeUtils.parseDurationShort(departureIn())), ModGuiIcons.CALENDAR.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); + lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.date", (getStart().getScheduledDepartureTime() + DragonLib.daytimeShift()) / DragonLib.ticksPerDay(), ModUtils.formatTime(getStart().getScheduledDepartureTime(), false)).append(" | ").append(TimeUtils.parseDurationShort(departureIn())), ModGuiIcons.CALENDAR.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.transfers", getTransferCount()).append(TextUtils.text(" | " + TimeUtils.parseDurationShort(travelTime()))), ModGuiIcons.INFO.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); if (isAnyCancelled()) { lines.add(new SaveableNavigatorDataLine(TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.cancelled").withStyle(ChatFormatting.RED), ModGuiIcons.IMPORTANT.getAsSprite(ModGuiIcons.ICON_SIZE, ModGuiIcons.ICON_SIZE))); diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java index 9e95601f..b0761907 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/RoutePart.java @@ -139,7 +139,7 @@ public Set getStatus() { } private static Set> getBetween(TrainSchedule schedule, StationTag start, StationTag end, UserSettings settings) { - List stops = schedule.getAllStops().stream().sorted((a, b) -> Long.compare(a.getScheduledDepartureTime(), b.getScheduledDepartureTime())).toList(); + List stops = schedule.getAllStopsChronologicallyDeparture(); if (stops.stream().noneMatch(x -> x.getTag().equals(start)) || stops.stream().noneMatch(x -> x.getTag().equals(end))) { return Set.of(); @@ -193,10 +193,14 @@ public CompoundTag toNbt() { nbt.putUUID(NBT_SESSION_ID, sessionId); nbt.putUUID(NBT_TRAIN_ID, trainId); ListTag stopsList = new ListTag(); - stopsList.addAll(routeStops.stream().map(x -> x.toNbt(true)).toList()); + for (TrainStop stop : routeStops) { + stopsList.add(stop.toNbt(true)); + } nbt.put(NBT_STOPS, stopsList); ListTag journeyList = new ListTag(); - journeyList.addAll(allStops.stream().map(x -> x.toNbt(true)).toList()); + for (TrainStop stop : allStops) { + journeyList.add(stop.toNbt(true)); + } nbt.put(NBT_JOURNEY, journeyList); return nbt; } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java index 43b8c06a..b0f77593 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/TrainSchedule.java @@ -10,6 +10,7 @@ import de.mrjulsen.crn.data.StationTag; import de.mrjulsen.crn.data.train.TrainListener; import de.mrjulsen.crn.data.train.TrainStop; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; import de.mrjulsen.mcdragonlib.data.Cache; public class TrainSchedule { @@ -17,6 +18,7 @@ public class TrainSchedule { private final Train train; private final List stops; private final Cache> stopsChronologically = new Cache<>(() -> getAllStops().stream().sorted((a, b) -> Long.compare(a.getScheduledArrivalTime(), b.getScheduledArrivalTime())).toList()); + private final Cache> stopsChronologicallyDeparture = new Cache<>(() -> getAllStops().stream().sorted((a, b) -> Long.compare(a.getScheduledDepartureTime(), b.getScheduledDepartureTime())).toList(), ECachingPriority.LOW); private boolean simulated; private long simulationTime; @@ -46,6 +48,10 @@ public List getAllStops() { public List getAllStopsChronologically() { return stopsChronologically.get(); } + + public List getAllStopsChronologicallyDeparture() { + return stopsChronologicallyDeparture.get(); + } public UUID getSessionId() { return sessionId; @@ -56,7 +62,12 @@ public Train getTrain() { } public boolean stopsAt(StationTag tag) { - return stops.stream().anyMatch(x -> x.getTag().equals(tag)); + for (TrainStop stop : stops) { + if (stop.getTag().equals(tag)) { + return true; + } + } + return false; } public TrainSchedule simulate(long ticks) { @@ -77,13 +88,24 @@ public boolean isEqual(TrainSchedule other) { if (other == null) { return false; } - if (getAllStops().size() != other.getAllStops().size()) { - return false; + + List allStops = getAllStops(); + List otherAllStops = other.getAllStops(); + synchronized (allStops) { + synchronized (otherAllStops) { + if (allStops.size() != otherAllStops.size()) { + return false; + } + + Set otherTags = new HashSet<>(otherAllStops.size()); + otherAllStops.forEach(x -> otherTags.add(x.getTag().getTagName().get())); + for (TrainStop stop : allStops) { + if (!otherTags.contains(stop.getTag().getTagName().get())) { + return false; + } + } + return true; + } } - Set tagsA = new HashSet<>(); - Set tagsB = new HashSet<>(); - getAllStops().stream().forEach(x -> tagsA.add(x.getTag().getTagName().get())); - other.getAllStops().stream().forEach(x -> tagsB.add(x.getTag().getTagName().get())); - return tagsA.size() == tagsB.size() && tagsA.stream().allMatch(x -> tagsB.contains(x)); } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/IConditionsRequiresInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/IConditionsRequiresInstruction.java new file mode 100644 index 00000000..08949b70 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/IConditionsRequiresInstruction.java @@ -0,0 +1,4 @@ +package de.mrjulsen.crn.data.schedule; + +public interface IConditionsRequiresInstruction { +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/INavigationExtension.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/INavigationExtension.java new file mode 100644 index 00000000..872a2070 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/INavigationExtension.java @@ -0,0 +1,10 @@ +package de.mrjulsen.crn.data.schedule; + +import de.mrjulsen.crn.data.schedule.condition.IDelayedWaitCondition; +import de.mrjulsen.crn.data.schedule.condition.IDelayedWaitCondition.DelayedWaitConditionContext; +import de.mrjulsen.mcdragonlib.data.Pair; + +public interface INavigationExtension { + void addDelayedWaitCondition(Pair pair); + boolean isDelayedWaitConditionPending(); +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/IDelayedWaitCondition.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/IDelayedWaitCondition.java new file mode 100644 index 00000000..81722903 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/IDelayedWaitCondition.java @@ -0,0 +1,18 @@ +package de.mrjulsen.crn.data.schedule.condition; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleEntry; +import com.simibubi.create.content.trains.station.GlobalStation; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; + +@FunctionalInterface +public interface IDelayedWaitCondition { + + + public static final String NBT_DELAY = "Delay"; + + boolean runDelayed(DelayedWaitConditionContext context); + public static record DelayedWaitConditionContext(Level level, Train train, CompoundTag nbt, GlobalStation station, ScheduleEntry scheduleEntry) {} +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java new file mode 100644 index 00000000..502b8757 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java @@ -0,0 +1,103 @@ +package de.mrjulsen.crn.data.schedule.condition; + +import java.util.List; +import com.google.common.collect.ImmutableList; +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.schedule.ScheduleEntry; +import com.simibubi.create.content.trains.schedule.condition.ScheduledDelay; +import com.simibubi.create.content.trains.schedule.destination.DestinationInstruction; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import com.simibubi.create.foundation.utility.Pair; + +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.data.schedule.IConditionsRequiresInstruction; +import de.mrjulsen.crn.data.schedule.INavigationExtension; +import de.mrjulsen.crn.data.train.StationDepartureHistory; +import de.mrjulsen.crn.data.train.StationDepartureHistory.ETrainFilter; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +public class TrainSeparationCondition extends ScheduledDelay implements IDelayedWaitCondition, IConditionsRequiresInstruction { + + public static final String NBT_TIME = "Value"; + public static final String NBT_TRAIN_FILTER = "TrainFilter"; + public static final String NBT_TIME_UNIT = "TimeUnit"; + + public TrainSeparationCondition() { + super(); + data.putByte(NBT_TRAIN_FILTER, ETrainFilter.ANY.getIndex()); + } + + @Override + public Pair getSummary() { + return Pair.of(ItemStack.EMPTY, TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule.condition." + getId().getPath() + ".title", formatTime(true))); + } + + @Override + public ItemStack getSecondLineIcon() { + return new ItemStack(Items.OBSERVER); + } + + @Override + public List getTitleAs(String type) { + return ImmutableList.of( + TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath()), + TextUtils.translate(CreateRailwaysNavigator.MOD_ID + ".schedule." + type + "." + getId().getPath() + ".description", + formatTime(false) + ).withStyle(ChatFormatting.DARK_AQUA), + TextUtils.translate(getTrainFilter().getValueTranslationKey(CreateRailwaysNavigator.MOD_ID)).withStyle(ChatFormatting.AQUA) + ); + } + + @Override + public boolean tickCompletion(Level level, Train train, CompoundTag context) { + ScheduleEntry entry = train.runtime.getSchedule().entries.get(train.runtime.currentEntry); + ((INavigationExtension)(Object)train.navigation).addDelayedWaitCondition(de.mrjulsen.mcdragonlib.data.Pair.of(this, new DelayedWaitConditionContext(level, train, context, train.getCurrentStation(), entry))); + return true; + } + + + @Override + public boolean runDelayed(DelayedWaitConditionContext context) { + int delayValue = totalWaitTicks(); + long lastDepartureTimestamp = Long.MIN_VALUE; + String stationName = ""; + ScheduleEntry entry = context.scheduleEntry(); + if (entry.instruction instanceof DestinationInstruction instruction) { + stationName = instruction.getFilter(); + lastDepartureTimestamp = StationDepartureHistory.getLastMatchingDepartureTime(getTrainFilter(), context.train(), stationName); + } + + if (lastDepartureTimestamp + delayValue < DragonLib.getCurrentServer().get().overworld().getGameTime()) { + StationDepartureHistory.updateDepartureHistory(context.train(), context.station().name); + return true; + } + return false; + } + + @Override + public ResourceLocation getId() { + return new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "train_separation"); + } + + public ETrainFilter getTrainFilter() { + return ETrainFilter.getByIndex(data.getByte(NBT_TRAIN_FILTER)); + } + + @Override + @Environment(EnvType.CLIENT) + public void initConfigurationWidgets(ModularGuiLineBuilder builder) { + ClientWrapper.initTimingAdjustmentGui(this, builder); + } + +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java index a5b54bac..25ebd90e 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/ResetTimingsInstruction.java @@ -63,4 +63,6 @@ public void run(ScheduleRuntime runtime, TrainData data, Train train, int index) @Override public void predict(TrainData data, ScheduleRuntime runtime, int indexInSchedule, Train train) { } + + } \ No newline at end of file diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java index 114e8eb2..67045076 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/instruction/TravelSectionInstruction.java @@ -76,7 +76,7 @@ public List getTitleAs(String type) { /** HERE BE DRAGONS! This code is very illegal, but it works... */ @Override @Environment(EnvType.CLIENT) - public void initConfigurationWidgets(ModularGuiLineBuilder builder) { + public void initConfigurationWidgets(ModularGuiLineBuilder builder) { ClientWrapper.initScheduleSectionInstruction(this, builder); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java b/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java index 91113968..5cf6a75d 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java +++ b/common/src/main/java/de/mrjulsen/crn/data/storage/GlobalSettings.java @@ -15,10 +15,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Optional; import java.util.Map; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.StationTag; import de.mrjulsen.crn.data.TagName; import de.mrjulsen.crn.data.TrainGroup; @@ -99,7 +101,7 @@ public synchronized void save() { try { NbtIo.writeCompressed(nbt, new File(server.getWorldPath(new LevelResource("data/" + FILENAME)).toString())); - CreateRailwaysNavigator.LOGGER.info("Saved global settings."); + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Saved global settings."); } catch (IOException e) { CreateRailwaysNavigator.LOGGER.error("Unable to save global settings.", e); } @@ -121,7 +123,7 @@ public synchronized static GlobalSettings open(MinecraftServer server) throws IO return file; } - public CompoundTag serializeNbt() { + public synchronized CompoundTag serializeNbt() { CompoundTag nbt = new CompoundTag(); nbt.putInt(NBT_VERSION, DATA_VERSION); @@ -134,11 +136,11 @@ public CompoundTag serializeNbt() { nbt.put(NBT_TRAIN_GROUPS, trainGroupComp); ListTag stationsBlacklist = new ListTag(); - this.stationBlacklist.stream().forEach(x -> stationsBlacklist.add(StringTag.valueOf(x))); + this.stationBlacklist.forEach(x -> stationsBlacklist.add(StringTag.valueOf(x))); nbt.put(NBT_STATION_BLACKLIST, stationsBlacklist); ListTag trainsBlacklist = new ListTag(); - this.trainBlacklist.stream().forEach(x -> trainsBlacklist.add(StringTag.valueOf(x))); + this.trainBlacklist.forEach(x -> trainsBlacklist.add(StringTag.valueOf(x))); nbt.put(NBT_TRAIN_BLACKLIST, trainsBlacklist); CompoundTag trainLinesComp = new CompoundTag(); @@ -166,7 +168,7 @@ public void deserializeNbt(CompoundTag nbt) { * @deprecated For data migration only. Use {@code deserializeNbt} instead. */ @Deprecated - public void deserializeNbtLegacy(CompoundTag nbt) { + private void deserializeNbtLegacy(CompoundTag nbt) { final String NBT_ALIAS_REGISTRY = "RegisteredAliasData"; final String NBT_BLACKLIST = "StationBlacklist"; final String NBT_TRAIN_BLACKLIST = "TrainBlacklist"; @@ -221,7 +223,12 @@ public boolean hasStationTag(GlobalStation station) { } public boolean hasStationTag(String stationName) { - return stationTags.values().stream().anyMatch(x -> x.contains(stationName)); + for (StationTag tag : stationTags.values()) { + if (tag.contains(stationName)) { + return true; + } + } + return false; } public boolean stationTagExists(String tagName) { @@ -229,7 +236,12 @@ public boolean stationTagExists(String tagName) { } public boolean stationTagExists(TagName tagName) { - return stationTags.values().stream().anyMatch(x -> x.getTagName().equals(tagName)); + for (StationTag tag : stationTags.values()) { + if (tag.getTagName().equals(tagName)) { + return true; + } + } + return false; } public boolean stationTagExists(UUID id) { @@ -253,19 +265,22 @@ public StationTag getOrCreateStationTagFor(String stationName) { return getOrCreateTagForWildcard(stationName); } - Optional a = stationTags.values().stream().filter(x -> x.contains(stationName)).findFirst(); - if (a.isPresent()) { - return a.get(); + for (StationTag tag : stationTags.values()) { + if (tag.contains(stationName)) { + return tag; + } } - return new StationTag(null, TagName.of(stationName), Map.of(stationName, StationInfo.empty())); } private StationTag getOrCreateTagForWildcard(String stationName) { String regex = stationName.isBlank() ? stationName : "\\Q" + stationName.replace("*", "\\E.*\\Q") + "\\E"; - Optional a = stationTags.values().stream().filter(x -> x.getAllStationNames().stream().anyMatch(y -> y.matches(regex))).findFirst(); - if (a.isPresent()) { - return a.get(); + for (StationTag tag : stationTags.values()) { + for (String name : tag.getAllStationNames()) { + if (name.matches(regex)) { + return tag; + } + } } return new StationTag(null, TagName.of(stationName), Map.of(stationName, StationInfo.empty())); @@ -310,7 +325,12 @@ public StationTag registerStationTag(StationTag tag) { } public Optional getTagByName(TagName name) { - return stationTags.values().stream().filter(x -> x.getTagName().equals(name)).findFirst(); + for (StationTag tag : stationTags.values()) { + if (tag.getTagName().equals(name)) { + return Optional.ofNullable(tag); + } + } + return Optional.empty(); } public Optional getStationTag(UUID id) { @@ -329,8 +349,8 @@ public StationTag removeStationTag(UUID id) { return stationTags.remove(id); } - public ImmutableList getAllStationTags() { - return ImmutableList.copyOf(stationTags.values()); + public List getAllStationTags() { + return new ArrayList<>(stationTags.values()); } //#endregion @@ -367,20 +387,26 @@ public ImmutableList getAllTrainGroups() { return ImmutableList.copyOf(trainGroups.values()); } - public boolean isTrainExcludedByUser(Train train, UserSettings settings) { - return !TrainListener.data.get(train.id).getSections().isEmpty() && TrainListener.data.get(train.id).getSections().stream().allMatch(x -> !x.isUsable() || (x.getTrainGroup() != null && settings.navigationExcludedTrainGroups.getValue().contains(x.getTrainGroup().getGroupName()))); - //List groupsOfTrain = getTrainGroupsOfTrain(train); - //Set excludedGroups = settings.navigationExcludedTrainGroups.getValue(); - //return !groupsOfTrain.isEmpty() && !excludedGroups.isEmpty() && groupsOfTrain.stream().allMatch(a -> excludedGroups.stream().anyMatch(b -> a.getId().equals(b.getId()))); + public boolean isTrainExcludedByUser(Train train, UserSettings settings) { + if (TrainListener.data.get(train.id).getSections().isEmpty()) { + return false; + } + + for (TrainTravelSection section : TrainListener.data.get(train.id).getSections()) { + if (section.isUsable() && !(section.getTrainGroup().map(x -> settings.navigationExcludedTrainGroups.getValue().contains(x.getGroupName())).orElse(false))) { + return false; + } + } + return true; } public boolean isTrainStationExcludedByUser(Train train, TrainPrediction at, UserSettings settings) { - return at.getSection().getTrainGroup() != null && (!at.getSection().isUsable() || (at.getSection().getTrainGroup() != null && settings.navigationExcludedTrainGroups.getValue().contains(at.getSection().getTrainGroup().getGroupName()))); + return at.getSection().getTrainGroup().map(x -> !at.getSection().isUsable() || (settings.navigationExcludedTrainGroups.getValue().contains(x.getGroupName()))).orElse(false); } public boolean isTrainStationExcludedByUser(Train train, TrainStop at, UserSettings settings) { TrainTravelSection section = TrainListener.data.get(train.id).getSectionByIndex(at.getSectionIndex()); - return section.getTrainGroup() != null && (!section.isUsable() || (section.getTrainGroup() != null && settings.navigationExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))); + return section.getTrainGroup().map(x -> !section.isUsable() || (settings.navigationExcludedTrainGroups.getValue().contains(x.getGroupName()))).orElse(false); } //#endregion @@ -414,7 +440,14 @@ public boolean isEntireStationTagBlacklisted(StationTag tag) { if (tag == null) { return true; } - return tag.getAllStationNames().stream().allMatch(x -> isStationBlacklisted(x)); + + Collection names = tag.getAllStationNames(); + for (String name : names) { + if (!isStationBlacklisted(name)) { + return false; + } + } + return !names.isEmpty(); } public ImmutableList getAllBlacklistedStations() { @@ -448,15 +481,6 @@ public boolean removeTrainFromBlacklist(String trainName) { return trainBlacklist.removeIf(x -> x.equals(trainName)); } - /* TODO - public boolean isEntireTrainGroupBlacklisted(TrainGroup tag) { - if (tag == null) { - return true; - } - return tag.getTrainNames().stream().allMatch(x -> isTrainBlacklisted(x)); - } - */ - public ImmutableList getAllBlacklistedTrains() { return ImmutableList.copyOf(trainBlacklist); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/StationDepartureHistory.java b/common/src/main/java/de/mrjulsen/crn/data/train/StationDepartureHistory.java new file mode 100644 index 00000000..d78140dc --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/train/StationDepartureHistory.java @@ -0,0 +1,369 @@ +package de.mrjulsen.crn.data.train; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; + +import com.simibubi.create.content.trains.entity.Train; +import com.simibubi.create.content.trains.station.GlobalStation; + +import de.mrjulsen.crn.data.TrainGroup; +import de.mrjulsen.crn.data.TrainLine; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; +import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; +import de.mrjulsen.mcdragonlib.data.MapCache; +import net.minecraft.nbt.CompoundTag; + +public final class StationDepartureHistory { + + private StationDepartureHistory() {} + + public static final ConcurrentHashMap trainDepartures = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> departureInputKeyByStation = new ConcurrentHashMap<>(); + + + public static String debug_departureHistory() { + long a = trainDepartures.size(); + long b = trainDepartures.values().stream().mapToLong(x -> x.debug_cachedDataCount()).sum(); + long c = departureInputKeyByStation.size(); + long d = departureInputKeyByStation.values().stream().mapToLong(x -> x.size()).sum(); + long e = departureDataCache.getCachedDataCount(); + long f = lastDepartureTimeDataCache.getCachedDataCount(); + + return String.format("DH: [%s,%s]/[%s,%s]/[%s,%s]", a, b, c, d, e, f); + } + + private static record DepartureTimeInputDataKey(ETrainFilter filter, UUID train, String stationName) { + @Override + public final int hashCode() { + return Objects.hash(filter.getIndex(), train, stationName); + } + @Override + public final boolean equals(Object a) { + if (a instanceof DepartureTimeInputDataKey o) { + return filter == o.filter && train.equals(o.train) && stationName.equals(o.stationName); + } + return false; + } + } + + private static final MapCache, String, String> departureDataCache = new MapCache<>((stationName) -> { + List departureData = new ArrayList<>(); + + if (stationName.contains("*")) { + String regex = stationName.isBlank() ? stationName : "\\Q" + stationName.replace("*", "\\E.*\\Q") + "\\E"; + for (Map.Entry e : trainDepartures.entrySet()) { + if (!e.getKey().matches(regex)) continue; + departureData.add(e.getValue()); + } + } else if (trainDepartures.containsKey(stationName)) { + departureData.add(trainDepartures.get(stationName)); + } + + return departureData; + }, String::hashCode, ECachingPriority.LOW); + + private static final MapCache lastDepartureTimeDataCache = new MapCache<>((key) -> { + long time = Long.MIN_VALUE; + TrainTravelSection section = null; + + if (TrainListener.data.containsKey(key.train())) { + section = TrainListener.data.get(key.train()).getCurrentSection(); + } + + List data = departureDataCache.get(key.stationName(), key.stationName()); + for (Data d : data) { + long newTime = d.getLastDepartureTime(key.filter(), section); + time = Math.max(newTime, time); + } + + return time; + }, DepartureTimeInputDataKey::hashCode, ECachingPriority.LOW); + + + public static synchronized long getLastMatchingDepartureTime(ETrainFilter filter, Train train, String stationName) { + DepartureTimeInputDataKey key = new DepartureTimeInputDataKey(filter, train.id, stationName); + departureInputKeyByStation.computeIfAbsent(stationName, x -> new HashSet<>()).add(key); + return lastDepartureTimeDataCache.get(key, key); + } + + public static synchronized List getAllDeparturesAt(String stationName) { + return departureDataCache.get(stationName, stationName); + } + + public static boolean hasDepartureHistory(String stationName) { + return trainDepartures.containsKey(stationName); + } + + public static synchronized void updateDepartureHistory(Train train, String stationName) { + if (train == null || stationName == null || stationName.isEmpty()) return; + trainDepartures.computeIfAbsent(stationName, x -> new Data()).setDeparture(train); + if (departureInputKeyByStation.containsKey(stationName)) { + Set keys = departureInputKeyByStation.remove(stationName); + for (DepartureTimeInputDataKey key : keys) { + lastDepartureTimeDataCache.clear(key); + } + } + departureDataCache.clear(stationName); + } + + public static synchronized void cleanUpDepartureHistory() { + Collection stations = TrainUtils.getAllStations(); + Collection stationNames = new ArrayList<>(stations.size()); + for (GlobalStation s : stations) { + stationNames.add(s.name); + } + if (trainDepartures.keySet().retainAll(stationNames)) { + lastDepartureTimeDataCache.clearAll(); + departureDataCache.clearAll(); + } + } + + public static synchronized void clearAll() { + trainDepartures.clear(); + departureInputKeyByStation.clear(); + departureDataCache.clearAll(); + lastDepartureTimeDataCache.clearAll(); + } + + + + public static class Data { + private long lastDepartureTime = Long.MIN_VALUE; + private Map lastDepartureByLine = new ConcurrentHashMap<>(); + private Map lastDepartureByGroup = new ConcurrentHashMap<>(); + + public void setDeparture(Train train) { + this.lastDepartureTime = DragonLib.getCurrentServer().get().overworld().getGameTime(); + if (TrainListener.data.containsKey(train.id)) { + TrainData trainData = TrainListener.data.get(train.id); + TrainTravelSection section = trainData.getCurrentSection(); + section.getTrainLine().ifPresent(x -> this.lastDepartureByLine.put(x, this.lastDepartureTime)); + section.getTrainGroup().ifPresent(x -> this.lastDepartureByGroup.put(x, this.lastDepartureTime)); + } + } + + public long getLastDepartureTime(ETrainFilter filter, @Nullable TrainTravelSection section) { + return switch (filter) { + case SAME_GROUP -> section != null ? section.getTrainGroup().map(x -> lastDepartureByGroup.getOrDefault(x, Long.MIN_VALUE)).orElse(Long.MIN_VALUE) : Long.MIN_VALUE; + case SAME_LINE -> section != null ? section.getTrainLine().map(x -> lastDepartureByLine.getOrDefault(x, Long.MIN_VALUE)).orElse(Long.MIN_VALUE) : Long.MIN_VALUE; + default -> lastDepartureTime; + }; + } + + public long getLastDepartureTime() { + return lastDepartureTime; + } + + public Map getLastDepartureByLine() { + return lastDepartureByLine; + } + + public Map getLastDepartureByGroup() { + return lastDepartureByGroup; + } + + public long debug_cachedDataCount() { + return 1 + lastDepartureByLine.size() + lastDepartureByGroup.size(); + } + } + + public static class StationStats { + + private static final String NBT_NAME = "Name"; + private static final String NBT_DEPARTURE_TIME = "LastDepartureTime"; + private static final String NBT_LINE = "Line"; + private static final String NBT_GROUP = "Group"; + private static final String NBT_LINE_COUNT = "LineCount"; + private static final String NBT_GROUP_COUNT = "GroupCount"; + + private final String stationName; + private final long lastDepartureTime; + + // On server + private final Map departuresByLine; + private final Map departuresByGroup; + + // On client + private final List> departuresListByLine; + private final List> departuresListByGroup; + private final int departuresByLineTotalCount; + private final int departuresByGroupTotalCount; + + public StationStats(String stationName) { + this.stationName = stationName; + List departures = StationDepartureHistory.getAllDeparturesAt(stationName); + long lastDepartureTime = Long.MIN_VALUE; + departuresByLine = new HashMap<>(); + departuresByGroup = new HashMap<>(); + for (StationDepartureHistory.Data data : departures) { + lastDepartureTime = Math.max(lastDepartureTime, data.getLastDepartureTime(ETrainFilter.ANY, null)); + for (Map.Entry line : data.getLastDepartureByLine().entrySet()) { + departuresByLine.merge(line.getKey().getLineName(), line.getValue(), (k, v) -> Math.max(v, line.getValue())); + } + for (Map.Entry line : data.getLastDepartureByGroup().entrySet()) { + departuresByGroup.merge(line.getKey().getGroupName(), line.getValue(), (k, v) -> Math.max(v, line.getValue())); + } + } + this.lastDepartureTime = lastDepartureTime; + this.departuresListByLine = null; + this.departuresListByGroup = null; + departuresByLineTotalCount = 0; + departuresByGroupTotalCount = 0; + } + + private StationStats(String stationName, long lastDepartureTime, List> departuresByLine, List> departuresByGroup, int linesCount, int groupsCount) { + this.stationName = stationName; + this.lastDepartureTime = lastDepartureTime; + this.departuresListByLine = departuresByLine; + this.departuresListByGroup = departuresByGroup; + this.departuresByLine = null; + this.departuresByGroup = null; + this.departuresByLineTotalCount = linesCount; + this.departuresByGroupTotalCount = groupsCount; + } + + public static StationStats empty() { + return new StationStats("", -1, null, null, 0, 0); + } + + public boolean isEmpty() { + return lastDepartureTime < 0 && !hasDeparturesByLine() && !hasDeparturesByGroup(); + } + + public boolean hasDeparturesByLine() { + return departuresListByLine != null && !departuresListByLine.isEmpty(); + } + + public boolean hasDeparturesByGroup() { + return departuresListByGroup != null && !departuresListByGroup.isEmpty(); + } + + public CompoundTag toNbt() { + CompoundTag nbt = new CompoundTag(); + nbt.putString(NBT_NAME, stationName); + nbt.putLong(NBT_DEPARTURE_TIME, lastDepartureTime); + + CompoundTag map1 = new CompoundTag(); + int i = 0; + for (Map.Entry e : departuresByLine.entrySet()) { + map1.putLong(e.getKey(), e.getValue()); + i++; + if (i > 5) break; + } + nbt.put(NBT_LINE, map1); + + CompoundTag map2 = new CompoundTag(); + i = 0; + for (Map.Entry e : departuresByGroup.entrySet()) { + map2.putLong(e.getKey(), e.getValue()); + i++; + if (i > 5) break; + } + nbt.put(NBT_GROUP, map2); + nbt.putInt(NBT_LINE_COUNT, departuresByLine.size()); + nbt.putInt(NBT_GROUP_COUNT, departuresByGroup.size()); + return nbt; + } + + public static StationStats fromNbt(CompoundTag nbt) { + Map departuresByLine = new HashMap<>(); + Map departuresByGroup = new HashMap<>(); + + CompoundTag map1 = nbt.getCompound(NBT_LINE); + for (String key : map1.getAllKeys()) { + departuresByLine.put(key, map1.getLong(key)); + } + + CompoundTag map2 = nbt.getCompound(NBT_GROUP); + for (String key : map2.getAllKeys()) { + departuresByGroup.put(key, map2.getLong(key)); + } + + List> sortedLines = new ArrayList<>(departuresByLine.entrySet()); + sortedLines.sort(Map.Entry.comparingByValue((a, b) -> Long.compare(a, b) * -1)); + List> sortedGroups = new ArrayList<>(departuresByGroup.entrySet()); + sortedGroups.sort(Map.Entry.comparingByValue((a, b) -> Long.compare(a, b) * -1)); + + return new StationStats( + nbt.getString(NBT_NAME), + nbt.getLong(NBT_DEPARTURE_TIME), + sortedLines, + sortedGroups, + nbt.getInt(NBT_LINE_COUNT), + nbt.getInt(NBT_GROUP_COUNT) + ); + } + + public String getStationName() { + return stationName; + } + + public long getLastDepartureTime() { + return lastDepartureTime; + } + + public List> getDeparturesByLine() { + return departuresListByLine; + } + + public List> getDeparturesByGroup() { + return departuresListByGroup; + } + + public int getDeparturesByLineTotalCount() { + return departuresByLineTotalCount; + } + + public int getDeparturesByGroupTotalCount() { + return departuresByGroupTotalCount; + } + } + + + public static enum ETrainFilter implements ITranslatableEnum { + ANY((byte)0, "any"), + SAME_LINE((byte)1, "same_line"), + SAME_GROUP((byte)2, "same_group"); + + final byte index; + final String name; + + ETrainFilter(byte index, String name) { + this.index = index; + this.name = name; + } + + public byte getIndex() { + return index; + } + + public String getName() { + return name; + } + + public static ETrainFilter getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(ANY); + } + + @Override + public String getEnumName() { + return "train_filter"; + } + + @Override + public String getEnumValueName() { + return name; + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java index 48ff4576..3cd094b2 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java @@ -8,7 +8,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.Map.Entry; import java.util.Map; import java.util.ArrayList; @@ -16,7 +15,6 @@ import java.util.HashSet; import java.util.IdentityHashMap; -import com.google.common.collect.ImmutableMap; import com.simibubi.create.content.trains.display.GlobalTrainDisplayData.TrainDeparturePrediction; import com.simibubi.create.content.trains.entity.Train; @@ -25,13 +23,14 @@ import de.mrjulsen.crn.util.ModUtils; import de.mrjulsen.crn.event.CRNEventsManager; import de.mrjulsen.crn.event.events.TotalDurationTimeChangedEvent; +import de.mrjulsen.crn.mixin.ScheduleRuntimeAccessor; import de.mrjulsen.crn.data.TrainInfo; import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; -import de.mrjulsen.crn.data.train.TrainStatus.CompiledTrainStatus; import de.mrjulsen.crn.data.train.TrainStatus.TrainStatusType; import de.mrjulsen.crn.util.IListenable; import de.mrjulsen.crn.util.LockedList; import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; import de.mrjulsen.mcdragonlib.data.Cache; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; @@ -65,11 +64,11 @@ public class TrainData implements IListenable { private UUID sessionId; private final ConcurrentHashMap predictionsByIndex = new ConcurrentHashMap<>(); private transient final ConcurrentHashMap sectionsByIndex = new ConcurrentHashMap<>(); - private transient final Cache defaultSection = new Cache<>(() -> TrainTravelSection.def(this)); + private transient final Cache defaultSection = new Cache<>(() -> TrainTravelSection.def(this), ECachingPriority.LOW); private transient final List predictionsChronologically = new LockedList<>(); private transient final Set validPredictionEntries = new HashSet<>(); private transient final Cache isDynamic = new Cache<>(() -> getTrain().runtime.getSchedule().entries.stream().anyMatch(x -> x.conditions.stream().flatMap(y -> y.stream()).anyMatch(y -> y instanceof DynamicDelayCondition c && c.minWaitTicks() < c.totalWaitTicks()))); - + private int currentScheduleIndex = INVALID; private transient int currentTravelSectionIndex = INVALID; private transient int lastScheduleIndex = INVALID; @@ -114,6 +113,47 @@ public class TrainData implements IListenable { // temp mem private boolean sectionChanged; private boolean destinationChanged; + + // Cache + /* + private transient final Cache> statusCache = new Cache<>(() -> { + Set status = new HashSet<>(currentStatusInfos.size()); + for (ResourceLocation loc : currentStatusInfos) { + status.add(TrainStatus.Registry.getRegisteredStatus().get(loc).compile()); + } + return status; + }, ECachingPriority.LOW); + */ + private final Cache isDelayedCache = new Cache<>(() -> { + for (TrainPrediction pred : predictionsChronologically) { + if (pred.isAnyDelayed()) { + return true; + } + } + return false; + }); + private final Cache highestDeviationCache = new Cache<>(() -> { + long max = 0; + for (TrainPrediction pred : predictionsByIndex.values()) { + long m = Math.max(pred.getArrivalTimeDeviation(), pred.getDepartureTimeDeviation()); + if (m > max) { + max = m; + } + } + return max; + }); + private final Cache currentSectionCache = new Cache<>(() -> { + return currentTravelSectionIndex < 0 || !hasCustomTravelSections() || !sectionsByIndex.containsKey(currentTravelSectionIndex) ? + defaultSection.get() : + sectionsByIndex.get(currentTravelSectionIndex) + ; + }); + private final Cache> sectionsCache = new Cache<>(() -> { + return sectionsByIndex.isEmpty() ? + List.of(defaultSection.get()) : + sectionsByIndex.values().stream().sorted((a, b) -> Integer.compare(a.getScheduleIndex(), b.getScheduleIndex())).toList() + ; + }); /* PLEASE NOTE! * Chronologically update order (once every ~5 seconds): @@ -160,7 +200,7 @@ public Train getTrain() { } public TrainInfo getTrainInfo(int scheduleIndex) { - return new TrainInfo(getSectionForIndex(scheduleIndex).getTrainLine(), getSectionForIndex(scheduleIndex).getTrainGroup()); + return new TrainInfo(getSectionForIndex(scheduleIndex).getTrainLine().orElse(null), getSectionForIndex(scheduleIndex).getTrainGroup().orElse(null)); } /** @@ -206,16 +246,13 @@ public boolean isWaitingAtStation() { return isAtStation; } - public TrainTravelSection getCurrentTravelSection() { - return currentTravelSectionIndex < 0 || !sectionsByIndex.containsKey(currentTravelSectionIndex) ? defaultSection.get() : sectionsByIndex.get(currentTravelSectionIndex); - } - public TrainTravelSection getSectionByIndex(int scheduleIndex) { return sectionsByIndex.isEmpty() ? defaultSection.get() : sectionsByIndex.get(scheduleIndex); } public void addTravelSection(TrainTravelSection section) { this.sectionsByIndex.put(section.getScheduleIndex(), section); + sectionsCache.clear(); } public String getCurrentTitle() { @@ -225,6 +262,10 @@ public String getCurrentTitle() { public String getTrainName() { return train.name.getString(); } + + public String getTrainDisplayName() { + return getCurrentSection() == null || getCurrentSection().getTrainLine().map(x -> x.getLineName().isEmpty()).orElse(true) ? getTrainName() : getCurrentSection().getTrainLine().get().getLineName(); + } public int getCurrentScheduleIndex() { return currentScheduleIndex; @@ -239,7 +280,7 @@ public boolean isSingleSection() { } public List getSections() { - return sectionsByIndex.isEmpty() ? List.of(defaultSection.get()) : sectionsByIndex.values().stream().sorted((a, b) -> Integer.compare(a.getScheduleIndex(), b.getScheduleIndex())).toList(); + return sectionsCache.get(); } public TrainTravelSection getSectionForIndex(int anyIndex) { @@ -273,12 +314,14 @@ public synchronized Optional getNextStopPrediction() { } public void resetPredictions() { - predictionsByIndex.values().stream().forEach(x -> x.reset()); + for (TrainPrediction pred : predictionsByIndex.values()) { + pred.reset(); + } lastSectionDelayOffset = 0; refreshTimingsCounter = 0; resetStatus(true); isDynamic.clear(); - if (CreateRailwaysNavigator.isDebug()) CreateRailwaysNavigator.LOGGER.info(getTrainName() + " has reset their scheduled times."); + if (CreateRailwaysNavigator.isDebug() || ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info(getTrainName() + " has reset their scheduled times."); } public void hardResetPredictions() { @@ -286,7 +329,7 @@ public void hardResetPredictions() { } public synchronized boolean isDelayed() { - return predictionsChronologically.stream().anyMatch(TrainPrediction::isAnyDelayed); + return isDelayedCache.get(); } public boolean isCurrentSectionDelayed() { @@ -294,7 +337,7 @@ public boolean isCurrentSectionDelayed() { } public long getHighestDeviation() { - return predictionsByIndex.values().stream().mapToLong(x -> Math.max(x.getArrivalTimeDeviation(), x.getDepartureTimeDeviation())).max().orElse(0); + return highestDeviationCache.get(); } public long getDeviationDelayOffset() { @@ -302,16 +345,23 @@ public long getDeviationDelayOffset() { } public TrainTravelSection getCurrentSection() { - return currentTravelSectionIndex < 0 || !hasCustomTravelSections() || !sectionsByIndex.containsKey(currentTravelSectionIndex) ? TrainTravelSection.def(this) : sectionsByIndex.get(currentTravelSectionIndex); + return currentSectionCache.get(); } public Map getWaitingForSignalsTime() { - return ImmutableMap.copyOf(delaysBySignal); + return new HashMap<>(delaysBySignal); + } + + public Set getStatus() { + return currentStatusInfos; } + /* + public Set getStatus() { - return currentStatusInfos.stream().map(x -> TrainStatus.Registry.getRegisteredStatus().get(x).compile(this)).collect(Collectors.toSet()); + return statusCache.get(); } + */ public int debug_statusInfoCount() { return currentStatusInfos.size(); @@ -337,8 +387,19 @@ public void applyStatus() { } } - - if (currentStatusInfos.stream().noneMatch(x -> TrainStatus.Registry.getRegisteredStatus().get(x).getImportance() == TrainStatusType.DELAY && !x.equals(TrainStatus.DEFAULT_DELAY.getLocation())) && isCurrentSectionDelayed()) { + boolean unknownDelayReason = isCurrentSectionDelayed(); + if (unknownDelayReason) { + for (ResourceLocation loc : currentStatusInfos) { + if (TrainStatus.Registry.getRegisteredStatus().get(loc).getImportance() == TrainStatusType.DELAY && + !loc.equals(TrainStatus.DEFAULT_DELAY.getLocation()) + ) { + unknownDelayReason = false; + break; + } + } + } + + if (unknownDelayReason) { currentStatusInfos.add(TrainStatus.DEFAULT_DELAY.getLocation()); } else { currentStatusInfos.remove(TrainStatus.DEFAULT_DELAY.getLocation()); @@ -354,9 +415,15 @@ public boolean hasSectionChanged() { * Trains that have not yet been initialized do not yet contain any reliable data to make any predictions. */ public boolean isInitialized() { - return !currentTransitTime.isEmpty() && - currentTransitTime.values().stream().noneMatch(x -> x < 0) - ; + if (currentTransitTime.isEmpty()) { + return false; + } + for (int i : currentTransitTime.values()) { + if (i < 0) { + return false; + } + } + return true; } public int debug_initializedStationsCount() { @@ -378,7 +445,7 @@ public synchronized TrainPrediction setPredictionData(int entryIndex, int curren // Update CRN predictions with data from Create TrainPrediction pred = predictionsByIndex.computeIfAbsent(entryIndex, i -> new TrainPrediction(this, entryIndex, predictionData, stayDuration, minStayDuration)); - currentTransitTime.computeIfAbsent(entryIndex, x -> -1); + currentTransitTime.computeIfAbsent(entryIndex, x -> ModCommonConfig.USE_CREATE_TRANSIT_TIMES_ON_INIT.get() ? ((ScheduleRuntimeAccessor)train.runtime).crn$predictionTicks().get(entryIndex) : INVALID); validPredictionEntries.add(entryIndex); pred.updateRealTime( @@ -394,6 +461,7 @@ public void changeCurrentSection(int sectionEntryIndex) { sectionChanged = true; lastSectionDelayOffset = Math.max(0, getHighestDeviation()); this.refreshTimingsCounter++; + currentSectionCache.clear(); } private void clearAll() { @@ -408,6 +476,9 @@ private void clearAll() { currentTransitTime.clear(); lastScheduleIndex = INVALID; hasStarted = false; + + sectionsCache.clear(); + resetCaches(); } /** Called every ~5 seconds */ @@ -429,7 +500,7 @@ public synchronized void refreshPre() { public synchronized void refreshPost() { // [] Remove invalid prediction data - if (hasStarted && !train.runtime.paused) { + if (/*TODO hasStarted && */!train.runtime.paused) { predictionsByIndex.keySet().retainAll(validPredictionEntries); measuredTransitTimes.keySet().retainAll(validPredictionEntries); transitTimeHistory.keySet().retainAll(validPredictionEntries); @@ -467,6 +538,13 @@ public synchronized void refreshPost() { initializationFinishTask = false; onInitialize(); } + + resetCaches(); + } + + private void resetCaches() { + isDelayedCache.clear(); + highestDeviationCache.clear(); } /** Called every tick */ @@ -668,5 +746,6 @@ protected void deserializeNbt(CompoundTag nbt) { public synchronized void shiftTime(long l) { this.destinationReachTime += l; predictionsByIndex.values().forEach(x -> x.shiftTime(l)); - } + } + } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java index 54da4195..efdf7881 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java @@ -3,15 +3,18 @@ import java.io.File; import java.io.IOException; import java.util.UUID; +import java.util.HashSet; +import java.util.Iterator; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; + import java.util.Set; import com.simibubi.create.content.trains.entity.Train; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.schedule.INavigationExtension; import de.mrjulsen.crn.data.storage.GlobalSettings; import de.mrjulsen.crn.event.CRNEventsManager; import de.mrjulsen.crn.event.ModCommonEvents; @@ -31,7 +34,7 @@ /** Monitors all trains in the world and processes their data and information to make it available for use. */ public final class TrainListener { - + private transient static final String FILENAME = CreateRailwaysNavigator.MOD_ID + "_train_data.nbt"; public static final ConcurrentHashMap data = new ConcurrentHashMap<>(); @@ -44,10 +47,14 @@ public final class TrainListener { public static void init() { // Register Event Listeners CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPre.class).register(CreateRailwaysNavigator.MOD_ID, () -> { - queueTrainListenerTask(TrainListener::refreshPre); + queueTrainListenerTask(() -> { + StationDepartureHistory.cleanUpDepartureHistory(); + TrainListener.refreshPre(); + }); }); CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPost.class).register(CreateRailwaysNavigator.MOD_ID, () -> { + TrainUtils.refreshCache(); queueTrainListenerTask(TrainListener::refreshPost); }); @@ -67,6 +74,11 @@ public static void init() { data.get(train.id).leaveDestination(); } } + + if (!isArrival && !((INavigationExtension)(Object)train.navigation).isDelayedWaitConditionPending()) { + // If not checking whether a delayed condition is pending, the train would block itself. + StationDepartureHistory.updateDepartureHistory(train, station.name); + } }); }); @@ -101,18 +113,30 @@ public static void init() { } public static Set getAllTrains() { - return data.values().stream().map(x -> x.getTrain()).collect(Collectors.toSet()); + Set result = new HashSet<>(data.size()); + for (TrainData v : data.values()) { + result.add(v.getTrain()); + } + return result; } public static boolean allTrainsInitialized() { - return data.values().stream().filter(x -> - !GlobalSettings.getInstance().isTrainBlacklisted(x.getTrain()) && - !x.getPredictionsRaw().isEmpty() && - !x.getTrain().runtime.paused && - !x.getTrain().derailed && - !x.getTrain().runtime.completed && - TrainUtils.isTrainValid(x.getTrain()) - ).allMatch(x -> x.isInitialized() && !x.isPreparing()); + for (TrainData data : data.values()) { + if (GlobalSettings.getInstance().isTrainBlacklisted(data.getTrain()) || + data.getPredictionsRaw().isEmpty() || + data.getTrain().runtime.paused || + data.getTrain().derailed || + data.getTrain().runtime.completed || + !TrainUtils.isTrainValid(data.getTrain()) + ) { + continue; + } + + if (!data.isInitialized() || data.isPreparing()) { + return false; + } + } + return true; } public static void start() { @@ -207,19 +231,29 @@ private static void queueTrainListenerTask(Runnable task) { public synchronized static void refreshPre() { if (!trainDataListenerActive) return; Set trains = TrainUtils.getTrains(true); - data.keySet().retainAll(trains.stream().filter(x -> !GlobalSettings.getInstance().isTrainBlacklisted(x)).map(x -> x.id).toList()); - trains.forEach(x -> { - data.computeIfAbsent(x.id, a -> TrainData.of(x)).refreshPre(); - }); + Iterator iterator = trains.iterator(); + while (iterator.hasNext()) { + Train train = iterator.next(); + if (GlobalSettings.getInstance().isTrainBlacklisted(train)) { + iterator.remove(); + data.remove(train.id); + continue; + } + data.computeIfAbsent(train.id, x -> TrainData.of(train)).refreshPre(); + } } public synchronized static void refreshPost() { if (!trainDataListenerActive) return; - data.values().forEach(x -> x.refreshPost()); + for (TrainData train : data.values()) { + train.refreshPost(); + } } public synchronized static void tick() { if (!trainDataListenerActive) return; - data.values().forEach(x -> x.tick()); + for (TrainData train : data.values()) { + train.tick(); + } } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java index 5ebd35bb..d5edf1a1 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainPrediction.java @@ -75,6 +75,7 @@ public class TrainPrediction implements Comparable { TrainTravelSection section = getSection(); return section.isFinalStop(this); }); + private final Cache tagCache = new Cache<>(() -> GlobalSettings.getInstance().getOrCreateStationTagFor(stationName)); private final Cache section; private TrainPrediction(TrainData data, int entryIndex, String stationName, String title, int ticks, int stayDuration, int minStayDuration) { @@ -251,19 +252,19 @@ public long getBufferTimeLeft() { } public long getScheduledArrivalDay() { - return getScheduledArrivalTime() / DragonLib.TICKS_PER_DAY; + return getScheduledArrivalTime() / DragonLib.ticksPerDay(); } public long getScheduledDepartureDay() { - return getScheduledDepartureDay() / DragonLib.TICKS_PER_DAY; + return getScheduledDepartureDay() / DragonLib.ticksPerDay(); } public long getRealTimeArrivalDay() { - return getRealTimeArrivalTime() / DragonLib.TICKS_PER_DAY; + return getRealTimeArrivalTime() / DragonLib.ticksPerDay(); } public long getRealTimeDepartureDay() { - return getRealTimeDepartureTime() / DragonLib.TICKS_PER_DAY; + return getRealTimeDepartureTime() / DragonLib.ticksPerDay(); } public void setStopovers(List stopovers) { @@ -341,7 +342,7 @@ public StationTag getStationTag() throws RuntimeSideException { if (!ModCommonEvents.hasServer()) { throw new RuntimeSideException(false); } - return GlobalSettings.getInstance().getOrCreateStationTagFor(stationName); + return tagCache.get(); } public TrainTravelSection getSection() { @@ -392,6 +393,11 @@ public void updateRealTime(String stationName, int realTimeTicks) { } this.arrivalTicksCorrection += Math.min(tempArrivalCorrection, getArrivalTimeDeviation()); this.departureTicksCorrection += Math.min(tempDepartureCorrection, getArrivalTimeDeviation()); + resetAllTimedCaches(); + } + + private void resetAllTimedCaches() { + tagCache.clear(); } @Override @@ -427,7 +433,10 @@ public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); ListTag stopovers = new ListTag(); - stopovers.addAll(this.stopovers.stream().map(x -> StringTag.valueOf(x)).toList()); + List stops = this.stopovers; + for (String s : stops) { + stopovers.add(StringTag.valueOf(s)); + } nbt.putInt(NBT_ENTRY_INDEX, entryIndex); nbt.putString(NBT_STATION_NAME, stationName == null ? "" : stationName); diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java index 0c19d13d..935543ac 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStatus.java @@ -1,10 +1,13 @@ package de.mrjulsen.crn.data.train; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import com.google.common.collect.ImmutableMap; @@ -12,41 +15,43 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.ClientWrapper; import de.mrjulsen.crn.client.gui.ModGuiIcons; +import de.mrjulsen.crn.client.lang.CustomLanguage; +import de.mrjulsen.crn.exceptions.RuntimeSideException; import de.mrjulsen.mcdragonlib.client.util.Graphics; import de.mrjulsen.mcdragonlib.client.util.GuiUtils; import de.mrjulsen.mcdragonlib.data.Single; -import de.mrjulsen.mcdragonlib.util.TextUtils; +import dev.architectury.platform.Platform; +import net.fabricmc.api.EnvType; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.Font; -import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; public class TrainStatus { - public static final MutableComponent textOperationalDisruption = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.operational_disruption"); // Betriebsstörung - public static final MutableComponent textTooFewTracks = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.too_few_tracks"); // Verfügbarkeit der Gleise eingeschränkt - public static final MutableComponent textOperationalStabilization = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.operational_stabilization"); // Betriebsstabilisierung - public static final MutableComponent textStaffShortage = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.staff_shortage"); // Kurzfristiger Personalausfall - public static final MutableComponent textTrackClosed = TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.track_closed"); + //public static final MutableComponent textOperationalDisruption = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.operational_disruption"); // Betriebsstörung + //public static final MutableComponent textTooFewTracks = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.too_few_tracks"); // Verfügbarkeit der Gleise eingeschränkt + //public static final MutableComponent textOperationalStabilization = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.operational_stabilization"); // Betriebsstabilisierung + //public static final MutableComponent textStaffShortage = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.staff_shortage"); // Kurzfristiger Personalausfall + //public static final MutableComponent textTrackClosed = ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".delay_reason.track_closed"); private static final Registry REGISTRY = Registry.create(CreateRailwaysNavigator.MOD_ID); - public static final TrainStatus DEFAULT_DELAY = REGISTRY.registerDefault("default_delay", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.unknown_delay"), null)); - public static final TrainStatus DELAY_FROM_PREVIOUS_JOURNEY = REGISTRY.registerDefault("delay_from_previous_journey", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.delay_previous_journey"), null)); - public static final TrainStatus CANCELLED = REGISTRY.registerDefault("cancelled", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> TextUtils.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.cancelled"), null)); + public static final TrainStatus DEFAULT_DELAY = REGISTRY.registerDefault("default_delay", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.unknown_delay"), null)); + public static final TrainStatus DELAY_FROM_PREVIOUS_JOURNEY = REGISTRY.registerDefault("delay_from_previous_journey", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.delay_previous_journey"), null)); + public static final TrainStatus CANCELLED = REGISTRY.registerDefault("cancelled", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".route_overview.cancelled"), null)); public static final int HEIGHT = 9; private final TrainStatusType importance; private final TrainStatusCategory category; - private final Function text; + private final Supplier text; //private final Function reason; private final Predicate trigger; private ResourceLocation location; - public TrainStatus(TrainStatusCategory category, TrainStatusType importance, Function text, /*Function reason,*/ Predicate trigger) { + public TrainStatus(TrainStatusCategory category, TrainStatusType importance, Supplier text, /*Function reason,*/ Predicate trigger) { this.importance = importance; this.category = category; this.text = text; @@ -58,8 +63,8 @@ public TrainStatusType getImportance() { return importance; } - public MutableComponent getText(TrainData data) { - return text.apply(data); + public MutableComponent getText() { + return text.get(); } /* @@ -72,8 +77,8 @@ public boolean isTriggerd(TrainData data) { return trigger != null && trigger.test(data); } - public CompiledTrainStatus compile(TrainData data) { - return new CompiledTrainStatus(category, importance, getText(data));//, getReason(data)); + public CompiledTrainStatus compile() { + return new CompiledTrainStatus(location, category, importance, getText());//, getReason(data)); } public ResourceLocation getLocation() { @@ -86,13 +91,14 @@ private void setLocation(ResourceLocation location) { } - public static record CompiledTrainStatus(TrainStatusCategory category, TrainStatusType type, Component text/*, Component reason*/) { + public static record CompiledTrainStatus(ResourceLocation id, TrainStatusCategory category, TrainStatusType type, Component text/*, Component reason*/) { public static final String NBT_CATEGORY = "Category"; public static final String NBT_TYPE = "Type"; public static final String NBT_TEXT = "Text"; public static final String NBT_REASON = "Reason"; + /* public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); nbt.putByte(NBT_CATEGORY, category().getIndex()); @@ -110,6 +116,7 @@ public static CompiledTrainStatus fromNbt(CompoundTag nbt) { //TextUtils.text(nbt.getString(NBT_REASON)) ); } + */ public int render(Graphics graphics, Single font, int x, int y, int maxWidth) { final int color = type().getColor(); @@ -124,6 +131,26 @@ public int render(Graphics graphics, Single font, int x, int y, int maxWid return Math.max(HEIGHT, height + 2); } + + /** Client-side only! */ + public static List load(Collection ids) throws RuntimeSideException { + if (Platform.getEnv() == EnvType.SERVER) { + throw new RuntimeSideException(true); + } + List status = new ArrayList<>(ids.size()); + for (ResourceLocation loc : ids) { + status.add(TrainStatus.Registry.getRegisteredStatus().get(loc).compile()); + } + return status; + } + + /** Client-side only! */ + public static CompiledTrainStatus load(ResourceLocation id) throws RuntimeSideException { + if (Platform.getEnv() == EnvType.SERVER) { + throw new RuntimeSideException(true); + } + return TrainStatus.Registry.getRegisteredStatus().get(id).compile(); + } } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java index 907a6e9e..4b505894 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainStop.java @@ -312,19 +312,19 @@ public int getTicksUntilArrival() { } public long getScheduledArrivalDay() { - return getScheduledArrivalTime() / DragonLib.TICKS_PER_DAY; + return getScheduledArrivalTime() / DragonLib.ticksPerDay(); } public long getScheduledDepartureDay() { - return getScheduledDepartureDay() / DragonLib.TICKS_PER_DAY; + return getScheduledDepartureDay() / DragonLib.ticksPerDay(); } public long getRealTimeArrivalDay() { - return getRealTimeArrivalTime() / DragonLib.TICKS_PER_DAY; + return getRealTimeArrivalTime() / DragonLib.ticksPerDay(); } public long getRealTimeDepartureDay() { - return getRealTimeDepartureTime() / DragonLib.TICKS_PER_DAY; + return getRealTimeDepartureTime() / DragonLib.ticksPerDay(); } /** diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java index 6c2a6f25..4fe2ec69 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainTravelSection.java @@ -1,13 +1,13 @@ package de.mrjulsen.crn.data.train; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.StationTag; import de.mrjulsen.crn.data.TrainGroup; import de.mrjulsen.crn.data.TrainLine; @@ -28,6 +28,14 @@ public class TrainTravelSection { private final TrainLine trainLine; private final Cache> predictions = new Cache<>(() -> getPredictions(INVALID, false)); + private final Cache> stopoversCache = new Cache<>(() -> { + List predictions = this.predictions.get(); + List result = new ArrayList<>(predictions.size() - 2); + for (int i = 1; i < predictions.size() - 1; i++) { + result.add(predictions.get(i).getStationTag().getTagName().get()); + } + return result; + }); private final Cache nextSection; private final Cache previousSection; @@ -103,12 +111,12 @@ public boolean isUsable() { return usable; } - public TrainGroup getTrainGroup() { - return trainGroup; + public Optional getTrainGroup() { + return Optional.ofNullable(trainGroup); } - public TrainLine getTrainLine() { - return trainLine; + public Optional getTrainLine() { + return Optional.ofNullable(trainLine); } public TrainTravelSection nextSection() { @@ -130,7 +138,16 @@ public List getPredictions(int startingAtIndex, boolean ignoreI } List result = new ArrayList<>(); TrainTravelSection nextSection = nextSection(); - Map predictions = data.getPredictionsRaw().entrySet().stream().filter(x -> !GlobalSettings.getInstance().isStationBlacklisted(x.getValue().getStationName())).collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); + + Map predictionsSrc = data.getPredictionsRaw(); + Map predictions = new HashMap<>(predictionsSrc.size()); + for (Map.Entry prediction : predictionsSrc.entrySet()) { + if (GlobalSettings.getInstance().isStationBlacklisted(prediction.getValue().getStationName())) { + continue; + } + predictions.put(prediction.getKey(), prediction.getValue()); + } + final int startIndex = getScheduleIndex(); final int stopIndex = nextSection.getScheduleIndex(); final int count = data.getTrain().runtime.getSchedule().entries.size(); @@ -155,7 +172,13 @@ public List getPredictions(int startingAtIndex, boolean ignoreI } public int getFirstIndexFor(StationTag tag) { - return getPredictions(INVALID, false).stream().filter(x -> x.getStationTag().equals(tag)).map(x -> x.getEntryIndex()).findFirst().orElse(0); + List predictions = getPredictions(INVALID, false); + for (TrainPrediction p : predictions) { + if (p.getStationTag().equals(tag)) { + return p.getEntryIndex(); + } + } + return 0; } /** @@ -186,8 +209,7 @@ public List getAllStops(long simulationTime, int currentIndex) { } public List getStopovers() { - List predictions = this.predictions.get(); - return predictions.stream().limit(predictions.size() - 1).skip(1).map(x -> x.getStationTag().getTagName().get()).toList(); /* TODO */ + return stopoversCache.get(); } public List getStopoversFrom(int startIndex) { @@ -240,13 +262,13 @@ public Optional getNextStop() { public String getDisplayText() { if (!isUsable()) { - return ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service").getString(); + return CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service").getString(); } return getFinalStop().map(x -> GlobalSettings.getInstance().getOrCreateStationTagFor(x.getStationName()).getTagName().get()).orElse("?"); } public String getDisplayTextStart() { - return !isUsable() ? ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service").getString() : getFirstStop().map(x -> GlobalSettings.getInstance().getOrCreateStationTagFor(x.getStationName()).getTagName().get()).orElse("?"); + return !isUsable() ? CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service").getString() : getFirstStop().map(x -> GlobalSettings.getInstance().getOrCreateStationTagFor(x.getStationName()).getTagName().get()).orElse("?"); } public String getStartStationName() { diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java index a6936c5c..238a299b 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainUtils.java @@ -2,17 +2,17 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Predicate; -import java.util.stream.Collectors; -import com.google.common.collect.ImmutableMap; import com.simibubi.create.Create; import com.simibubi.create.content.decoration.slidingDoor.DoorControlBehaviour; import com.simibubi.create.content.trains.GlobalRailwayManager; @@ -21,6 +21,7 @@ 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.signal.SignalBoundary; import com.simibubi.create.content.trains.signal.TrackEdgePoint; import com.simibubi.create.content.trains.station.GlobalStation; @@ -33,6 +34,9 @@ import de.mrjulsen.crn.data.storage.GlobalSettings; import de.mrjulsen.crn.event.ModCommonEvents; import de.mrjulsen.crn.data.navigation.TrainSchedule; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; +import de.mrjulsen.mcdragonlib.data.Cache; +import de.mrjulsen.mcdragonlib.data.MapCache; import de.mrjulsen.mcdragonlib.data.Single.MutableSingle; import de.mrjulsen.mcdragonlib.util.MathUtils; import net.minecraft.core.BlockPos; @@ -42,6 +46,82 @@ import net.minecraft.world.phys.Vec3; public final class TrainUtils { + + private static final Cache> allStationsCache = new Cache<>(() -> { + final Collection stations = new ArrayList<>(); + getRailwayManager().trackNetworks.forEach((uuid, graph) -> { + Collection foundStations = graph.getPoints(EdgePointType.STATION); + stations.addAll(foundStations); + }); + return stations; + }, ECachingPriority.LOWEST); + + private static final Cache> allSignalsCache = new Cache<>(() -> { + Set signals = new HashSet<>(); + for (TrackGraph graph : getRailwayManager().trackNetworks.values()) { + signals.addAll(graph.getPoints(EdgePointType.SIGNAL)); + } + return signals; + }, ECachingPriority.LOWEST); + + private static final MapCache, StationTag, StationTag> departingTrainsAtTagCache = new MapCache<>((station) -> { + Set trains = new HashSet<>(); + for (Map.Entry> e : GlobalTrainDisplayData.statusByDestination.entrySet()) { + if (!station.contains(e.getKey())) { + continue; + } + + for (TrainDeparturePrediction pred : e.getValue()) { + trains.add(pred.train); + } + } + return trains; + }, StationTag::hashCode, ECachingPriority.LOWEST); + + private static final MapCache, String, String> departingTrainsAtStationCache = new MapCache<>((station) -> { + Set trains = new HashSet<>(); + for (Map.Entry> e : GlobalTrainDisplayData.statusByDestination.entrySet()) { + if (!station.equals(e.getKey())) { + continue; + } + + for (TrainDeparturePrediction pred : e.getValue()) { + trains.add(pred.train); + } + } + return trains; + }, String::hashCode, ECachingPriority.LOWEST); + + private static record DeparturesFromTagContext(StationTag station, UUID selfTrain) { + @Override + public final int hashCode() { + return Objects.hash(station, selfTrain); + } + } + private static final MapCache, DeparturesFromTagContext, DeparturesFromTagContext> departuresAtTagCache = new MapCache<>((context) -> { + return getDeparturesAt(x -> x.getStationTag().equals(context.station()), context.selfTrain()); + }, DeparturesFromTagContext::hashCode, ECachingPriority.LOWEST); + + private static record DeparturesFromStationContext(String station, UUID selfTrain) { + @Override + public final int hashCode() { + return Objects.hash(station, selfTrain); + } + } + private static final MapCache, DeparturesFromStationContext, DeparturesFromStationContext> departuresAtStationCache = new MapCache<>((context) -> { + return getDeparturesAt(x -> TrainUtils.stationMatches(x.getStationName(), context.station()), context.selfTrain()); + }, DeparturesFromStationContext::hashCode, ECachingPriority.LOWEST); + + public static void refreshCache() { + allStationsCache.clear(); + allSignalsCache.clear(); + departingTrainsAtTagCache.clearAll(); + departingTrainsAtStationCache.clearAll(); + departuresAtTagCache.clearAll(); + departuresAtStationCache.clearAll(); + } + + private TrainUtils() {} public static GlobalRailwayManager getRailwayManager() { return Create.RAILWAYS; @@ -56,7 +136,12 @@ public static Map> allPredictionsRa } public static boolean isStationKnown(String station) { - return allPredictionsRaw().keySet().stream().anyMatch(x -> TrainUtils.stationMatches(station, x)); + for (String stationKey : allPredictionsRaw().keySet()) { + if (TrainUtils.stationMatches(station, stationKey)) { + return true; + } + } + return false; } /** @@ -64,12 +149,7 @@ public static boolean isStationKnown(String station) { * @return a list containing all track stations. */ public static Collection getAllStations() { - final Collection stations = new ArrayList<>(); - getRailwayManager().trackNetworks.forEach((uuid, graph) -> { - Collection foundStations = graph.getPoints(EdgePointType.STATION); - stations.addAll(foundStations); - }); - return stations; + return allStationsCache.get(); } public static Optional getTrain(UUID trainId) { @@ -81,27 +161,37 @@ public static Set getTrainIds() { } public static Set getTrains(boolean onlyValid) { - return new HashSet<>(getRailwayManager().trains.values().stream().filter(x -> !onlyValid || isTrainValid(x)).toList()); + Set trains = new HashSet<>(); + for (Train train : getRailwayManager().trains.values()) { + if (onlyValid && !isTrainValid(train)) { + continue; + } + trains.add(train); + } + return trains; } - public static Set getAllSignals() { - return new HashSet<>(getRailwayManager().trackNetworks.values().stream().flatMap(x -> x.getPoints(EdgePointType.SIGNAL).stream()).toList()); + public static Set getAllSignals() { + return allSignalsCache.get(); } public static Set getDepartingTrainsAt(StationTag station) { - return ImmutableMap.copyOf(GlobalTrainDisplayData.statusByDestination).entrySet().stream().filter(x -> station.contains(x.getKey())).flatMap(x -> x.getValue().stream()).map(x -> x.train).collect(Collectors.toSet()); + return departingTrainsAtTagCache.get(station, station); } public static Set getDepartingTrainsAt(String station) { - return ImmutableMap.copyOf(GlobalTrainDisplayData.statusByDestination).entrySet().stream().filter(x -> station.equals(x.getKey())).flatMap(x -> x.getValue().stream()).map(x -> x.train).collect(Collectors.toSet()); + return departingTrainsAtStationCache.get(station, station); } + public static List getDeparturesAt(StationTag station, UUID selfTrain) { - return getDeparturesAt(x -> x.getStationTag().equals(station), selfTrain); + DeparturesFromTagContext context = new DeparturesFromTagContext(station, selfTrain); + return departuresAtTagCache.get(context, context); } public static List getDeparturesAtStationName(String stationName, UUID selfTrain) { - return getDeparturesAt(x -> TrainUtils.stationMatches(x.getStationName(), stationName), selfTrain); + DeparturesFromStationContext context = new DeparturesFromStationContext(stationName, selfTrain); + return departuresAtStationCache.get(context, context); } public static List getDeparturesAt(Predicate stationFilter, UUID selfTrain) { @@ -111,28 +201,39 @@ public static List getDeparturesAt(Predicate station selfSchedule.setFirst(new TrainSchedule(TrainListener.data.containsKey(x.id) ? TrainListener.data.get(x.id).getSessionId() : new UUID(0, 0), x)); }); - List list = TrainListener.data.values().stream() - .filter(x -> !x.getTrainId().equals(selfTrain) && TrainUtils.isTrainUsable(x.getTrain())) - .flatMap(x -> x.getPredictions().stream().filter(stationFilter)) - .map(TrainStop::new) - .filter(x -> { - if (selfSchedule.getFirst() != null) { - return true; + List stops = new ArrayList<>(); + for (TrainData data : TrainListener.data.values()) { + + if (data.getTrainId().equals(selfTrain) || !TrainUtils.isTrainUsable(data.getTrain())) { + continue; + } + + for (TrainPrediction pred : data.getPredictions()) { + if (!stationFilter.test(pred)) { + continue; + } + + TrainStop stop = new TrainStop(pred); + if (selfSchedule.getFirst() == null) { + Optional train = TrainUtils.getTrain(stop.getTrainId()); + if (!train.isPresent()) { + continue; + } + TrainSchedule sched = new TrainSchedule(TrainListener.data.containsKey(train.get().id) ? TrainListener.data.get(train.get().id).getSessionId() : new UUID(0, 0), train.get()); + if (sched.isEqual(selfSchedule.getFirst())) { + continue; + } } - Optional train = TrainUtils.getTrain(x.getTrainId()); - if (!train.isPresent()) { - return false; - } - TrainSchedule sched = new TrainSchedule(TrainListener.data.containsKey(train.get().id) ? TrainListener.data.get(train.get().id).getSessionId() : new UUID(0, 0), train.get()); - return !sched.isEqual(selfSchedule.getFirst()); - }) - .sorted((a, b) -> Long.compare(a.getScheduledDepartureTime(), b.getScheduledDepartureTime())) - .toList(); + stops.add(stop); + } + + Collections.sort(stops, (a, b) -> Long.compare(a.getScheduledDepartureTime(), b.getScheduledDepartureTime())); + } List results = new ArrayList<>(); Set usedTrains = new HashSet<>(); usedTrains.add(selfTrain); - for (TrainStop stop : list) { + for (TrainStop stop : stops) { if (!TrainListener.data.containsKey(stop.getTrainId())) continue; TrainData data = TrainListener.data.get(stop.getTrainId()); TrainTravelSection section = data.getSectionByIndex(stop.getSectionIndex()); @@ -150,12 +251,38 @@ public static List getDeparturesAt(Predicate station } public static Set isSignalOccupied(UUID signalId, Set excludedTrains) { - Optional signal = getAllSignals().stream().filter(x -> x.getId().equals(signalId)).findFirst(); + Optional signal = Optional.empty(); + for (SignalBoundary s : getAllSignals()) { + if (s.getId().equals(signalId)) { + signal = Optional.of(s); + break; + } + } if (!signal.isPresent()) { return Set.of(); } - Set occupyingTrains = getTrains(false).stream().filter(x -> !excludedTrains.contains(x.id) && x.occupiedSignalBlocks.keySet().stream().anyMatch(y -> y.equals(signal.get().groups.getFirst()) || y.equals(signal.get().groups.getSecond()))).collect(Collectors.toSet()); + Set occupyingTrains = new HashSet<>(); + for (Train train : getTrains(false)) { + if (excludedTrains.contains(train.id)) { + continue; + } + + boolean isOccupyingSignal = false; + for (UUID occupiedSignal : train.occupiedSignalBlocks.keySet()) { + if (occupiedSignal.equals(signal.get().groups.getFirst()) || occupiedSignal.equals(signal.get().groups.getSecond())) { + isOccupyingSignal = true; + break; + } + } + + if (!isOccupyingSignal) { + continue; + } + + occupyingTrains.add(train); + } + return occupyingTrains; } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java index 862b2af4..d1b2d536 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/BasicTrainDisplayData.java @@ -1,6 +1,7 @@ package de.mrjulsen.crn.data.train.portable; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -8,6 +9,7 @@ import com.simibubi.create.content.trains.entity.TrainIconType; import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.mcdragonlib.data.Cache; import de.mrjulsen.crn.data.train.TrainData; import de.mrjulsen.crn.data.train.TrainListener; import de.mrjulsen.crn.data.train.TrainStop; @@ -15,6 +17,7 @@ import de.mrjulsen.crn.event.ModCommonEvents; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.resources.ResourceLocation; @@ -22,32 +25,42 @@ public class BasicTrainDisplayData { private final UUID id; private final String name; + private final int color; private final TrainIconType icon; - private final List status; + private final Collection statusLocations; // Server private final boolean cancelled; + private final Cache> clientStatus; + private static final String NBT_ID = "Id"; private static final String NBT_NAME = "Name"; private static final String NBT_ICON = "Icon"; + private static final String NBT_COLOR = "Color"; private static final String NBT_STATUS = "Status"; private static final String NBT_CANCELLED = "Cancelled"; - public BasicTrainDisplayData( + private BasicTrainDisplayData( UUID id, String name, + int color, TrainIconType icon, - List status, + Collection statusLocations, boolean cancelled ) { this.id = id; this.name = name; + this.color = color; this.icon = icon; - this.status = status; + this.statusLocations = statusLocations; this.cancelled = cancelled; + + this.clientStatus = new Cache<>(() -> { + return CompiledTrainStatus.load(statusLocations); + }); } public static BasicTrainDisplayData empty() { - return new BasicTrainDisplayData(new UUID(0, 0), "", TrainIconType.getDefault(), List.of(), true); + return new BasicTrainDisplayData(new UUID(0, 0), "", 0, TrainIconType.getDefault(), List.of(), true); } /** Server-side only! */ @@ -61,13 +74,15 @@ public static BasicTrainDisplayData of(UUID train) throws RuntimeSideException { TrainData data = TrainListener.data.get(train); return new BasicTrainDisplayData( data.getTrainId(), - data.getTrainName(), - TrainIconType.getDefault(), // TODO + data.getTrainDisplayName(), + data.getCurrentSection().getTrainLine().map(x -> x.getColor()).orElse(0), + data.getTrain().icon, new ArrayList<>(data.getStatus()), data.isCancelled() ); } + /** Server-side only! */ public static BasicTrainDisplayData of(TrainStop stop) throws RuntimeSideException { if (!ModCommonEvents.hasServer()) { throw new RuntimeSideException(false); @@ -78,7 +93,8 @@ public static BasicTrainDisplayData of(TrainStop stop) throws RuntimeSideExcepti TrainData data = TrainListener.data.get(stop.getTrainId()); return new BasicTrainDisplayData( stop.getTrainId(), - stop.getTrainName(), + stop.getTrainDisplayName(), + data.getSectionForIndex(stop.getScheduleIndex()).getTrainLine().map(x -> x.getColor()).orElse(0), stop.getTrainIcon(), new ArrayList<>(data.getStatus()), data.isCancelled() @@ -98,25 +114,37 @@ public TrainIconType getIcon() { } public List getStatus() { - return status; + return clientStatus.get(); } public boolean isCancelled() { return cancelled; } + public int getColor() { + return color; + } + + public boolean hasColor() { + return color != 0; + } + public boolean hasStatusInfo() { return !getStatus().isEmpty(); } + public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); ListTag statusList = new ListTag(); - statusList.addAll(status.stream().map(x -> x.toNbt()).toList()); + for (ResourceLocation s : statusLocations) { + statusList.add(StringTag.valueOf(s.toString())); + } nbt.putUUID(NBT_ID, id); nbt.putString(NBT_NAME, name); nbt.putString(NBT_ICON, icon.getId().toString()); + nbt.putInt(NBT_COLOR, color); nbt.put(NBT_STATUS, statusList); nbt.putBoolean(NBT_CANCELLED, cancelled); return nbt; @@ -125,9 +153,10 @@ public CompoundTag toNbt() { public static BasicTrainDisplayData fromNbt(CompoundTag nbt) { return new BasicTrainDisplayData( nbt.getUUID(NBT_ID), - nbt.getString(NBT_NAME), + nbt.getString(NBT_NAME), + nbt.getInt(NBT_COLOR), TrainIconType.byId(new ResourceLocation(nbt.getString(NBT_ICON))), - nbt.getList(NBT_STATUS, Tag.TAG_COMPOUND).stream().map(x -> CompiledTrainStatus.fromNbt(((CompoundTag)x))).toList(), + nbt.getList(NBT_STATUS, Tag.TAG_STRING).stream().map(x -> new ResourceLocation(((StringTag)x).getAsString())).toList(), nbt.getBoolean(NBT_CANCELLED) ); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java index 641bacd6..5a74d2b9 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/NextConnectionsDisplayData.java @@ -1,11 +1,13 @@ package de.mrjulsen.crn.data.train.portable; +import java.util.ArrayList; import java.util.List; import java.util.UUID; import de.mrjulsen.crn.data.TagName; import de.mrjulsen.crn.data.storage.GlobalSettings; import de.mrjulsen.crn.exceptions.RuntimeSideException; +import de.mrjulsen.crn.data.train.TrainStop; import de.mrjulsen.crn.data.train.TrainUtils; import de.mrjulsen.crn.event.ModCommonEvents; import net.minecraft.nbt.CompoundTag; @@ -33,9 +35,15 @@ public static NextConnectionsDisplayData at(String stationName, UUID selfTrainId if (!ModCommonEvents.hasServer()) { throw new RuntimeSideException(false); } + + List departures = TrainUtils.getDeparturesAt(GlobalSettings.getInstance().getOrCreateStationTagFor(TagName.of(stationName)), selfTrainId); + List displayData = new ArrayList<>(departures.size()); + for (TrainStop stop : departures) { + displayData.add(TrainStopDisplayData.of(stop)); + } return new NextConnectionsDisplayData( - TrainUtils.getDeparturesAt(GlobalSettings.getInstance().getOrCreateStationTagFor(TagName.of(stationName)), selfTrainId).stream().map(x -> TrainStopDisplayData.of(x)).toList() + displayData ); } public List getConnections() { @@ -46,7 +54,10 @@ public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); ListTag list = new ListTag(); - list.addAll(stops.stream().map(x -> x.toNbt()).toList()); + List displayData = stops; + for (TrainStopDisplayData d : displayData) { + list.add(d.toNbt()); + } nbt.put(NBT_STOPS, list); return nbt; diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java index dece556a..16a30124 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/StationDisplayData.java @@ -19,6 +19,7 @@ public class StationDisplayData { private final TrainStopDisplayData stationData; private final String firstStopName; private final boolean isLastStop; + private final boolean isNextSectionExcluded; private final List stopovers; private static final String NBT_TRAIN = "Train"; @@ -26,6 +27,7 @@ public class StationDisplayData { private static final String NBT_STOPOVERS = "Stopovers"; private static final String NBT_FIRST_STOP = "FirstStop"; private static final String NBT_IS_LAST = "IsLast"; + private static final String NBT_IS_NEXT_EXCLUDED = "IsNextSectionExcluded"; @@ -34,6 +36,7 @@ public StationDisplayData( TrainStopDisplayData stationData, String firstStopName, boolean isLastStop, + boolean isNextSectionExcluded, List stopovers ) { this.trainData = trainData; @@ -41,10 +44,11 @@ public StationDisplayData( this.stopovers = stopovers; this.firstStopName = firstStopName; this.isLastStop = isLastStop; + this.isNextSectionExcluded = isNextSectionExcluded; } public static StationDisplayData empty() { - return new StationDisplayData(BasicTrainDisplayData.empty(), TrainStopDisplayData.empty(), "", false, List.of()); + return new StationDisplayData(BasicTrainDisplayData.empty(), TrainStopDisplayData.empty(), "", false, false, List.of()); } /** Server-side only! */ @@ -76,6 +80,7 @@ public static StationDisplayData of(TrainStop stop) throws RuntimeSideException TrainStopDisplayData.of(stop), firstStop, isLastStopOfSection, + isLastStopOfSection && !section.nextSection().isUsable(), section.getStopoversFrom(stop.getScheduleIndex()) ); } @@ -100,6 +105,10 @@ public boolean isLastStop() { return isLastStop; } + public boolean isNextSectionExcluded() { + return isNextSectionExcluded; + } + public boolean isDelayed() { return isLastStop() ? getStationData().isArrivalDelayed() : getStationData().isDepartureDelayed(); } @@ -117,12 +126,16 @@ public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); ListTag stopoversList = new ListTag(); - stopoversList.addAll(getStopovers().stream().map(x -> StringTag.valueOf(x)).toList()); + List stations = getStopovers(); + for (String name : stations) { + stopoversList.add(StringTag.valueOf(name)); + } nbt.put(NBT_TRAIN, trainData.toNbt()); nbt.put(NBT_STATION, stationData.toNbt()); nbt.putString(NBT_FIRST_STOP, firstStopName); nbt.putBoolean(NBT_IS_LAST, isLastStop); + nbt.putBoolean(NBT_IS_NEXT_EXCLUDED, isNextSectionExcluded); nbt.put(NBT_STOPOVERS, stopoversList); return nbt; } @@ -133,6 +146,7 @@ public static StationDisplayData fromNbt(CompoundTag nbt) { TrainStopDisplayData.fromNbt(nbt.getCompound(NBT_STATION)), nbt.getString(NBT_FIRST_STOP), nbt.getBoolean(NBT_IS_LAST), + nbt.getBoolean(NBT_IS_NEXT_EXCLUDED), nbt.getList(NBT_STOPOVERS, Tag.TAG_STRING).stream().map(x -> ((StringTag)x).getAsString()).toList() ); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java index f26a097d..848e1514 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainDisplayData.java @@ -11,6 +11,7 @@ import de.mrjulsen.crn.exceptions.RuntimeSideException; import de.mrjulsen.crn.data.train.TrainData; import de.mrjulsen.crn.data.train.TrainListener; +import de.mrjulsen.crn.data.train.TrainPrediction; import de.mrjulsen.crn.data.train.TrainStop; import de.mrjulsen.crn.data.train.TrainTravelSection; import de.mrjulsen.crn.data.train.TrainUtils; @@ -112,9 +113,18 @@ public static TrainDisplayData of(Train train) throws RuntimeSideException { TrainData data = TrainListener.data.get(train.id); TrainTravelSection section = data.getCurrentSection(); + + List displayData = new ArrayList<>(); + if (section.isUsable()) { + List predictions = data.getCurrentSection().getPredictions(-1, false); + for (TrainPrediction prediction : predictions) { + displayData.add(TrainStopDisplayData.of(new TrainStop(prediction))); + } + } + return new TrainDisplayData( BasicTrainDisplayData.of(train.id), - section.isUsable() ? data.getCurrentSection().getPredictions(-1, false).stream().map(x -> TrainStopDisplayData.of(new TrainStop(x))).toList() : List.of(), + displayData, data.getCurrentScheduleIndex(), side, train.speed, @@ -177,7 +187,10 @@ public CompoundTag toNbt() { CompoundTag nbt = new CompoundTag(); ListTag stopsList = new ListTag(); - stopsList.addAll(getAllStops().stream().map(x -> x.toNbt()).toList()); + List allStops = getAllStops(); + for (TrainStopDisplayData stop : allStops) { + stopsList.add(stop.toNbt()); + } nbt.put(NBT_TRAIN, trainData.toNbt()); nbt.put(NBT_STOPS, stopsList); diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java index 7414ba06..0a440247 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/portable/TrainStopDisplayData.java @@ -68,8 +68,8 @@ public static TrainStopDisplayData of(TrainStop stop) throws RuntimeSideExceptio stop.getScheduledArrivalTime(), stop.getRealTimeDepartureTime(), stop.getRealTimeArrivalTime(), - stop.getDisplayTitle(),//stop.getRealTimeStationTag().stationName(), - stop.getTrainName(), + stop.getDisplayTitle(), + stop.getTrainDisplayName(), stop.getRealTimeStationTag().info() ); } diff --git a/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java b/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java index a46bebf8..edb81867 100644 --- a/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java +++ b/common/src/main/java/de/mrjulsen/crn/debug/DebugOverlay.java @@ -49,7 +49,7 @@ public void render(Graphics graphics, float partialTicks, int screenWidth, int s trainIndex %= TrainListener.data.size(); TrainData data = TrainListener.data.values().stream().skip(trainIndex).findFirst().get(); drawLine(graphics, data.getTrain().name.getString() + " (" + (data.isPreparing() ? "PREPARING" : (data.isInitialized() ? "READY" : "INITIALIZING")) + ") SessionId: " + data.getSessionId()); - drawLine(graphics, "Display: " + data.getCurrentTravelSection().getDisplayText() + ", Title: " + data.getCurrentTitle() + ", IsDynamic: " + data.isDynamic()); + drawLine(graphics, "Display: " + data.getCurrentSection().getDisplayText() + ", Title: " + data.getCurrentTitle() + ", IsDynamic: " + data.isDynamic()); drawLine(graphics, "Track: " + !((TrainStatusAccessor)data.getTrain().status).crn$track() + ", Conductor: " + !((TrainStatusAccessor)data.getTrain().status).crn$conductor() + ", Navigation: " + !((TrainStatusAccessor)data.getTrain().status).crn$navigation() + ", Paused: " + data.getTrain().runtime.paused + ", Auto: " + data.getTrain().runtime.isAutoSchedule + ", Manual: " + data.isManualControlled + ", Cancelled: " + data.isCancelled()); drawLine(graphics, "Duration: " + data.getTotalDuration() + ", Ticks: " + data.getTransitTicks() + "/" + data.waitingAtStationTicks() + "/" + data.waitingForSignalTicks + ", Dest: " + (data.getTrain().navigation.destination == null ? "(at station)" : (data.getTrain().navigation.destination.name + ", DestID: " + data.getTrain().navigation.destination.id)) + ", Delay: " + data.getHighestDeviation() + " (-" + data.getDeviationDelayOffset() + "), Status: " + data.debug_statusInfoCount()); data.getPredictions().forEach(a -> { @@ -87,7 +87,7 @@ public void render(Graphics graphics, float partialTicks, int screenWidth, int s drawLine(graphics, "Sections:"); for (TrainTravelSection section : data.getSections()) { - drawLine(graphics, " - [ " + section.getScheduleIndex() + " ]: " + section.getDisplayText() + " (" + section.getStartStationName() + " -> " + section.getDestinationStationName() + "), Group: " + (section.getTrainGroup() == null ? "none" : section.getTrainGroup().getGroupName()) + ", Line: " + (section.getTrainLine() == null ? "none" : section.getTrainLine().getLineName()) + ", Include: " + section.shouldIncludeNextStationOfNextSection() + ", Navigable: " + section.isUsable() + ", Next: " + section.nextSection().getScheduleIndex()); + drawLine(graphics, " - [ " + section.getScheduleIndex() + " ]: " + section.getDisplayText() + " (" + section.getStartStationName() + " -> " + section.getDestinationStationName() + "), Group: " + section.getTrainGroup().map(x -> x.getGroupName()).orElse("none") + ", Line: " + section.getTrainLine().map(x -> x.getLineName()).orElse("none") + ", Include: " + section.shouldIncludeNextStationOfNextSection() + ", Navigable: " + section.isUsable() + ", Next: " + section.nextSection().getScheduleIndex()); } } drawLine(graphics, TextUtils.text("Press K to switch train.").withStyle(ChatFormatting.AQUA)); diff --git a/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java b/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java index 40c6c06d..197d97af 100644 --- a/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java +++ b/common/src/main/java/de/mrjulsen/crn/event/ModClientEvents.java @@ -2,9 +2,12 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.client.input.ModKeys; import de.mrjulsen.crn.config.ModClientConfig; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.SavedRoutesManager; import de.mrjulsen.crn.data.navigation.ClientTrainListener; +import de.mrjulsen.crn.data.train.StationDepartureHistory; import de.mrjulsen.crn.event.events.DefaultTrainDataRefreshEvent; import de.mrjulsen.crn.event.events.RouteDetailsActionsEvent; import de.mrjulsen.crn.network.InstanceManager; @@ -27,6 +30,7 @@ public class ModClientEvents { public static void init() { ClientLifecycleEvent.CLIENT_SETUP.register((mc) -> { + ModKeys.init(); ModDisplayTags.register(); }); @@ -76,9 +80,12 @@ public static void init() { }); ClientGuiEvent.DEBUG_TEXT_LEFT.register((texts) -> { - texts.add(String.format("CRN | RL: %s", - ClientTrainListener.debug_registeredListenersCount() - )); + if (ModCommonConfig.ADVANCED_LOGGING.get()) { + texts.add(String.format("CRN | RL: %s, %s", + ClientTrainListener.debug_registeredListenersCount(), + StationDepartureHistory.debug_departureHistory() + )); + } }); } } diff --git a/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java b/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java index 992c9244..f2f89b0e 100644 --- a/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java +++ b/common/src/main/java/de/mrjulsen/crn/event/ModCommonEvents.java @@ -5,7 +5,10 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.block.display.AdvancedDisplayTarget; import de.mrjulsen.crn.cmd.DebugCommand; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.storage.GlobalSettings; +import de.mrjulsen.crn.data.train.StationDepartureHistory; +import de.mrjulsen.crn.data.train.TrainData; import de.mrjulsen.crn.data.train.TrainListener; import de.mrjulsen.crn.event.events.CreateTrainPredictionEvent; import de.mrjulsen.crn.event.events.GlobalTrainDisplayDataRefreshEventPost; @@ -16,7 +19,6 @@ import de.mrjulsen.crn.event.events.TrainArrivalAndDepartureEvent; import de.mrjulsen.crn.event.events.TrainDestinationChangedEvent; import de.mrjulsen.crn.registry.ModExtras; -import de.mrjulsen.crn.web.SimpleWebServer; import de.mrjulsen.mcdragonlib.internal.ClientWrapper; import dev.architectury.event.events.common.CommandRegistrationEvent; import dev.architectury.event.events.common.LifecycleEvent; @@ -56,12 +58,6 @@ public static void init() { TrainListener.start(); AdvancedDisplayTarget.start(); - - try { - SimpleWebServer.start(); - } catch (Exception e) { - e.printStackTrace(); - } }); LifecycleEvent.SERVER_STOPPING.register((server) -> { @@ -70,8 +66,7 @@ public static void init() { TrainListener.stop(); AdvancedDisplayTarget.stop(); CRNEventsManager.clearEvents(); - - SimpleWebServer.stop(); + StationDepartureHistory.clearAll(); }); LifecycleEvent.SERVER_STOPPED.register((server) -> { @@ -86,8 +81,10 @@ public static void init() { long currentTicks = ModCommonEvents.getPhysicalLevel().dayTime(); long diff = currentTicks - lastTicks; if (Math.abs(diff) > 1) { - TrainListener.data.values().forEach(x -> x.shiftTime(diff)); - CreateRailwaysNavigator.LOGGER.info("All times have been corrected: " + (diff) + " Ticks"); + for (TrainData data : TrainListener.data.values()) { + data.shiftTime(diff); + } + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("All times have been corrected: " + (diff) + " Ticks"); } lastTicks = currentTicks; } @@ -95,11 +92,14 @@ public static void init() { TrainListener.tick(); }); - CommandRegistrationEvent.EVENT.register((dispatcher, buildContext, selection) -> { + CommandRegistrationEvent.EVENT.register((dispatcher, context, selection) -> { DebugCommand.register(dispatcher, selection); }); LifecycleEvent.SERVER_LEVEL_SAVE.register((server) -> { + if (!getCurrentServer().isPresent()) return; + if (server != getCurrentServer().get().overworld()) return; + TrainListener.save(); if (GlobalSettings.hasInstance()) GlobalSettings.getInstance().save(); }); diff --git a/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java b/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java index 01c7c2f9..08c7aee0 100644 --- a/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java +++ b/common/src/main/java/de/mrjulsen/crn/item/NavigatorItem.java @@ -22,4 +22,27 @@ public InteractionResultHolder use(Level pLevel, Player pPlayer, Inte } return super.use(pLevel, pPlayer, pUsedHand); } + + /* + @Environment(EnvType.CLIENT) + @SuppressWarnings("resource") + @Override + public void renderAdditional(RenderGraphics graphics, ItemStack itemStack, TransformType transformType, boolean leftHand, PoseStack poseStack, MultiBufferSource buffer, int combinedLight, int combinedOverlay, BakedModel model) { + Font font = Minecraft.getInstance().font; + poseStack.mulPose(Vector3f.XP.rotationDegrees(90F)); + poseStack.translate(4, 2, -0.76); + + poseStack.pushPose(); + poseStack.translate(4, 0.8f, 0); + poseStack.scale(0.075f, 0.075f, 0.075f); + BERUtils.drawString(graphics, font, 0, 0, "Day " + (DragonLib.getCurrentWorldTime() / DragonLib.ticksPerDay()), 0xFFFFFFFF, EAlignment.CENTER, false, LightTexture.FULL_BRIGHT); + poseStack.popPose(); + + poseStack.pushPose(); + poseStack.translate(4, 2, 0); + poseStack.scale(0.2f, 0.2f, 0.2f); + BERUtils.drawString(graphics, font, 0, 0, TimeUtils.formatTime(DragonLib.getCurrentWorldTime(), ModClientConfig.TIME_FORMAT.get()), 0xFFFFFFFF, EAlignment.CENTER, false, LightTexture.FULL_BRIGHT); + poseStack.popPose(); + } + */ } diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ItemMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ItemMixin.java new file mode 100644 index 00000000..44a10e1e --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ItemMixin.java @@ -0,0 +1,36 @@ +package de.mrjulsen.crn.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import de.mrjulsen.crn.block.AbstractAdvancedDisplayBlock; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.DyeItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(Item.class) +public abstract class ItemMixin { + + public Item self() { + return (Item)(Object)this; + } + + @Inject(method = "useOn", at = @At(value = "HEAD"), cancellable = true) + public void onUseOn(UseOnContext context, CallbackInfoReturnable cir) { + if (self() instanceof DyeItem) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + BlockState state = level.getBlockState(pos); + if (context.getPlayer().isShiftKeyDown() && state.getBlock() instanceof AbstractAdvancedDisplayBlock block) { + cir.setReturnValue(block.use(state, level, pos, context.getPlayer(), context.getHand(), null)); + return; + } + } + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/NavigationMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/NavigationMixin.java new file mode 100644 index 00000000..a2278a54 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/NavigationMixin.java @@ -0,0 +1,60 @@ +package de.mrjulsen.crn.mixin; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.simibubi.create.content.trains.entity.Navigation; + +import de.mrjulsen.crn.data.schedule.INavigationExtension; +import de.mrjulsen.crn.data.schedule.condition.IDelayedWaitCondition; +import de.mrjulsen.crn.data.schedule.condition.IDelayedWaitCondition.DelayedWaitConditionContext; +import de.mrjulsen.mcdragonlib.data.Pair; +import net.minecraft.world.level.Level; + +@Mixin(Navigation.class) +public abstract class NavigationMixin implements INavigationExtension { + + public Queue> delayedWaitConditions = new ConcurrentLinkedQueue<>(); + + @Override + public void addDelayedWaitCondition(Pair pair) { + delayedWaitConditions.add(pair); + } + + @Override + public boolean isDelayedWaitConditionPending() { + return !delayedWaitConditions.isEmpty(); + } + + @Inject(method = "tick", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/trains/entity/Train;leaveStation()V", shift = Shift.BEFORE), remap = false, cancellable = true) + public void onTick(Level level, CallbackInfo ci) { + if (!delayedWaitConditions.isEmpty()) { + Pair p = delayedWaitConditions.peek(); + if (!p.getSecond().nbt().contains(IDelayedWaitCondition.NBT_DELAY)) { + p.getSecond().nbt().putInt(IDelayedWaitCondition.NBT_DELAY, 0); + } + if (p.getFirst().runDelayed(p.getSecond())) { + delayedWaitConditions.poll().getSecond().nbt().remove(IDelayedWaitCondition.NBT_DELAY); + } else { + p.getSecond().nbt().putInt(IDelayedWaitCondition.NBT_DELAY, p.getSecond().nbt().getInt(IDelayedWaitCondition.NBT_DELAY) + 1); + } + ci.cancel(); + } + } + + @Inject(method = "cancelNavigation", at = @At(value = "HEAD"), remap = false) + public void resetOnCancel(CallbackInfo ci) { + delayedWaitConditions.clear(); + } + + //@Inject(method = "startNavigation", at = @At(value = "HEAD"), remap = false) + //public void resetOnStart(DiscoveredPath path, CallbackInfoReturnable cir) { + // delayedWaitConditions.clear(); + //} +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java deleted file mode 100644 index ea5e6652..00000000 --- a/common/src/main/java/de/mrjulsen/crn/mixin/ReloadableServerResourcesMixin.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.mrjulsen.crn.mixin; - -import java.util.ArrayList; -import java.util.List; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import de.mrjulsen.crn.util.ModUtils; -import de.mrjulsen.crn.web.WebsitePreparableReloadListener; -import net.minecraft.commands.Commands; -import net.minecraft.core.RegistryAccess; -import net.minecraft.server.ReloadableServerResources; -import net.minecraft.server.packs.resources.PreparableReloadListener; - -@Mixin(ReloadableServerResources.class) -public class ReloadableServerResourcesMixin { - - public WebsitePreparableReloadListener websitemanager; - - @Inject(method = "", at = @At(value = "TAIL")) - public void onInit(RegistryAccess.Frozen frozen, Commands.CommandSelection commandSelection, int i, CallbackInfo ci) { - this.websitemanager = new WebsitePreparableReloadListener(); - ModUtils.setWebsiteResourceManager(websitemanager); - } - - @Inject(method = "listeners", at = @At("RETURN"), cancellable = true) - private void addListener(CallbackInfoReturnable> cir) { - List listeners = new ArrayList<>(cir.getReturnValue()); - listeners.add(websitemanager); - cir.setReturnValue(listeners); - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleEntryMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleEntryMixin.java new file mode 100644 index 00000000..37378de5 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleEntryMixin.java @@ -0,0 +1,22 @@ +package de.mrjulsen.crn.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import com.simibubi.create.content.trains.schedule.ScheduleEntry; +import net.minecraft.nbt.CompoundTag; + +/** + * Fixes a crash in Create when a condition got removed from the game + */ +@Mixin(ScheduleEntry.class) +public class ScheduleEntryMixin { + + @Inject(method = "fromTag", remap = false, at = @At(value = "RETURN"), locals = LocalCapture.CAPTURE_FAILHARD) + private static void onFromTag(CompoundTag tag, CallbackInfoReturnable cir, ScheduleEntry entry) { + entry.conditions.forEach(x -> x.removeIf(y -> y == null)); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java index 6d737ab3..d8f8effe 100644 --- a/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java +++ b/common/src/main/java/de/mrjulsen/crn/mixin/ScheduleScreenMixin.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; - import org.spongepowered.asm.mixin.Mixin; import com.simibubi.create.content.trains.schedule.IScheduleInput; import com.simibubi.create.content.trains.schedule.ScheduleScreen; @@ -29,6 +28,7 @@ public int getTopPos() { return ((AbstractContainerScreen)(Object)this).topPos; } + /* @SuppressWarnings("resource") @Inject(method = "updateEditorSubwidgets", remap = false, at = @At(value = "INVOKE", shift = Shift.AFTER, target = "Lcom/simibubi/create/foundation/gui/ModularGuiLine;loadValues(Lnet/minecraft/nbt/CompoundTag;Ljava/util/function/Consumer;Ljava/util/function/Consumer;)V"), cancellable = true) diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/StationBlockEntityMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/StationBlockEntityMixin.java new file mode 100644 index 00000000..baf9f076 --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/mixin/StationBlockEntityMixin.java @@ -0,0 +1,109 @@ +package de.mrjulsen.crn.mixin; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.spongepowered.asm.mixin.Mixin; + +import com.simibubi.create.content.equipment.goggles.IHaveGoggleInformation; +import com.simibubi.create.content.trains.station.StationBlockEntity; +import com.simibubi.create.foundation.utility.Lang; + +import de.mrjulsen.crn.CRNPlatformSpecific; +import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.data.train.StationDepartureHistory.StationStats; +import de.mrjulsen.crn.registry.ModAccessorTypes; +import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.util.TextUtils; +import de.mrjulsen.mcdragonlib.util.TimeUtils; +import de.mrjulsen.mcdragonlib.util.accessor.DataAccessor; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +@Environment(EnvType.CLIENT) +@Mixin(StationBlockEntity.class) +public class StationBlockEntityMixin implements IHaveGoggleInformation { + + private StationBlockEntity self() { + return (StationBlockEntity)(Object)this; + } + + private StationStats stats; + + @SuppressWarnings("resource") + @Override + public boolean addToGoggleTooltip(List tooltip, boolean isPlayerSneaking) { + if (CRNPlatformSpecific.getStationFromBlockEntity(self()) == null) { + return false; + } + + if (Minecraft.getInstance().level.getGameTime() % 100 == 0) { + DataAccessor.getFromServer(CRNPlatformSpecific.getStationFromBlockEntity(self()).name, ModAccessorTypes.GET_STATION_DEPARTURE_HISTORY, x -> this.stats = x); + } + + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.title")) + .forGoggles(tooltip); + + if (this.stats == null || this.stats.isEmpty()) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.nothing").withStyle(ChatFormatting.RED)) + .forGoggles(tooltip); + return false; + } + + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.any").withStyle(ChatFormatting.GRAY)) + .forGoggles(tooltip); + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(formatTime(Minecraft.getInstance().level.getGameTime() - stats.getLastDepartureTime())) + .forGoggles(tooltip, 1); + + if (stats.hasDeparturesByLine()) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.line").withStyle(ChatFormatting.GRAY)) + .forGoggles(tooltip); + + List> data = stats.getDeparturesByLine(); + for (Map.Entry d : data) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.empty().append(TextUtils.text(d.getKey() + ": ").withStyle(ChatFormatting.DARK_AQUA)).append(formatTime(Minecraft.getInstance().level.getGameTime() - d.getValue()))) + .forGoggles(tooltip, 1); + } + if (stats.getDeparturesByLineTotalCount() > data.size()) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.has_more", stats.getDeparturesByLineTotalCount() - data.size()).withStyle(ChatFormatting.GRAY)) + .forGoggles(tooltip); + } + } + + if (stats.hasDeparturesByGroup()) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.group").withStyle(ChatFormatting.GRAY)) + .forGoggles(tooltip); + + List> data = stats.getDeparturesByGroup(); + for (Map.Entry d : data) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.empty().append(TextUtils.text(d.getKey() + ": ").withStyle(ChatFormatting.DARK_AQUA)).append(formatTime(Minecraft.getInstance().level.getGameTime() - d.getValue()))) + .forGoggles(tooltip, 1); + } + if (stats.getDeparturesByGroupTotalCount() > data.size()) { + Lang.builder(CreateRailwaysNavigator.MOD_ID) + .add(TextUtils.translate("goggles." + CreateRailwaysNavigator.MOD_ID + ".train_listener.departures.has_more", stats.getDeparturesByGroupTotalCount() - data.size()).withStyle(ChatFormatting.GRAY)) + .forGoggles(tooltip); + } + } + + return true; + } + + private MutableComponent formatTime(long ticks) { + return TextUtils.text(TimeUtils.formatDurationMs(TimeUnit.SECONDS.toMillis((long)(ticks / DragonLib.tps())))).withStyle(ChatFormatting.AQUA); + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java index 8cbc8a46..86270854 100644 --- a/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java +++ b/common/src/main/java/de/mrjulsen/crn/network/packets/cts/AdvancedDisplayUpdatePacket.java @@ -4,11 +4,14 @@ import de.mrjulsen.crn.block.AbstractAdvancedSidedDisplayBlock; import de.mrjulsen.crn.block.blockentity.AdvancedDisplayBlockEntity; +import de.mrjulsen.crn.block.display.properties.IDisplaySettings; import de.mrjulsen.crn.block.properties.ESide; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; import de.mrjulsen.mcdragonlib.net.IPacketBase; import dev.architectury.networking.NetworkManager.PacketContext; import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; @@ -18,27 +21,34 @@ public class AdvancedDisplayUpdatePacket implements IPacketBase { - be.setDisplayTypeKey(packet.key); + be.setDisplayType(packet.key, packet.settings); if (level.getBlockState(be.getBlockPos()).getBlock() instanceof AbstractAdvancedSidedDisplayBlock) { BlockState state = level.getBlockState(be.getBlockPos()); state = state.setValue(AbstractAdvancedSidedDisplayBlock.SIDE, packet.doubleSided ? ESide.BOTH : ESide.FRONT); diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ClientWrapper.java b/common/src/main/java/de/mrjulsen/crn/registry/ClientWrapper.java index 407612b5..bf7c88a2 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ClientWrapper.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ClientWrapper.java @@ -15,12 +15,17 @@ public class ClientWrapper { public static final CTSpriteShiftEntry CT_ADVANCED_DISPLAY_ALL = ClientWrapper.getCT(AllCTTypes.OMNIDIRECTIONAL, "advanced_display", "advanced_display"); + public static final CTSpriteShiftEntry CT_ADVANCED_DISPLAY_ALL_BORDER = ClientWrapper.getCT(AllCTTypes.OMNIDIRECTIONAL, "advanced_display_border", "advanced_display_border"); public static final CTSpriteShiftEntry CT_ADVANCED_DISPLAY = ClientWrapper.getCT(AllCTTypes.OMNIDIRECTIONAL, "advanced_display", "advanced_display"); - public static final CTSpriteShiftEntry CT_HORIZONTAL_ADVANCED_DISPLAY = ClientWrapper.getCT(AllCTTypes.HORIZONTAL_KRYPPERS, "advanced_display", "advanced_display"); + public static final CTSpriteShiftEntry CT_ADVANCED_DISPLAY_BORDER = ClientWrapper.getCT(AllCTTypes.OMNIDIRECTIONAL, "advanced_display_border", "advanced_display_border"); + //public static final CTSpriteShiftEntry CT_HORIZONTAL_ADVANCED_DISPLAY = ClientWrapper.getCT(AllCTTypes.HORIZONTAL_KRYPPERS, "advanced_display_border", "advanced_display_border"); + //public static final CTSpriteShiftEntry CT_HORIZONTAL_ADVANCED_DISPLAY_BORDER = ClientWrapper.getCT(AllCTTypes.HORIZONTAL_KRYPPERS, "advanced_display_border", "advanced_display_border"); public static final CTSpriteShiftEntry CT_ADVANCED_DISPLAY_SMALL = ClientWrapper.getCT(AllCTTypes.OMNIDIRECTIONAL, "advanced_display_small", "advanced_display_small"); + public static final CTSpriteShiftEntry CT_ADVANCED_DISPLAY_SMALL_BORDER = ClientWrapper.getCT(AllCTTypes.OMNIDIRECTIONAL, "advanced_display_small_border", "advanced_display_small_border"); public static final CTSpriteShiftEntry CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL = ClientWrapper.getCT(AllCTTypes.HORIZONTAL_KRYPPERS, "advanced_display_small", "advanced_display_small"); + public static final CTSpriteShiftEntry CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL_BORDER = ClientWrapper.getCT(AllCTTypes.HORIZONTAL_KRYPPERS, "advanced_display_small_border", "advanced_display_small_border"); public static void registerCTBehviour(Block entry, Supplier behaviorSupplier) { diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java b/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java index cb7d3940..ee90a1bc 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModAccessorTypes.java @@ -3,12 +3,12 @@ import java.util.UUID; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.stream.Collectors; - import com.simibubi.create.content.trains.entity.Train; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.Set; @@ -38,6 +38,7 @@ import de.mrjulsen.crn.data.train.TrainTravelSection; import de.mrjulsen.crn.data.train.TrainUtils; import de.mrjulsen.crn.data.train.ClientTrainStop.TrainStopRealTimeData; +import de.mrjulsen.crn.data.train.StationDepartureHistory.StationStats; import de.mrjulsen.crn.data.train.portable.NextConnectionsDisplayData; import de.mrjulsen.crn.data.train.portable.TrainDisplayData; import de.mrjulsen.crn.debug.TrainDebugData; @@ -498,19 +499,24 @@ public final class ModAccessorTypes { } TrainData data = TrainListener.data.get(in); - Map values = data.getPredictions().stream().map(a -> new TrainStopRealTimeData( - a.getStationTag().getClientTag(a.getStationName()), - a.getEntryIndex(), - a.getScheduledArrivalTime(), - a.getScheduledDepartureTime(), - a.getRealTimeArrivalTime(), - a.getRealTimeDepartureTime(), - a.getArrivalTimeDeviation(), - a.getDepartureTimeDeviation(), - a.getRealTimeArrivalTicks(), - a.getCurrentCycle() - )).collect(Collectors.toMap(a -> a.entryIndex(), a -> a)); - nbt.put(DataAccessorType.DEFAULT_NBT_DATA, new TrainRealTimeData(data.getSessionId(), values, data.getStatus(), data.isCancelled()).toNbt()); + List predictions = data.getPredictions(); + Map values = new HashMap<>(); + for (TrainPrediction prediction : predictions) { + TrainStopRealTimeData realTimeData = new TrainStopRealTimeData( + prediction.getStationTag().getClientTag(prediction.getStationName()), + prediction.getEntryIndex(), + prediction.getScheduledArrivalTime(), + prediction.getScheduledDepartureTime(), + prediction.getRealTimeArrivalTime(), + prediction.getRealTimeDepartureTime(), + prediction.getArrivalTimeDeviation(), + prediction.getDepartureTimeDeviation(), + prediction.getRealTimeArrivalTicks(), + prediction.getCurrentCycle() + ); + values.put(realTimeData.entryIndex(), realTimeData); + } + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, TrainRealTimeData.createServer(data.getSessionId(), values, data.getStatus(), data.isCancelled()).toNbt()); return false; }, (hasMore, previousData, iteration, nbt) -> { return nbt.contains(DataAccessorType.DEFAULT_NBT_DATA) ? TrainRealTimeData.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)) : null; @@ -567,7 +573,7 @@ public final class ModAccessorTypes { } )); - public static final DataAccessorType GET_TRAIN_DISPLAY_DATA = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_train_display_data"), DataAccessorType.Builder.create( + public static final DataAccessorType GET_TRAIN_DISPLAY_DATA_FROM_SERVER = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_train_display_data"), DataAccessorType.Builder.create( (in, nbt) -> { nbt.putUUID(DataAccessorType.DEFAULT_NBT_DATA, in); }, (nbt) -> { @@ -732,7 +738,7 @@ public static record NavigationData(String start, String end, UUID player) {} settings.getTagByName(TagName.of(in.end())).orElse(settings.getOrCreateStationTagFor(in.end())), in.player(), true - ); + ); temp.setFirst(new ConcurrentLinkedQueue<>(routes)); } @SuppressWarnings("unchecked") @@ -773,26 +779,31 @@ public static record DepartureRoutesData(String stationTagName, UUID player) {} if (temp.getFirst() == null) { UserSettings settings = UserSettings.getSettingsFor(in.player(), true); StationTag station = GlobalSettings.getInstance().getOrCreateStationTagFor(TagName.of(in.stationTagName())); - Set trains = TrainUtils.getDepartingTrainsAt(station).stream().filter(x -> + Set trains = TrainUtils.getDepartingTrainsAt(station); + trains.removeIf(x -> !( TrainUtils.isTrainUsable(x) && !GlobalSettings.getInstance().isTrainBlacklisted(x) && TrainListener.data.containsKey(x.id) - ).collect(Collectors.toSet()); + )); - List> routesL = new ArrayList<>(); + List> routesL = new LinkedList<>(); for (Train train : trains) { TrainData data = TrainListener.data.get(train.id); - List matchingPredictions = data.getPredictionsChronologically().stream().filter(x -> x.getStationTag().equals(station)).toList(); + List matchingPredictions = data.getPredictionsChronologically(); for (TrainPrediction prediction : matchingPredictions) { + if (!prediction.getStationTag().equals(station)) { + continue; + } + TrainTravelSection section = prediction.getSection(); - if ((!section.isUsable() && !(section.isFirstStop(prediction) && section.previousSection().isUsable() && section.previousSection().shouldIncludeNextStationOfNextSection())) || (section.getTrainGroup() != null && settings.searchExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))) { + if ((!section.isUsable() && !(section.isFirstStop(prediction) && section.previousSection().isUsable() && section.previousSection().shouldIncludeNextStationOfNextSection())) || (section.getTrainGroup().map(x -> settings.searchExcludedTrainGroups.getValue().contains(x.getGroupName())).orElse(false))) { continue; } TrainTravelSection previousSection = section.previousSection(); boolean isStart = section.isFirstStop(prediction); - boolean isStartAndFinal = isStart && previousSection.isUsable() && previousSection.shouldIncludeNextStationOfNextSection() && (previousSection.getTrainGroup() == null || !settings.searchExcludedTrainGroups.getValue().contains(previousSection.getTrainGroup().getGroupName())); + boolean isStartAndFinal = isStart && previousSection.isUsable() && previousSection.shouldIncludeNextStationOfNextSection() && (previousSection.getTrainGroup().map(x -> !settings.searchExcludedTrainGroups.getValue().contains(x.getGroupName())).orElse(true)); TrainStop stop = new TrainStop(prediction); stop.simulateTicks(settings.searchDepartureInTicks.getValue()); @@ -801,7 +812,7 @@ public static record DepartureRoutesData(String stationTagName, UUID player) {} Route route = new Route(List.of(new RoutePart(data.getSessionId(), train.id, List.of(stop /* current/target */, from /* from */), section.getAllStops(settings.searchDepartureInTicks.getValue(), prediction.getEntryIndex()))), false); - if ((!isStart || isStartAndFinal) && (section.getTrainGroup() == null || !settings.searchExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))) { + if ((!isStart || isStartAndFinal) && (section.getTrainGroup().map(x -> !settings.searchExcludedTrainGroups.getValue().contains(x.getGroupName())).orElse(true))) { Route selectedRoute = route; if (isStartAndFinal) { @@ -811,7 +822,7 @@ public static record DepartureRoutesData(String stationTagName, UUID player) {} } routesL.add(Pair.of(true, selectedRoute)); // Arrival } - if ((section.isUsable()) && (section.getTrainGroup() == null || !settings.searchExcludedTrainGroups.getValue().contains(section.getTrainGroup().getGroupName()))) { + if ((section.isUsable()) && (section.getTrainGroup().map(x -> !settings.searchExcludedTrainGroups.getValue().contains(x.getGroupName())).orElse(true))) { routesL.add(Pair.of(false, route)); // Departure } } @@ -866,8 +877,10 @@ public static record DeparturesData(UUID stationTagId, UUID trainId) {} return false; } StationTag tag = GlobalSettings.getInstance().getStationTag(in.stationTagId()).get(); - ListTag list = new ListTag(); - list.addAll(TrainUtils.getDeparturesAt(tag, in.trainId()).stream().map(x -> x.toNbt(true)).toList()); + ListTag list = new ListTag(); + for (TrainStop stop : TrainUtils.getDeparturesAt(tag, in.trainId())) { + list.add(stop.toNbt(true)); + } nbt.put(DataAccessorType.DEFAULT_NBT_DATA, list); } catch (Exception e) { CreateRailwaysNavigator.LOGGER.error("Next connections error.", e); @@ -890,7 +903,9 @@ public static record DeparturesData(UUID stationTagId, UUID trainId) {} public static final DataAccessorType, List> GET_ALL_TRAINS_DEBUG_DATA = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_all_trains_debug_data"), DataAccessorType.Builder.createNoInput( (player, in, temp, nbt, iteration) -> { ListTag list = new ListTag(); - list.addAll(TrainListener.data.values().stream().map(x -> TrainDebugData.fromTrain(x).toNbt()).toList()); + for (TrainData x : TrainListener.data.values()) { + list.add(TrainDebugData.fromTrain(x).toNbt()); + } nbt.put(DataAccessorType.DEFAULT_NBT_DATA, list); return false; }, (hasMore, data, iteration, nbt) -> { @@ -930,6 +945,19 @@ public static record DeparturesData(UUID stationTagId, UUID trainId) {} return false; } )); + + public static final DataAccessorType GET_STATION_DEPARTURE_HISTORY = DataAccessorType.register(new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "get_station_departure_history"), DataAccessorType.Builder.create( + (in, nbt) -> { + nbt.putString(DataAccessorType.DEFAULT_NBT_DATA, in); + }, (nbt) -> { + return nbt.getString(DataAccessorType.DEFAULT_NBT_DATA); + }, (player, in, temp, nbt, iteration) -> { + nbt.put(DataAccessorType.DEFAULT_NBT_DATA, new StationStats(in).toNbt()); + return false; + }, (hasMore, data, iteration, nbt) -> { + return StationStats.fromNbt(nbt.getCompound(DataAccessorType.DEFAULT_NBT_DATA)); + } + )); public static void init() {} } diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java b/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java index 89b8ab72..36afa128 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModBlocks.java @@ -10,6 +10,7 @@ import com.tterrag.registrate.util.nullness.NonNullConsumer; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.AbstractAdvancedDisplayBlock; import de.mrjulsen.crn.block.AdvancedDisplayBlock; import de.mrjulsen.crn.block.AdvancedDisplayBoardBlock; import de.mrjulsen.crn.block.AdvancedDisplayHalfPanelBlock; @@ -34,7 +35,9 @@ public class ModBlocks { public static final BlockEntry ADVANCED_DISPLAY_BLOCK = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_block", AdvancedDisplayBlock::new) .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY_ALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY_ALL_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) @@ -44,7 +47,9 @@ public class ModBlocks { .register(); public static final BlockEntry ADVANCED_DISPLAY_SLAB = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_slab", AdvancedDisplaySlabBlock::new) .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL_BORDER, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) @@ -54,7 +59,9 @@ public class ModBlocks { public static final BlockEntry ADVANCED_DISPLAY = CreateRailwaysNavigator.REGISTRATE.block("advanced_display", AdvancedDisplayBoardBlock::new) .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY))) + .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) @@ -64,7 +71,9 @@ public class ModBlocks { public static final BlockEntry ADVANCED_DISPLAY_SMALL = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_small", AdvancedDisplaySmallBlock::new) .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL_BORDER, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) @@ -73,8 +82,10 @@ public class ModBlocks { .register(); public static final BlockEntry ADVANCED_DISPLAY_PANEL = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_panel", AdvancedDisplayPanelBlock::new) - .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY))) + .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY))) + .onRegister(connectedTextures(() -> new AdvancedDisplayCTBehaviour(ClientWrapper.CT_ADVANCED_DISPLAY_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) @@ -83,8 +94,10 @@ public class ModBlocks { .register(); public static final BlockEntry ADVANCED_DISPLAY_HALF_PANEL = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_half_panel", AdvancedDisplayHalfPanelBlock::new) - .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL_BORDER, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) @@ -93,8 +106,10 @@ public class ModBlocks { .register(); public static final BlockEntry ADVANCED_DISPLAY_SLOPED = CreateRailwaysNavigator.REGISTRATE.block("advanced_display_sloped", AdvancedDisplaySlopedBlock::new) - .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL))) + .onRegister(connectedTextures(() -> new AdvancedDisplaySmallCTBehaviour(ClientWrapper.CT_HORIZONTAL_ADVANCED_DISPLAY_SMALL_BORDER, ClientWrapper.CT_ADVANCED_DISPLAY_SMALL_BORDER))) .addLayer(() -> RenderType::cutout) + .color(() -> AbstractAdvancedDisplayBlock::getDisplayColor) .initialProperties(SharedProperties::softMetal) .transform(TagGen.pickaxeOnly()) .onRegister(AllDisplayBehaviours.assignDataBehaviour(new AdvancedDisplayTarget())) diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java b/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java index 95edd1e1..0e72c00e 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModDisplayTypes.java @@ -1,11 +1,20 @@ package de.mrjulsen.crn.registry; -import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.block.display.properties.DepartureBoardDisplayTableSettings; +import de.mrjulsen.crn.block.display.properties.PassengerInformationDetailedSettings; +import de.mrjulsen.crn.block.display.properties.PassengerInformationScrollingTextSettings; +import de.mrjulsen.crn.block.display.properties.PlatformDisplayFocusSettings; +import de.mrjulsen.crn.block.display.properties.PlatformDisplayScrollingTextSettings; +import de.mrjulsen.crn.block.display.properties.PlatformDisplayTableSettings; +import de.mrjulsen.crn.block.display.properties.TrainDestinationCompactSettings; +import de.mrjulsen.crn.block.display.properties.TrainDestinationDetailedSettings; +import de.mrjulsen.crn.block.display.properties.TrainDestinationExtendedSettings; import de.mrjulsen.crn.block.properties.EDisplayInfo; import de.mrjulsen.crn.block.properties.EDisplayType; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry; -import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeInfo; +import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayProperties; import de.mrjulsen.crn.client.AdvancedDisplaysRegistry.DisplayTypeResourceKey; +import de.mrjulsen.crn.client.ber.variants.BERDepartureBoardTable; import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoInformative; import de.mrjulsen.crn.client.ber.variants.BERPassengerInfoSimple; import de.mrjulsen.crn.client.ber.variants.BERPlatformDetailed; @@ -14,64 +23,67 @@ import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationDetailed; import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationInformative; import de.mrjulsen.crn.client.ber.variants.BERTrainDestinationSimple; -import net.minecraft.resources.ResourceLocation; public final class ModDisplayTypes { - public static final DisplayTypeResourceKey PASSENGER_INFORMATION_RUNNING_TEXT = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.PASSENGER_INFORMATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "running_text"), - BERPassengerInfoSimple::new, new DisplayTypeInfo(true, null)); + public static final DisplayTypeResourceKey PASSENGER_INFORMATION_RUNNING_TEXT = AdvancedDisplaysRegistry.register( + EDisplayType.PASSENGER_INFORMATION, "running_text", + PassengerInformationScrollingTextSettings::new, BERPassengerInfoSimple::new, new DisplayProperties(true, null)); - public static final DisplayTypeResourceKey PASSENGER_INFORMATION_OVERVIEW = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.PASSENGER_INFORMATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "detailed_with_schedule"), - BERPassengerInfoInformative::new, new DisplayTypeInfo(false, null)); + public static final DisplayTypeResourceKey PASSENGER_INFORMATION_OVERVIEW = AdvancedDisplaysRegistry.register( + EDisplayType.PASSENGER_INFORMATION, "detailed_with_schedule", + PassengerInformationDetailedSettings::new, BERPassengerInfoInformative::new, new DisplayProperties(false, null)); - public static final DisplayTypeResourceKey TRAIN_DESTINATION_SIMPLE = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.TRAIN_DESTINATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "simple"), - BERTrainDestinationSimple::new, new DisplayTypeInfo(true, null)); + public static final DisplayTypeResourceKey TRAIN_DESTINATION_SIMPLE = AdvancedDisplaysRegistry.register( + EDisplayType.TRAIN_DESTINATION, "simple", + TrainDestinationCompactSettings::new, BERTrainDestinationSimple::new, new DisplayProperties(true, null)); - public static final DisplayTypeResourceKey TRAIN_DESTINATION_DETAILED = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.TRAIN_DESTINATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "extended"), - BERTrainDestinationDetailed::new, new DisplayTypeInfo(true, null)); + public static final DisplayTypeResourceKey TRAIN_DESTINATION_DETAILED = AdvancedDisplaysRegistry.register( + EDisplayType.TRAIN_DESTINATION, "extended", + TrainDestinationExtendedSettings::new, BERTrainDestinationDetailed::new, new DisplayProperties(true, null)); - public static final DisplayTypeResourceKey TRAIN_DESTINATION_OVERVIEW = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.TRAIN_DESTINATION, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "detailed"), - BERTrainDestinationInformative::new, new DisplayTypeInfo(true, null)); + public static final DisplayTypeResourceKey TRAIN_DESTINATION_OVERVIEW = AdvancedDisplaysRegistry.register( + EDisplayType.TRAIN_DESTINATION, "detailed", + TrainDestinationDetailedSettings::new, BERTrainDestinationInformative::new, new DisplayProperties(true, null)); - public static final DisplayTypeResourceKey PLATFORM_RUNNING_TEXT = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.PLATFORM, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "running_text"), - BERPlatformSimple::new, new DisplayTypeInfo(true, be -> 32)); + public static final DisplayTypeResourceKey PLATFORM_RUNNING_TEXT = AdvancedDisplaysRegistry.register( + EDisplayType.PLATFORM, "running_text", + PlatformDisplayScrollingTextSettings::new, BERPlatformSimple::new, new DisplayProperties(true, be -> 16)); - public static final DisplayTypeResourceKey PLATFORM_TABLE = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.PLATFORM, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "table"), - BERPlatformDetailed::new, new DisplayTypeInfo(false, be -> be.getYSize() * 3 - 1)); + public static final DisplayTypeResourceKey PLATFORM_TABLE = AdvancedDisplaysRegistry.register( + EDisplayType.PLATFORM, "table", + PlatformDisplayTableSettings::new, BERPlatformDetailed::new, new DisplayProperties(false, be -> be.getYSize() * 3 - 1)); - public static final DisplayTypeResourceKey PLATFORM_FOCUS = AdvancedDisplaysRegistry.registerDisplayType( - EDisplayType.PLATFORM, new ResourceLocation(CreateRailwaysNavigator.MOD_ID, "focus"), - BERPlatformInformative::new, new DisplayTypeInfo(false, be -> be.getYSize() * 3 - 2)); + public static final DisplayTypeResourceKey PLATFORM_FOCUS = AdvancedDisplaysRegistry.register( + EDisplayType.PLATFORM, "focus", + PlatformDisplayFocusSettings::new, BERPlatformInformative::new, new DisplayProperties(false, be -> be.getYSize() * 3 - 2)); + + public static final DisplayTypeResourceKey DEPARTURE_BOARD_TABLE = AdvancedDisplaysRegistry.register( + EDisplayType.DEPARTURE_BOARD, "table", + DepartureBoardDisplayTableSettings::new, BERDepartureBoardTable::new, new DisplayProperties(false, be -> be.getYSize() * 3 - 1)); @Deprecated public static DisplayTypeResourceKey legacy_getKeyForType(EDisplayType type, EDisplayInfo info) { switch (type) { case PASSENGER_INFORMATION -> { - switch (info) { - case INFORMATIVE -> { return PASSENGER_INFORMATION_OVERVIEW; } - default -> { return PASSENGER_INFORMATION_RUNNING_TEXT; } - } + return switch (info) { + case INFORMATIVE -> PASSENGER_INFORMATION_OVERVIEW; + default -> PASSENGER_INFORMATION_RUNNING_TEXT; + }; } case TRAIN_DESTINATION -> { - switch (info) { - case DETAILED -> { return TRAIN_DESTINATION_DETAILED; } - case INFORMATIVE -> { return TRAIN_DESTINATION_OVERVIEW; } - default -> { return TRAIN_DESTINATION_SIMPLE; } - } + return switch (info) { + case DETAILED -> TRAIN_DESTINATION_DETAILED; + case INFORMATIVE -> TRAIN_DESTINATION_OVERVIEW; + default -> TRAIN_DESTINATION_SIMPLE; + }; } case PLATFORM -> { - switch (info) { - case DETAILED -> { return PLATFORM_TABLE; } - case INFORMATIVE -> { return PLATFORM_FOCUS; } - default -> { return PLATFORM_RUNNING_TEXT; } - } + return switch (info) { + case DETAILED -> PLATFORM_TABLE; + case INFORMATIVE -> PLATFORM_FOCUS; + default -> PLATFORM_RUNNING_TEXT; + }; } default -> { return TRAIN_DESTINATION_SIMPLE; } } diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java b/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java index 83a424dd..382fad91 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModSchedule.java @@ -9,6 +9,7 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.data.schedule.condition.DynamicDelayCondition; +import de.mrjulsen.crn.data.schedule.condition.TrainSeparationCondition; import de.mrjulsen.crn.data.schedule.instruction.ResetTimingsInstruction; import de.mrjulsen.crn.data.schedule.instruction.TravelSectionInstruction; import net.minecraft.resources.ResourceLocation; @@ -20,6 +21,7 @@ public class ModSchedule { registerInstruction("reset_timings", ResetTimingsInstruction::new); registerCondition("dynamic_delay", DynamicDelayCondition::new); + registerCondition("train_separation", TrainSeparationCondition::new); } private static void registerInstruction(String name, Supplier factory) { diff --git a/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java b/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java index e9f23df3..d6e7481f 100644 --- a/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java +++ b/common/src/main/java/de/mrjulsen/crn/registry/ModTrainStatusInfos.java @@ -1,11 +1,9 @@ package de.mrjulsen.crn.registry; -import java.util.Optional; - import com.simibubi.create.content.trains.entity.Train; import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.client.lang.ELanguage; +import de.mrjulsen.crn.client.lang.CustomLanguage; import de.mrjulsen.crn.data.train.TrainListener; import de.mrjulsen.crn.data.train.TrainStatus; import de.mrjulsen.crn.data.train.TrainStatus.Registry; @@ -17,8 +15,9 @@ public final class ModTrainStatusInfos { public static final Registry REGISTRY = Registry.create(CreateRailwaysNavigator.MOD_ID); + // TODO Translation keys erst auf dem client übersetzen! // Custom - public static final TrainStatus RED_SIGNAL = REGISTRY.register("red_signal", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.red_signal"), (data) -> { + public static final TrainStatus RED_SIGNAL = REGISTRY.register("red_signal", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.red_signal"), (data) -> { return data.isCurrentSectionDelayed() && data.getTrain().navigation.waitingForSignal != null && data.getTrain().navigation.waitingForSignal.getSecond() && @@ -26,49 +25,59 @@ public final class ModTrainStatusInfos { ; })); - public static final TrainStatus PRIORITY_OTHER_TRAIN = REGISTRY.register("priority_other_train", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.priority_other_train"), (data) -> { + public static final TrainStatus PRIORITY_OTHER_TRAIN = REGISTRY.register("priority_other_train", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.priority_other_train"), (data) -> { if (!data.isCurrentSectionDelayed() || data.getTrain().navigation.waitingForSignal == null) { return false; } - Optional occupyingTrain = data.occupyingTrains.stream().findFirst(); - if (!occupyingTrain.isPresent()) { + if (data.occupyingTrains.isEmpty()) { return false; } - return TrainListener.data.containsKey(occupyingTrain.get().id) ? !TrainListener.data.get(occupyingTrain.get().id).isDelayed() : false; + Train occupyingTrain = null; + for (Train t : data.occupyingTrains) { + occupyingTrain = t; + break; + } + + return TrainListener.data.containsKey(occupyingTrain.id) ? !TrainListener.data.get(occupyingTrain.id).isDelayed() : false; })); - public static final TrainStatus PERVIOUS_TRAIN_DELAYED = REGISTRY.register("previous_train_delayed", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.delay_other_train"), (data) -> { + public static final TrainStatus PERVIOUS_TRAIN_DELAYED = REGISTRY.register("previous_train_delayed", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.delay_other_train"), (data) -> { if (!data.isCurrentSectionDelayed() || data.getTrain().navigation.waitingForSignal == null) { return false; - } + } - Optional occupyingTrain = data.occupyingTrains.stream().findFirst(); - if (!occupyingTrain.isPresent()) { + if (data.occupyingTrains.isEmpty()) { return false; } - return TrainListener.data.containsKey(occupyingTrain.get().id) ? TrainListener.data.get(occupyingTrain.get().id).isDelayed() : false; + Train occupyingTrain = null; + for (Train t : data.occupyingTrains) { + occupyingTrain = t; + break; + } + + return TrainListener.data.containsKey(occupyingTrain.id) ? TrainListener.data.get(occupyingTrain.id).isDelayed() : false; })); - public static final TrainStatus TRACK_CLOSED = REGISTRY.register("track_closed", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.track_closed"), (data) -> { + public static final TrainStatus TRACK_CLOSED = REGISTRY.register("track_closed", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.track_closed"), (data) -> { return data.isCurrentSectionDelayed() && ((TrainStatusAccessor)data.getTrain().status).crn$track(); })); - public static final TrainStatus STAFF_SHORTAGE = REGISTRY.register("staff_shortage", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.staff_shortage"), (data) -> { + public static final TrainStatus STAFF_SHORTAGE = REGISTRY.register("staff_shortage", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.staff_shortage"), (data) -> { return data.isCurrentSectionDelayed() && ((TrainStatusAccessor)data.getTrain().status).crn$conductor(); })); - public static final TrainStatus OPERATIONAL_DISRUPTION = REGISTRY.register("operational_disruption", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.operational_disruption"), (data) -> { + public static final TrainStatus OPERATIONAL_DISRUPTION = REGISTRY.register("operational_disruption", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.operational_disruption"), (data) -> { return data.isCurrentSectionDelayed() && ((TrainStatusAccessor)data.getTrain().status).crn$navigation(); })); - public static final TrainStatus SPECIAL_JOURNEY = REGISTRY.register("special_journey", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.special_trip"), (data) -> { + public static final TrainStatus SPECIAL_JOURNEY = REGISTRY.register("special_journey", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("gui." + CreateRailwaysNavigator.MOD_ID + ".train_status.special_trip"), (data) -> { return false; })); - public static final TrainStatus OUT_OF_SERVICE = REGISTRY.register("out_of_service", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, (data) -> ELanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service"), (data) -> { + public static final TrainStatus OUT_OF_SERVICE = REGISTRY.register("out_of_service", new TrainStatus(TrainStatusCategory.TRAIN, TrainStatusType.DELAY, () -> CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.not_in_service"), (data) -> { return data.isCurrentSectionDelayed() && data.getTrain().runtime.paused; })); diff --git a/common/src/main/java/de/mrjulsen/crn/util/IListenable.java b/common/src/main/java/de/mrjulsen/crn/util/IListenable.java index 09ca49b2..42c8d66a 100644 --- a/common/src/main/java/de/mrjulsen/crn/util/IListenable.java +++ b/common/src/main/java/de/mrjulsen/crn/util/IListenable.java @@ -1,9 +1,10 @@ package de.mrjulsen.crn.util; -import java.util.ArrayList; import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.Map; import java.util.function.Consumer; +import java.util.stream.Collectors; public interface IListenable { @@ -32,7 +33,7 @@ default int eventsCount() { default int listenersCount(String name) { if (!hasEvent(name)) { - throw new IllegalArgumentException("This listener event does not exist: " + name); + throw new IllegalArgumentException("This listener event does not exist: " + name + ". Valid events are: [" + getListeners().keySet().stream().collect(Collectors.joining(", ")) + "]"); } return getListeners().get(name).size(); @@ -44,7 +45,7 @@ default boolean hasEvent(String name) { default void listen(String name, Object listenerObject, Consumer listener) { if (!hasEvent(name)) { - throw new IllegalArgumentException("This listener event does not exist: " + name); + throw new IllegalArgumentException("This listener event does not exist: " + name + ". Valid events are: [" + getListeners().keySet().stream().collect(Collectors.joining(", ")) + "]"); } getListeners().get(name).put(listenerObject, listener); @@ -52,25 +53,30 @@ default void listen(String name, Object listenerObject, Consumer listener) { default void stopListening(String name, Object listenerObject) { if (!hasEvent(name)) { - throw new IllegalArgumentException("This listener event does not exist: " + name); + throw new IllegalArgumentException("This listener event does not exist: " + name + ". Valid events are: [" + getListeners().keySet().stream().collect(Collectors.joining(", ")) + "]"); } getListeners().get(name).remove(listenerObject); } - default void stopListeningAll(Object listenerObject) { - getListeners().values().stream().forEach(x -> { + default void stopListeningAll(Object listenerObject) { + Iterator>> iterator = getListeners().values().iterator(); + while (iterator.hasNext()) { + Map> x = iterator.next(); if (x.containsKey(listenerObject)) { - x.remove(listenerObject); + iterator.remove(); } - }); + } } default void notifyListeners(String name, T data) { if (!hasEvent(name)) { - throw new IllegalArgumentException("This listener event does not exist: " + name); + throw new IllegalArgumentException("This listener event does not exist: " + name + ". Valid events are: [" + getListeners().keySet().stream().collect(Collectors.joining(", ")) + "]"); } - new ArrayList<>(getListeners().get(name).values()).stream().forEach(x -> x.accept(data)); + Iterator> iterator = getListeners().get(name).values().iterator(); + while (iterator.hasNext()) { + iterator.next().accept(data); + } } } diff --git a/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java b/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java index 84673a5d..7508b277 100644 --- a/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java +++ b/common/src/main/java/de/mrjulsen/crn/util/ModUtils.java @@ -1,5 +1,6 @@ package de.mrjulsen.crn.util; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -10,25 +11,27 @@ import de.mrjulsen.crn.config.ModClientConfig; import de.mrjulsen.crn.exceptions.RuntimeSideException; -import de.mrjulsen.crn.web.WebsitePreparableReloadListener; import de.mrjulsen.mcdragonlib.DragonLib; +import de.mrjulsen.mcdragonlib.config.ECachingPriority; +import de.mrjulsen.mcdragonlib.data.Cache; import de.mrjulsen.mcdragonlib.util.TextUtils; import de.mrjulsen.mcdragonlib.util.TimeUtils; import dev.architectury.platform.Platform; import dev.architectury.utils.Env; import net.minecraft.network.chat.MutableComponent; import net.minecraft.util.Mth; +import net.minecraft.world.item.DyeColor; public class ModUtils { - private static WebsitePreparableReloadListener websitemanager; + private static final Cache dyeColorsCache = new Cache<>(() -> Arrays.stream(DyeColor.values()).mapToInt(x -> x == DyeColor.ORANGE ? 0xFFFF9900 : (0xFF << 24) | (x.getTextColor() & 0x00FFFFFF)).toArray(), ECachingPriority.LOW); public static float clockHandDegrees(long time, int divisor) { return 360.0F / divisor * (time % divisor); } public static double calcSpeed(double metersPerTick, ESpeedUnit unit) { - return metersPerTick * DragonLib.TPS * unit.getFactor(); + return metersPerTick * 20 * unit.getFactor(); // TODO: DragonLib minecraftTps() for the game tick rate } public static MutableComponent calcSpeedString(double metersPerTick, ESpeedUnit unit) { @@ -94,14 +97,6 @@ public static long generateId(Predicate exists) { return id; } - public static void setWebsiteResourceManager(WebsitePreparableReloadListener manager) { - websitemanager = manager; - } - - public static WebsitePreparableReloadListener getWebsiteResourceManager() { - return websitemanager; - } - /** Client-side only! */ public static String formatTime(long time, boolean asETA) throws RuntimeSideException { if (Platform.getEnvironment() != Env.CLIENT) { @@ -110,6 +105,10 @@ public static String formatTime(long time, boolean asETA) throws RuntimeSideExce if (asETA) { return timeRemainingString(time - DragonLib.getCurrentWorldTime()); } - return TimeUtils.parseTime((time + DragonLib.DAYTIME_SHIFT) % DragonLib.TICKS_PER_DAY, ModClientConfig.TIME_FORMAT.get()); + return TimeUtils.parseTime((time + DragonLib.daytimeShift()) % DragonLib.ticksPerDay(), ModClientConfig.TIME_FORMAT.get()); + } + + public static int[] getDyeColors() { + return dyeColorsCache.get(); } } diff --git a/common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java b/common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java deleted file mode 100644 index 82812505..00000000 --- a/common/src/main/java/de/mrjulsen/crn/web/SimpleWebServer.java +++ /dev/null @@ -1,274 +0,0 @@ -package de.mrjulsen.crn.web; - -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -import de.mrjulsen.crn.CreateRailwaysNavigator; -import de.mrjulsen.crn.util.ModUtils; -import de.mrjulsen.crn.data.storage.GlobalSettings; -import de.mrjulsen.crn.event.ModCommonEvents; -import de.mrjulsen.crn.data.navigation.NavigatableGraph; -import de.mrjulsen.crn.data.navigation.Route; -import de.mrjulsen.mcdragonlib.DragonLib; -import de.mrjulsen.mcdragonlib.util.DLUtils; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.PackType; - -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsServer; -import com.google.common.net.MediaType; -import com.sun.net.httpserver.HttpExchange; - -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; - -public class SimpleWebServer { - - static HttpServer server; - - public static void start() throws Exception { - /* - server = HttpServer.create(new InetSocketAddress(80), 0); - server.createContext("/hello", new HelloWorldHandler()); - server.createContext("/api/" + CreateRailwaysNavigator.MOD_ID + "/navigate", new V1NavigationHandler()); - server.createContext("/" + CreateRailwaysNavigator.MOD_ID, new WebsiteHandler(CreateRailwaysNavigator.MOD_ID)); - server.createContext("/" + CreateRailwaysNavigator.SHORT_MOD_ID, new WebsiteHandler(CreateRailwaysNavigator.SHORT_MOD_ID)); - server.createContext("/status", new TestHandler()); - server.setExecutor(null); // creates a default executor - server.start(); - */ - } - - public static void stop() { - DLUtils.doIfNotNull(server, x -> { - x.stop(0); - }); - } - - public static Map> parseQueryParameters(HttpExchange ex, Charset charset) { - String queryString = ex.getRequestURI().getRawQuery(); - if (queryString == null || queryString.isEmpty()) { - return Collections.emptyMap(); - } - Map> parsedParams = new TreeMap>(); - for (String param : queryString.split("&")) { - String[] parts = param.split("=", 2); - String key = parts[0]; - String value = parts.length == 2 ? parts[1] : ""; - try { - key = URLDecoder.decode(key, charset.name()); - value = URLDecoder.decode(value, charset.name()); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - List values = parsedParams.get(key); - if (values == null) { - values = new LinkedList(); - parsedParams.put(key, values); - } - values.add(value); - } - - for (Map.Entry> me : parsedParams.entrySet()) { - me.setValue(Collections.unmodifiableList(me.getValue())); - } - return Collections.unmodifiableMap(parsedParams); - } - - private static void startResponse(HttpExchange ex, int code, MediaType contentType, boolean hasBody) throws IOException { - if (contentType != null) { - ex.getResponseHeaders().set("Content-Type", contentType.type()); - } - if (!hasBody) { // No body. Required for HEAD requests - ex.sendResponseHeaders(code, -1); - } else { // Chuncked encoding - ex.sendResponseHeaders(code, 0); - } - } - - private static void sendError(HttpExchange ex, int code, String msg) { - CreateRailwaysNavigator.LOGGER.warn(msg); - try { - respond(ex, code, MediaType.PLAIN_TEXT_UTF_8, msg.getBytes()); - } catch (IOException e) { - CreateRailwaysNavigator.LOGGER.error("Unable to send error response.", e); - } - } - - private static void respond(HttpExchange ex, int code, MediaType contentType, byte response[]) throws IOException { - startResponse(ex, code, contentType, response != null); - if (response != null) { - OutputStream responseBody = ex.getResponseBody(); - responseBody.write(response); - responseBody.flush(); - responseBody.close(); - } - ex.close(); - } - - public static void sendRedirect(HttpExchange ex, URI location) throws IOException { - ex.getResponseHeaders().set("Location", location.toString()); - respond(ex, HttpURLConnection.HTTP_SEE_OTHER, null, null); - } - - public static URI getRequestUri(HttpExchange ex) { - String host = ex.getRequestHeaders().getFirst("Host"); - if (host == null) { // Client must be using HTTP/1.0 - CreateRailwaysNavigator.LOGGER.warn("Request did not provide Host header, using 'localhost' as hostname"); - int port = ex.getHttpContext().getServer().getAddress().getPort(); - host = "localhost:" + port; - } - String protocol = (ex.getHttpContext().getServer() instanceof HttpsServer) ? "https" : "http"; - URI base; - try { - base = new URI(protocol, host, "/", null, null); - } catch (URISyntaxException e) { - throw new IllegalStateException(e); - } - URI requestedUri = ex.getRequestURI(); - requestedUri = base.resolve(requestedUri); - return requestedUri; - } - - public static void redirectTo(HttpExchange ex, String redirect) { - URI base = getRequestUri(ex); - URI path; - try { - path = new URI(redirect); - sendRedirect(ex, base.resolve(path)); - } catch (URISyntaxException | IOException e) { - CreateRailwaysNavigator.LOGGER.error("Could not construct URI.", e); - } - } - - - - static class HelloWorldHandler implements HttpHandler { - - @Override - public void handle(HttpExchange t) throws IOException { - String response = "Hello World! " + CreateRailwaysNavigator.MOD_ID; - t.sendResponseHeaders(200, response.length()); - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } - } - - static class WebsiteHandler implements HttpHandler { - - private final String subUrl; - private final int subUrlLength; - - public WebsiteHandler(String subUrl) { - this.subUrl = subUrl; - this.subUrlLength = subUrl.length() + 1; - } - - @Override - public void handle(HttpExchange t) throws IOException { - - String requestedPath = t.getRequestURI().getPath(); - if (requestedPath.startsWith("/" + subUrl) && requestedPath.length() >= subUrlLength) { - requestedPath = requestedPath.substring(subUrlLength); - } else { - sendError(t, HttpURLConnection.HTTP_BAD_REQUEST, "The requested URL is invalid: " + requestedPath); - } - - if (requestedPath.isBlank()) { - redirectTo(t, "/" + subUrl + "/"); - return; - } - - if (requestedPath.equals("/")) { - requestedPath = "/index.html"; // Set default page to index.html - } - - CreateRailwaysNavigator.LOGGER.info("A web service requested a resource: " + requestedPath); - Optional fileData = ModUtils.getWebsiteResourceManager().getFileBytesFor(requestedPath); - - if (!fileData.isPresent()) { - sendError(t, HttpURLConnection.HTTP_NOT_FOUND, "The requested resource does not exist: " + requestedPath); - /* - String response = "404 (Not Found)\n" + requestedPath + " does not exist."; - t.sendResponseHeaders(404, response.length()); - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - */ - } else { - respond(t, HttpURLConnection.HTTP_OK, null, fileData.get()); - /* - byte[] fileBytes = fileData.get(); - System.out.println("BYTES: " + fileBytes.length); - t.sendResponseHeaders(200, 0); - OutputStream os = t.getResponseBody(); - os.write(fileBytes); - os.close(); - */ - } - } - } - - static class TestHandler implements HttpHandler { - @Override - public void handle(HttpExchange t) throws IOException { - String response = "PACKS"; - for (String str : ModCommonEvents.getCurrentServer().get().getPackRepository().getAvailableIds()) { - response += "\n - " + str; - } - response += "\nCRN PACK"; - for (ResourceLocation str : ModCommonEvents.getCurrentServer().get().getPackRepository().getPack("mod:" + CreateRailwaysNavigator.MOD_ID).open().getResources(PackType.SERVER_DATA, CreateRailwaysNavigator.MOD_ID, "", (str) -> true)) { - response += "\n - " + str; - } - t.sendResponseHeaders(200, response.length()); - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } - } - - static class V1NavigationHandler implements HttpHandler { - - private static final String KEY_START = "start"; - private static final String KEY_DESTINATION = "destination"; - - @Override - public void handle(HttpExchange t) throws IOException { - Map> params = parseQueryParameters(t, StandardCharsets.UTF_8); - - if (!params.containsKey(KEY_START) || !params.containsKey(KEY_DESTINATION)) { - sendError(t, HttpURLConnection.HTTP_BAD_REQUEST, "Wrong parameters."); - } - - try { - - GlobalSettings settings = GlobalSettings.getInstance(); - List routes = NavigatableGraph.searchRoutes(settings.getOrCreateStationTagFor(params.get(KEY_START).get(0)), settings.getOrCreateStationTagFor(params.get(KEY_DESTINATION).get(0)), null, true); - String response = DragonLib.GSON.toJson(routes); - - t.sendResponseHeaders(200, 0); - OutputStream os = t.getResponseBody(); - os.write(response.getBytes()); - os.close(); - - } catch (Exception e) { - CreateRailwaysNavigator.LOGGER.error("DEAD", e); - } - - - } - } -} diff --git a/common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java b/common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java deleted file mode 100644 index cd2b0086..00000000 --- a/common/src/main/java/de/mrjulsen/crn/web/WebsitePreparableReloadListener.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.mrjulsen.crn.web; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Supplier; - -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.server.packs.resources.SimplePreparableReloadListener; -import net.minecraft.util.profiling.ProfilerFiller; - -public class WebsitePreparableReloadListener extends SimplePreparableReloadListener>> { - - public static final String RESOURCE_PATH = "crn_website"; - - private Map> data; - - @Override - protected void apply(Map> object, ResourceManager resourceManager, ProfilerFiller profiler) { - data = object; - } - - @Override - protected Map> prepare(ResourceManager resourceManager, ProfilerFiller profiler) { - - Map> map = new HashMap<>(); - Collection locations = resourceManager.listResources(RESOURCE_PATH, x -> true).keySet(); - - for (ResourceLocation loc : locations) { - final ResourceLocation localLoc = loc; - String path = localLoc.getPath().replace(RESOURCE_PATH, ""); - map.put(path, () -> { - try { - Resource resource = resourceManager.getResourceOrThrow(localLoc); - try (InputStream inputstream = resource.open()) { - return inputstream.readAllBytes(); - } - } catch (IOException e) { - e.printStackTrace(); - return null; - } - }); - } - - return map; - } - - public Optional getFileBytesFor(String path) { - return Optional.ofNullable(data.containsKey(path) ? data.get(path).get() : null); - } - -} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json index 9d9aea12..782a8124 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/bar.json @@ -3,56 +3,45 @@ "advancement.createrailwaysnavigator.navigator.description": "Stelle oan Navi her, um Zugverbindungen zwischn zwoa verschiedenen Bahnhöfen zu fina.", "advancement.createrailwaysnavigator.advanced_display": "Ned gonz 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Verbessere dei Anzeigetafel, um meahr Informationen darzustäin und sie sogar af Zügen zu verwenden.", - - "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", - - "item.createrailwaysnavigator.navigator": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator.tooltip.summary": "Dea _Navigator_ zoagts mögliche _Zugverbindungen_ mid zusätzlichen Informationen, wia _Zwischenhoit_, _Echtzeitdaten_ und meahr.", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "Dea _Navigator_ zoagts mögliche _Zugverbindungen_ mid zusätzlichen Informationen, wia _Zwischenhoit_, _Echtzeitdaten_ und meahr.", "block.createrailwaysnavigator.train_station_clock": "Bahnhofsuhr", "block.createrailwaysnavigator.advanced_display_block": "Verbesserter Anzeige-Block", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", "block.createrailwaysnavigator.advanced_display": "Verbesserte Anzeigetafel", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", "block.createrailwaysnavigator.advanced_display_small": "Kleine verbesserte Anzeigetafel", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", "block.createrailwaysnavigator.advanced_display_panel": "Verbessertes Anzeigepanel", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Hoibs Vabesserts Ozeigpanl", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", "block.createrailwaysnavigator.advanced_display_sloped": "Abgeschrägte Verbesserte Anzeigetafel", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Benutze den Block af _Zügen_ ois _Zugzielanzeigen_ oda _Fahrgastinformationsanzeigen_ oda an _Bahnhöfen_ ois verbesserte _Bahnsteiganzeigen_, de a meahr Informationen anzeigen ois normale Anzeigetafeln.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "R-Klick mid am Schraubenschlüssel", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne a _Menü_, um de Anzeige zu _konfigurieren_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Betriebsfahrt", - - "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Routenanzeig Einstäiunga", - "enum.createrailwaysnavigator.overlay_position": "Ozeigposition", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Ecke om links", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Ecke om rechts", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Ecke undn links", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Ecke undn rechts", - "gui.createrailwaysnavigator.loading.title": "Dadn wern herunterglon...", - "gui.createrailwaysnavigator.overlay_settings.title": "Routenanzeig Einstäiunga", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Details ozeign", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Ozeig entferna", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Eazählerankündigunga aktiviad.", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Eazählerankündigunga deaktiviad.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Eazählerankündigunga deaktiviad.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Benachrichtigunga aktiviad.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Benachrichtigunga deaktiviad.", "gui.createrailwaysnavigator.route_overlay_settings.scale": "Benutzeroberflächenskaliarung", @@ -60,7 +49,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Da Eazähla kündigt wichtig Eaeignisse auf doana Route an, z.B. da naxte Hoid.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Benachrichtigunga", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Eahoide Benachrichtigunga in Form vo Pop-Ups zua wichtign Eaeignisse, z.B. Vaspätunga", - "gui.createrailwaysnavigator.common.expand": "Details ozeign", "gui.createrailwaysnavigator.common.collapse": "Details vabeagn", "gui.createrailwaysnavigator.common.go_back": "Zrugg", @@ -72,9 +60,6 @@ "gui.createrailwaysnavigator.common.search": "Suchn", "gui.createrailwaysnavigator.common.auto": "Autom.", "gui.createrailwaysnavigator.common.server_error": "Fehla beim Ausführn da Aufgob. Schaue in de Servakonsole.", - "gui.createrailwaysnavigator.via": "üba", - - "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "Koa Vabindungn gfundn.", "gui.createrailwaysnavigator.navigator.not_searched": "Bisha nix gsucht.", "gui.createrailwaysnavigator.navigator.searching": "Suach noch Vabindungn...", @@ -89,13 +74,10 @@ "gui.createrailwaysnavigator.navigator.search.tooltip": "Suchn", "gui.createrailwaysnavigator.navigator.location.tooltip": "Nächsglegne Station vo aktuella Position", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Eingobn tauschn", - "gui.createrailwaysnavigator.route_details.title": "Streckendtails", "gui.createrailwaysnavigator.route_details.departure": "Obfahrt in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Umstieg in", "gui.createrailwaysnavigator.route_details.transfer": "Umstieg", - "gui.createrailwaysnavigator.route_details.save_route": "Route speichern", - "gui.createrailwaysnavigator.route_overview.title": "Streckendtails", "gui.createrailwaysnavigator.route_overview.journey_begins": "Ihre Reise ofangt! %s noch %s, Obfahrt %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Ihre Reise ofangt! %s noch %s, Obfahrt %s vo Gleis %s", @@ -138,7 +120,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Sie hom Ihr Zil eareicht!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Mia bedankn uns fia Ihre Reise und wünschn oan scheenen Dog.", "gui.createrailwaysnavigator.route_overview.date": "Dog %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Globale Einstäiunga", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Zum Bearbadn klickn", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Bohhof Dogs", @@ -149,8 +130,6 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Zuagkategorin deann da Organisation vo Zügn (z.B. Regionalvakeah, Feanvakeah, ...). Nutza könna noch desn Kategorin filtern.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Zuag Blacklist", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Schliaße Züg aus, z.B. Güterzüg, Sonderzüg, etc., damit de ned in den Vorschlägn auftaan.", - - "gui.createrailwaysnavigator.station_tags.title": "Bohhof Dogs Einstäiunga", "gui.createrailwaysnavigator.station_tags.summary": "Enthält %s Stationa", "gui.createrailwaysnavigator.station_tags.editor": "Zualetzt vo %s am %s bearbadet", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Nein Eintrog eastäin", @@ -160,22 +139,18 @@ "gui.createrailwaysnavigator.station_tags.hint.station_name": "Bohhofsnama", "gui.createrailwaysnavigator.station_tags.hint.platform": "Gleisbezeichnung", "gui.createrailwaysnavigator.station_tags.enter_name": "Nama eingem", - "gui.createrailwaysnavigator.train_group_settings.title": "Zuagkategorin Einstäiunga", "gui.createrailwaysnavigator.train_group_settings.summary": "Enthält %s Ziag", "gui.createrailwaysnavigator.train_group_settings.editor": "Zualetzt vo %s am %s bearbadet", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Kategorie löschn", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Zuag entferna", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Zuag hizufügn", - "gui.createrailwaysnavigator.blacklist.title": "Bohhof Blacklist", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Zua Blacklist hizufügn", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Vo Blacklist entferna", - "gui.createrailwaysnavigator.train_blacklist.title": "Zuag Blacklist", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Zua Blacklist hizufügn", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Vo Blacklist entferna", - "gui.createrailwaysnavigator.search_settings.title": "Sucheinstäiunga", "gui.createrailwaysnavigator.search_settings.transfer_time": "Minimale Umstiegszeid", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "De minimale Zeid fia oan Umstieg. (1h ~ 50 Reoi Life Sekundn)", @@ -185,28 +160,23 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Olle", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Koa", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Filta zuaruggsetzn", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Hizufügn", - "gui.createrailwaysnavigator.time": "Zeid: %s", "gui.createrailwaysnavigator.time.now": "'etz", "gui.createrailwaysnavigator.time_format.dhm": "%s Doge %s Std. %s min.", "gui.createrailwaysnavigator.time_format.hm": "%s Std. %s min.", - "gui.createrailwaysnavigator.time_format.m": "%s min.", - "gui.createrailwaysnavigator.platform": "Gleis", "gui.createrailwaysnavigator.departure": "Obfahrt", "gui.createrailwaysnavigator.destination": "Zil", "gui.createrailwaysnavigator.line": "Linie", "gui.createrailwaysnavigator.following_trains": "Foigezüg:", - + "gui.createrailwaysnavigator.via": "üba", "gui.createrailwaysnavigator.advanced_display_settings.title": "Vabesserte Ozeig Einstäiunga", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Ozeigtyp", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "De Informadiona, de ozoagd wern soin.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informadionsart", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Gibt an, wia detailliad de Informadiona san.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Beidseitig", - "enum.createrailwaysnavigator.display_info_type": "Informadionsart", "enum.createrailwaysnavigator.display_info_type.description": "Gibt an, wia vui Informadiona dargstäit wern soin.", "enum.createrailwaysnavigator.display_info_type.simple": "Oafach", @@ -215,7 +185,6 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "De wichtigsdn Informadiona mid einign Details, wia z.B Zwischenhoide.", "enum.createrailwaysnavigator.display_info_type.informative": "Informadiv", "enum.createrailwaysnavigator.display_info_type.info.informative": "Zeigt olle Informadiona, de relevant sei könna, und stäit sie möglichst oschaulich dar. Ned fia gloae Ozeign empfohln.", - "enum.createrailwaysnavigator.display_type": "Ozeigtyp", "enum.createrailwaysnavigator.display_type.description": "De Art da Ozeig, wos vo ihrem Zweck obhängt.", "enum.createrailwaysnavigator.display_type.train_destination": "Zuagzielanzeig", @@ -224,26 +193,20 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Repräsentiern de Ozeign, de in Zügn vorkomma. Dargstäit wern da naxte Hoid, de Ausstiegsseitn und (je noch Einstäiung) no weidere Informadiona.", "enum.createrailwaysnavigator.display_type.platform": "Bohsteiganzeig", "enum.createrailwaysnavigator.display_type.info.platform": "De Ozeign wern an Bohsteign vawendt und zeign de naxtn obfahrendn Züg mid zuasätzlichn Informadiona an. Funktioniad ned auf Zügn!", - "enum.createrailwaysnavigator.side": "Seitn", "enum.createrailwaysnavigator.side.description": "De Seitn des Bloggs, auf am de Informadiona ozoagd wern soin.", "enum.createrailwaysnavigator.side.front": "Voadaseitn", "enum.createrailwaysnavigator.side.info.front": "De Informadiona wern grod auf da Voadaseitn dargstäit.", "enum.createrailwaysnavigator.side.both": "Beidseitig", "enum.createrailwaysnavigator.side.info.both": "De Informadiona wern auf beidn Seitn dargstäit.", - "enum.createrailwaysnavigator.time_display": "Zeidanzeig", "enum.createrailwaysnavigator.time_display.description": "Gibt an, in welchem Formad de Zeid oggm werd.", - "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (Obsolut)", - "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (Gschätzte Okunftszeid)", - "create.display_source.advanced_display": "Vabesserte Ozeig", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Zuagnama Spoidenbreadn", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "De Breadn da Spoide fia den Zuagnama in Pixl. (Standard: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Gleis Spoidenbreadn", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "De Breadn da Spoide fia des Gleis in Pixl. (-1 = Autom., Standard: -1)", - - "createrailwaysnavigator.moin": "Servus" + "gui.createrailwaysnavigator.station_tags.title": "Bohhof Dogs Einstäiunga" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json index e690d6a3..5765e4d8 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json @@ -74,11 +74,16 @@ "gui.createrailwaysnavigator.common.true": "Ja", "gui.createrailwaysnavigator.common.false": "Nein", "gui.createrailwaysnavigator.common.search": "Suchen", - "gui.createrailwaysnavigator.common.auto": "Autom.", + "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.max": "Max", "gui.createrailwaysnavigator.common.server_error": "Fehler beim Ausführen der Aufgabe. Schaue in die Serverkonsole.", "gui.createrailwaysnavigator.common.delete": "Löschen", "gui.createrailwaysnavigator.common.add": "Hinzufügen", "gui.createrailwaysnavigator.common.help": "Hilfe erhalten", + "gui.createrailwaysnavigator.common.copy": "Kopieren", + "gui.createrailwaysnavigator.common.paste": "Einfügen", + "gui.createrailwaysnavigator.common.click_to_search": "Klicke zum Durchsuchen", + "gui.createrailwaysnavigator.common.click_to_edit": "Klicke zum Bearbeiten", "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "Keine Verbindungen gefunden.", @@ -93,12 +98,13 @@ "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Globale Einstellungen", "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Sucheinstellungen", "gui.createrailwaysnavigator.navigator.search.tooltip": "Suchen", - "gui.createrailwaysnavigator.navigator.location.tooltip": "Nächsgelegene Station von aktueller Position", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Nächstgelegene Station von aktueller Position", "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Aktualisieren", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Eingaben tauschen", "gui.createrailwaysnavigator.route_details.title": "Streckendetails", "gui.createrailwaysnavigator.route_details.departure": "Abfahrt in", + "gui.createrailwaysnavigator.route_details.arrival": "Ankunft in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Umstieg in", "gui.createrailwaysnavigator.route_details.transfer": "Umstieg", @@ -150,7 +156,7 @@ "gui.createrailwaysnavigator.global_settings.option_alias.title": "Bahnhof Tags", "gui.createrailwaysnavigator.global_settings.option_alias.description": "Fasse einzelne Bahnhöfe zu einem einzigen Bahnhof mit mehreren Bahnsteigen und eigenem Namen zusammen, damit der Navigator Umsteigemöglichkeiten etc. vorschlagen kann.", "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Bahnhof Blacklist", - "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Schließe Stationen aus, die nicht in den Navigtionsergebnissen erscheinen sollen. Diese Stationen werden bei der Suche ignoriert.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Schließe Stationen aus, die nicht in den Navigationsergebnissen erscheinen sollen. Diese Stationen werden bei der Suche ignoriert.", "gui.createrailwaysnavigator.global_settings.train_group.title": "Zugkategorien", "gui.createrailwaysnavigator.global_settings.train_group.description": "Zugkategorien dienen der Organisation von Zügen (z.B. Regionalverkehr, Fernverkehr, ...). Nutzer können nach diesen Kategorien filtern.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Zug Blacklist", @@ -206,6 +212,7 @@ "gui.createrailwaysnavigator.line": "Linie", "gui.createrailwaysnavigator.following_trains": "Folgezüge:", "gui.createrailwaysnavigator.via": "über", + "gui.createrailwaysnavigator.too_small": "Zu klein!", "gui.createrailwaysnavigator.advanced_display_settings.title": "Verbesserte Anzeige Einstellungen", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Anzeigetyp", @@ -213,7 +220,28 @@ "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informationsart", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Gibt an, wie detailliert die Informationen sind.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Beidseitig", + "gui.createrailwaysnavigator.advanced_display_settings.advanced_settings": "Erweiterte Einstellungen", + "gui.createrailwaysnavigator.advanced_display_settings.font_color": "Zeichenfarbe", + "gui.createrailwaysnavigator.advanced_display_settings.back_color": "Hintergrundfarbe", + "gui.createrailwaysnavigator.advanced_display_settings.show_arrival": "Zeige Ankunft der Züge", + "gui.createrailwaysnavigator.advanced_display_settings.show_arrival.description": "Wenn aktiviert oder der Zug anschließend in einen ausgeblendeten Abschnitt fährt (unabhängig von dieser Einstellung), wird die Ankunft des Zuges angezeigt.", + "gui.createrailwaysnavigator.advanced_display_settings.carriage_index": "Wagennummernversatz", + "gui.createrailwaysnavigator.advanced_display_settings.carriage_index.description": "Die Zahl, die zur aktuellen Wagennummer hinzuaddiert wird. (z.B. Wagennummer = 3, Versatz = 5, Ergebnis = 08)", + "gui.createrailwaysnavigator.advanced_display_settings.overwrite_carriage_index": "Überschreiben", + "gui.createrailwaysnavigator.advanced_display_settings.overwrite_carriage_index.description": "Wenn aktiviert, wird der Wert nicht zur aktuellen Wagennummer hinzuaddiert, sondern durch den eingegebenen Wert ersetzt. (z.B. Wagennummer = 3, Versatz = 5, Ergebnis = 05)", + "gui.createrailwaysnavigator.advanced_display_settings.show_stats": "Zeige Zugstatistik", + "gui.createrailwaysnavigator.advanced_display_settings.show_exit": "Zeige Ausstiegsseite", + "gui.createrailwaysnavigator.advanced_display_settings.show_time_and_date": "Zeige Datum und Uhrzeit", + "gui.createrailwaysnavigator.advanced_display_settings.show_connections": "Zeige Anschlüsse", + "gui.createrailwaysnavigator.advanced_display_settings.show_line_color": "Zeige Linienfarbe", + "gui.createrailwaysnavigator.advanced_display_settings.show_line_color.description": "Hebt den Zugname mit der Farbe der aktuellen Linie hervor.", + "enum.createrailwaysnavigator.train_text_components": "Zugbezeichnung", + "enum.createrailwaysnavigator.train_text_components.description": "Text, der auf den Bahnsteiganzeigen verwendet werden sollen.", + "enum.createrailwaysnavigator.train_text_components.all": "Alle", + "enum.createrailwaysnavigator.train_text_components.train_name": "Nur Zugname", + "enum.createrailwaysnavigator.train_text_components.destination": "Nur Ziel", + "enum.createrailwaysnavigator.display_info_type": "Informationsart", "enum.createrailwaysnavigator.display_info_type.description": "Gibt an, wie viele Informationen dargestellt werden sollen.", "enum.createrailwaysnavigator.display_info_type.simple": "Einfach", @@ -231,7 +259,9 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Repräsentieren die Anzeigen, die in Zügen vorkommen. Dargestellt werden der nächste Halt, die Ausstiegsseite und (je nach Einstellung) noch weitere Informationen.", "enum.createrailwaysnavigator.display_type.platform": "Bahnsteiganzeige", "enum.createrailwaysnavigator.display_type.info.platform": "Diese Anzeigen werden an Bahnsteigen verwendet und zeigen die nächsten abfahrenden Züge mit zusätzlichen Informationen an. Funktioniert nicht auf Zügen!", - + "enum.createrailwaysnavigator.display_type.departure_board": "Abfahrtstafel", + "enum.createrailwaysnavigator.display_type.info.departure_board": "Eine Zusammenfassung aller Züge, die an einem Bahnhof abfahren.", + "enum.createrailwaysnavigator.side": "Seite", "enum.createrailwaysnavigator.side.description": "Die Seite des Blocks, auf dem die Informationen angezeigt werden sollen.", "enum.createrailwaysnavigator.side.front": "Vorderseite", @@ -246,11 +276,28 @@ "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (Geschätzte Ankunftszeit)", + "enum.createrailwaysnavigator.train_filter": "Zugfilter", + "enum.createrailwaysnavigator.train_filter.description": "Vergleicht Züge basierend auf das ausgewählte Kriterium, um zu entscheiden, welche Züge berücksichtigt werden sollen.", + "enum.createrailwaysnavigator.train_filter.any": "Beliebiger Zug", + "enum.createrailwaysnavigator.train_filter.info.any": "Jeder Zug wird hier berücksichtigt.", + "enum.createrailwaysnavigator.train_filter.same_line": "Gleiche Linie", + "enum.createrailwaysnavigator.train_filter.info.same_line": "Nut Züge mit der gleichen Zuglinie werden berücksichtigt.", + "enum.createrailwaysnavigator.train_filter.same_group": "Gleiche Kategorie", + "enum.createrailwaysnavigator.train_filter.info.same_group": "Nur Züge mit der gleichen Zugkategorie werden berücksichtigt.", + "create.display_source.advanced_display": "Verbesserte Anzeige", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Zugname Spaltenbreite", - "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "Die Breite der Spalte für den Zugname in Pixel. (Standard: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width_next": "Zugname Breite (Nächste Abfahrt)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width_table": "Zugname Breite (Folgezüge)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "in Blockpixel", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Gleis Spaltenbreite", - "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "Die Breite der Spalte für das Gleis in Pixel. (-1 = Autom., Standard: -1)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width_next": "Gleis Breite (Nächste Abfahrt)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width_table": "Gleis Breite (Folgezüge)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "in Blockpixel", + "gui.createrailwaysnavigator.display_source.advanced_display.info_width": "Infobereich Breite", + "gui.createrailwaysnavigator.display_source.advanced_display.info_width.description": "in Prozent des verbleibenden Platzes", + "gui.createrailwaysnavigator.display_source.advanced_display.stopovers_width": "Zwischenhalte Breite", + "gui.createrailwaysnavigator.display_source.advanced_display.stopovers_width.description": "in Prozent des verbleibenden Platzes", "gui.createrailwaysnavigator.section_settings.title": "Abschnittseinstellungen", "gui.createrailwaysnavigator.section_settings.train_groups": "Zugkategorie zuweisen", @@ -264,15 +311,27 @@ "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Warte: %s..%s", "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "oder mindestens %s", + "createrailwaysnavigator.schedule.condition.train_separation.settings": "Zugverteilung Einstellungen", + "createrailwaysnavigator.schedule.condition.train_separation": "Zugverteilung", + "createrailwaysnavigator.schedule.condition.train_separation.title": "Trennung: %s", + "createrailwaysnavigator.schedule.condition.train_separation.description": "Abfahrt %s nach:", + + "createrailwaysnavigator.schedule.instruction.configure": "Konfigurieren...", "createrailwaysnavigator.schedule.instruction.travel_section": "Neuer Fahrplanabschnitt", "createrailwaysnavigator.schedule.instruction.travel_section.description": "Anfang eines neuen Fahrplanabschnitts.", - "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Konfigurieren...", "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Setze Zugkategorie: ", "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Setze Zuglinie: ", "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Start des nächsten Bereichs einbinden: ", "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Navigierbar: ", - "createrailwaysnavigator.schedule.instruction.reset_timings": "Zeiten zurücksetzen", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Zeiten zurücksetzen", + + "goggles.createrailwaysnavigator.train_listener.departures.title": "Abfahrtverlauf", + "goggles.createrailwaysnavigator.train_listener.departures.any": "Letzte Abfahrt:", + "goggles.createrailwaysnavigator.train_listener.departures.line": "Abfahrt Zuglinie:", + "goggles.createrailwaysnavigator.train_listener.departures.group": "Abfahrt Zugkategorie:", + "goggles.createrailwaysnavigator.train_listener.departures.nothing": "(Kürzlich keine Abfahrten)", + "goggles.createrailwaysnavigator.train_listener.departures.has_more": "+ %s weitere...", "display.createrailwaysnavigator.train_destination.simple": "Kompakt", "display.createrailwaysnavigator.train_destination.extended": "Erweitert", @@ -282,9 +341,10 @@ "display.createrailwaysnavigator.platform.running_text": "Lauftext", "display.createrailwaysnavigator.platform.table": "Tabelle", "display.createrailwaysnavigator.platform.focus": "Fokussiert", + "display.createrailwaysnavigator.departure_board.table": "Tabelle", "gui.createrailwaysnavigator.saved_routes.title": "Gespeicherte Reisen", - "gui.createrailwaysnavigator.schedule_board.title": "Abfahrtenliste", + "gui.createrailwaysnavigator.schedule_board.title": "Abfahrtsliste", "gui.createrailwaysnavigator.station_tags.title": "Bahnhof-Tags", "gui.createrailwaysnavigator.journey_info.title": "Reiseinformation", "gui.createrailwaysnavigator.journey_info.date": "Tag %s", @@ -312,11 +372,12 @@ "gui.createrailwaysnavigator.saved_routes.saved_route": "Gespeicherte Reise", "block.createrailwaysnavigator.advanced_display.ber.arrival": "Ankunft", "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Fällt heute aus!", - "block.createrailwaysnavigator.advanced_display.ber.delayed": "Verspätung ca. %s Minuten", + "block.createrailwaysnavigator.advanced_display.ber.delay_abs_suffix": "Minuten", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Verspätung ca. %s", "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information zu %s: Fällt heute aus!", - "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information zu %s: Verspätung ca. %s Minuten", - "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", fällt heute aus. Wir bitten um Entschuldigung.", - "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", heute circa %s Minuten später.", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information zu %s: Verspätung ca. %s", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": "fällt heute aus. Wir bitten um Entschuldigung.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": "heute circa %s später.", "block.createrailwaysnavigator.advanced_display.ber.reason": "Grund dafür ist eine ", "gui.createrailwaysnavigator.route_widget.show_details": "Details ansehen", "gui.createrailwaysnavigator.route_widget.save": "Speichern", @@ -328,7 +389,7 @@ "gui.createrailwaysnavigator.saved_routes.in_days": "In %s Tagen", "gui.createrailwaysnavigator.saved_route_widget.show_details": "Details ansehen", "gui.createrailwaysnavigator.saved_route_widget.share": "Teilen...", - "gui.createrailwaysnavigator.saved_route_widget.notifications": "Benachrichtigunen aktivieren", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Benachrichtigungen aktivieren", "gui.createrailwaysnavigator.train_status.unknown_delay": "Verzögerungen im Betriebsablauf", "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Verspätung aus vorheriger Fahrt", "gui.createrailwaysnavigator.train_status.red_signal": "Haltzeigendes Signal", @@ -342,7 +403,7 @@ "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Route nicht mehr speichern", "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Popup-Fenster anzeigen", "gui.createrailwaysnavigator.navigator.my_profile": "Mein Profil", - "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Manche Routen können unvollständig sein oder überhaupt nicht vorgeschalgen werden, da Create Railways Navigator noch nicht alle Züge initialisiert hat.", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Manche Routen können unvollständig sein oder überhaupt nicht vorgeschlagen werden, da Create Railways Navigator noch nicht alle Züge initialisiert hat.", "gui.createrailwaysnavigator.global_settings.train_line.title": "Zuglinien", "gui.createrailwaysnavigator.global_settings.train_line.color": "Farbe auswählen", "gui.createrailwaysnavigator.global_settings.train_line.description": "Erstelle Linien, um mehrere Züge zu Linienzügen zusammenzufassen, deren Anzeigename zu überschreiben oder um weitere Einstellungen anzupassen. (Kann nur im Fahrplan zugewiesen werden)", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json index c12e9f8c..f51299f8 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json @@ -75,10 +75,15 @@ "gui.createrailwaysnavigator.common.false": "No", "gui.createrailwaysnavigator.common.search": "Search", "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.max": "Max", "gui.createrailwaysnavigator.common.server_error": "Server error while executing task. Look console for details.", "gui.createrailwaysnavigator.common.delete": "Delete", "gui.createrailwaysnavigator.common.add": "Add", "gui.createrailwaysnavigator.common.help": "Get Help", + "gui.createrailwaysnavigator.common.copy": "Copy", + "gui.createrailwaysnavigator.common.paste": "Paste", + "gui.createrailwaysnavigator.common.click_to_search": "Click to search", + "gui.createrailwaysnavigator.common.click_to_edit": "Click to edit", "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "No connections found.", @@ -99,6 +104,7 @@ "gui.createrailwaysnavigator.route_details.title": "Route Details", "gui.createrailwaysnavigator.route_details.departure": "Departure in", + "gui.createrailwaysnavigator.route_details.arrival": "Arrival in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transfer in", "gui.createrailwaysnavigator.route_details.transfer": "Transfer", @@ -206,6 +212,7 @@ "gui.createrailwaysnavigator.line": "Line", "gui.createrailwaysnavigator.following_trains": "Following trains:", "gui.createrailwaysnavigator.via": "via", + "gui.createrailwaysnavigator.too_small": "Too small!", "gui.createrailwaysnavigator.advanced_display_settings.title": "Advanced Display Settings", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Display Type", @@ -213,7 +220,28 @@ "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Information Type", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Determines how detailed the information is.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Double-sided", + "gui.createrailwaysnavigator.advanced_display_settings.advanced_settings": "Advanced Settings", + "gui.createrailwaysnavigator.advanced_display_settings.font_color": "Font color", + "gui.createrailwaysnavigator.advanced_display_settings.back_color": "Background color", + "gui.createrailwaysnavigator.advanced_display_settings.show_arrival": "Show arrival of trains", + "gui.createrailwaysnavigator.advanced_display_settings.show_arrival.description": "If activated or the train will go in an excluded section (regardless of this setting), the arrival of the train will be displayed.", + "gui.createrailwaysnavigator.advanced_display_settings.carriage_index": "Carriage Index Offset", + "gui.createrailwaysnavigator.advanced_display_settings.carriage_index.description": "The number that is added to the current carriage index. (e.g. Index = 3, Offset = 5, Result = 08)", + "gui.createrailwaysnavigator.advanced_display_settings.overwrite_carriage_index": "Overwrite index", + "gui.createrailwaysnavigator.advanced_display_settings.overwrite_carriage_index.description": "If checked, the value will not be added to the carriage index, but replaced by the entered value. (e.g. Index = 3, Offset = 5, Result = 05)", + "gui.createrailwaysnavigator.advanced_display_settings.show_stats": "Show train stats", + "gui.createrailwaysnavigator.advanced_display_settings.show_exit": "Show exit direction", + "gui.createrailwaysnavigator.advanced_display_settings.show_time_and_date": "Show time and date", + "gui.createrailwaysnavigator.advanced_display_settings.show_connections": "Show next connections", + "gui.createrailwaysnavigator.advanced_display_settings.show_line_color": "Show train line color", + "gui.createrailwaysnavigator.advanced_display_settings.show_line_color.description": "Highlights the train name text with the color of the current train line.", + "enum.createrailwaysnavigator.train_text_components": "Train Text Components", + "enum.createrailwaysnavigator.train_text_components.description": "Text components which should be displayed on the passenger displays.", + "enum.createrailwaysnavigator.train_text_components.all": "All", + "enum.createrailwaysnavigator.train_text_components.train_name": "Train Name only", + "enum.createrailwaysnavigator.train_text_components.destination": "Destination only", + "enum.createrailwaysnavigator.display_info_type": "Display Info Type", "enum.createrailwaysnavigator.display_info_type.description": "Determines how much information should be displayed on your display board.", "enum.createrailwaysnavigator.display_info_type.simple": "Simple", @@ -226,11 +254,13 @@ "enum.createrailwaysnavigator.display_type": "Display Type", "enum.createrailwaysnavigator.display_type.description": "The type of your display which depends on their purpose.", "enum.createrailwaysnavigator.display_type.train_destination": "Train Destination", - "enum.createrailwaysnavigator.display_type.info.train_destination": "Inteded to be used outside of trains as it shows information about the train itself such as the name, destination and (if selected) stopovers and other information.", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Intended to be used outside of trains as it shows information about the train itself such as the name, destination and (if selected) stopovers and other information.", "enum.createrailwaysnavigator.display_type.passenger_information": "Passenger Information", "enum.createrailwaysnavigator.display_type.info.passenger_information": "Represents the displays found inside of trains. These displays will show the next stop, the exit direction and (if selected) train speed and a route overview.", "enum.createrailwaysnavigator.display_type.platform": "Platform Display", - "enum.createrailwaysnavigator.display_type.info.platform": "These displays should be used on train station platforms and shows the next ariving trains with additional details if selected. Cannot be used in trains!", + "enum.createrailwaysnavigator.display_type.info.platform": "These displays should be used on train station platforms and shows the next arriving trains with additional details if selected. Cannot be used in trains!", + "enum.createrailwaysnavigator.display_type.departure_board": "Departure Board", + "enum.createrailwaysnavigator.display_type.info.departure_board": "A summary of all trains departing from a station.", "enum.createrailwaysnavigator.side": "Side", "enum.createrailwaysnavigator.side.description": "The side of the block where the information should be rendered on.", @@ -246,11 +276,28 @@ "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (estimated time of arrival)", + "enum.createrailwaysnavigator.train_filter": "Train Filter", + "enum.createrailwaysnavigator.train_filter.description": "Compares trains based on the selected criterion to decide which trains should be taken into account.", + "enum.createrailwaysnavigator.train_filter.any": "Any train", + "enum.createrailwaysnavigator.train_filter.info.any": "Every train is taken into account here.", + "enum.createrailwaysnavigator.train_filter.same_line": "Same Line", + "enum.createrailwaysnavigator.train_filter.info.same_line": "Only trains with the same train line are taken into account.", + "enum.createrailwaysnavigator.train_filter.same_group": "Same Group", + "enum.createrailwaysnavigator.train_filter.info.same_group": "Only trains with the same train group are taken into account.", + "create.display_source.advanced_display": "Advanced Displays", - "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Train Name Column Width", - "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "in block pixels. (Default: 16)", - "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Platform Column Width", - "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "in block pixels. (Default: Auto)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Train Name Width", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width_next": "Train Name Width (Next departure)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width_table": "Train Name Width (Following trains)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "in block pixels", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Platform Width", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width_next": "Platform Width (Next departure)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width_table": "Platform Width (Following trains)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "in block pixels", + "gui.createrailwaysnavigator.display_source.advanced_display.info_width": "Info Section Width", + "gui.createrailwaysnavigator.display_source.advanced_display.info_width.description": "in percent of the remaining screen width", + "gui.createrailwaysnavigator.display_source.advanced_display.stopovers_width": "Stopovers Section Width", + "gui.createrailwaysnavigator.display_source.advanced_display.stopovers_width.description": "in percent of the remaining screen width", "gui.createrailwaysnavigator.section_settings.title": "Section Settings", "gui.createrailwaysnavigator.section_settings.train_groups": "Assign Train Group", @@ -263,10 +310,15 @@ "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Minimum Duration", "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Wait: %s..%s", "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "or at least %s", + + "createrailwaysnavigator.schedule.condition.train_separation.settings": "Train Separation Settings", + "createrailwaysnavigator.schedule.condition.train_separation": "Train Separation", + "createrailwaysnavigator.schedule.condition.train_separation.title": "Separation: %s", + "createrailwaysnavigator.schedule.condition.train_separation.description": "Departure %s after:", + "createrailwaysnavigator.schedule.instruction.configure": "Configure...", "createrailwaysnavigator.schedule.instruction.travel_section": "New Schedule Section", "createrailwaysnavigator.schedule.instruction.travel_section.description": "Beginning of a new schedule section.", - "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Configure...", "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Set Train Group: ", "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Set Train Line: ", "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Include start of next section: ", @@ -274,6 +326,13 @@ "createrailwaysnavigator.schedule.instruction.reset_timings": "Reset Timings", + "goggles.createrailwaysnavigator.train_listener.departures.title": "Departure History", + "goggles.createrailwaysnavigator.train_listener.departures.any": "Last departure:", + "goggles.createrailwaysnavigator.train_listener.departures.line": "Departures by Train Line:", + "goggles.createrailwaysnavigator.train_listener.departures.group": "Departures by Train Group:", + "goggles.createrailwaysnavigator.train_listener.departures.nothing": "(No recent departures)", + "goggles.createrailwaysnavigator.train_listener.departures.has_more": "+ %s more...", + "display.createrailwaysnavigator.train_destination.simple": "Compact", "display.createrailwaysnavigator.train_destination.extended": "Extended", "display.createrailwaysnavigator.train_destination.detailed": "Detailed", @@ -282,6 +341,7 @@ "display.createrailwaysnavigator.platform.running_text": "Scrolling Text", "display.createrailwaysnavigator.platform.table": "Table", "display.createrailwaysnavigator.platform.focus": "Focus", + "display.createrailwaysnavigator.departure_board.table": "Table", "gui.createrailwaysnavigator.saved_routes.title": "Saved Routes", "gui.createrailwaysnavigator.schedule_board.title": "Schedule Board", @@ -312,11 +372,13 @@ "gui.createrailwaysnavigator.saved_routes.saved_route": "Saved Route", "block.createrailwaysnavigator.advanced_display.ber.arrival": "Arrival", "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Cancelled", - "block.createrailwaysnavigator.advanced_display.ber.delayed": "Delay approx. %s minutes", + "block.createrailwaysnavigator.advanced_display.ber.delay_abs_suffix": "minutes", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Delay approx. %s", "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information about %s: This train got cancelled", - "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information about %s: Delay approx. %s minutes", - "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", is cancelled today. We apologize for the inconvenience.", - "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", today approx. %s minutes delayed.", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information about %s: Delay approx. %s", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed_with_unit": "Information about %s: Delay approx. %s", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": "is cancelled today. We apologize for the inconvenience.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": "today approx. %s delayed.", "block.createrailwaysnavigator.advanced_display.ber.reason": "Reason: ", "gui.createrailwaysnavigator.route_widget.show_details": "Show details", "gui.createrailwaysnavigator.route_widget.save": "Save", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json index be3d0836..c37b9dc5 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/es_es.json @@ -3,12 +3,8 @@ "advancement.createrailwaysnavigator.navigator.description": "Crea un navegador para buscar conexiones de tren de una estación de tren a otra.", "advancement.createrailwaysnavigator.advanced_display": "No del todo 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Actualiza tus paneles de visualización para mostrar más información e incluso colócalos en tus trenes.", - - "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator": "Navegador de ferrocarriles", "item.createrailwaysnavigator.navigator.tooltip.summary": "El _navegador_ muestra posibles _conexiones de trenes_ con información adicional como _escalas_, _datos en tiempo real_ y más.", - "block.createrailwaysnavigator.train_station_clock": "Reloj de estación de tren", "block.createrailwaysnavigator.advanced_display_block": "Bloque de pantalla avanzada", "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Úsalo en _trenes_, como _pantalla de destino del tren_ o _pantalla de información para pasajeros_, o en _estaciones de tren_ como pantallas de _plataforma mejoradas_ que también muestran más información que los tableros de pantalla regulares.", @@ -34,20 +30,18 @@ "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Úsalo en _trenes_, como _pantalla de destino del tren_ o _pantalla de información para pasajeros_, o en _estaciones de tren_ como pantallas de _plataforma mejoradas_ que también muestran más información que los tableros de pantalla regulares.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Al hacer clic derecho con una llave inglesa", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Abre un menú para _configurar_ la _pantalla_.", - + "block.createrailwaysnavigator.advanced_display_slab": "Losa de pantalla avanzada", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Úsalo en los _trenes_, como un _pantalla de destino del tren_ o _pantalla de información de pasejeros_ o en los _estaciones del tren_ como ...", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Abrir un menú para _configurar_ la _pantalla_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "¡Fuera de servicio!", - "category.createrailwaysnavigator.crn": "Navegador de ferrocarriles", "key.createrailwaysnavigator.route_overlay_options": "Mostrar opciones de superposición de ruta", - "enum.createrailwaysnavigator.overlay_position": "Posición de la pantalla", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Esquina superior izquierda", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Esquina superior derecha", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Esquina inferior izquierda", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Esquina inferior derecha", - "gui.createrailwaysnavigator.loading.title": "Descargando datos del servidor...", - "gui.createrailwaysnavigator.overlay_settings.title": "Configuración de superposición de ruta", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Mostrar detalles", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Eliminar superposición de ruta", @@ -60,7 +54,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "El narrador anuncia eventos importantes en tu viaje, por ejemplo, la próxima parada, cambios, etc.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notificaciones", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Recibe notificaciones emergentes sobre eventos importantes en tu viaje, por ejemplo, la próxima parada, cambios, etc.", - "gui.createrailwaysnavigator.common.expand": "Mostrar detalles", "gui.createrailwaysnavigator.common.collapse": "Ocultar detalles", "gui.createrailwaysnavigator.common.go_back": "Volver", @@ -72,7 +65,6 @@ "gui.createrailwaysnavigator.common.search": "Buscar", "gui.createrailwaysnavigator.common.auto": "Automático", "gui.createrailwaysnavigator.common.server_error": "Error del servidor al ejecutar la tarea. Mira la consola para más detalles.", - "gui.createrailwaysnavigator.navigator.title": "Navegador de ferrocarriles", "gui.createrailwaysnavigator.navigator.no_connections": "No se encontraron conexiones.", "gui.createrailwaysnavigator.navigator.not_searched": "Aún no se ha buscado nada.", @@ -88,13 +80,10 @@ "gui.createrailwaysnavigator.navigator.search.tooltip": "Buscar", "gui.createrailwaysnavigator.navigator.location.tooltip": "Estación más cercana desde la posición actual", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Cambiar campos", - "gui.createrailwaysnavigator.route_details.title": "Detalles de la ruta", "gui.createrailwaysnavigator.route_details.departure": "Salida en", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transbordo en", "gui.createrailwaysnavigator.route_details.transfer": "Transbordo", - "gui.createrailwaysnavigator.route_details.save_route": "Guardar conexión", - "gui.createrailwaysnavigator.route_overview.title": "Detalles de la ruta", "gui.createrailwaysnavigator.route_overview.journey_begins": "¡Tu viaje comienza! %s a %s, salida %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "¡Tu viaje comienza! %s a %s, salida %s en el andén %s", @@ -124,7 +113,7 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "¡Tu andén ha cambiado!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Tu tren en %s sale hoy desde el andén %s.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Llegada %s retrasada.", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s en lugar de %s en %s", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s en lugar de %s en %s", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Tren cancelado", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s a %s está cancelado hoy.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Transbordo próximo", @@ -137,7 +126,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "¡Has llegado a tu destino!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Gracias por viajar y que tengas un buen día", "gui.createrailwaysnavigator.route_overview.date": "Día %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Configuración global", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Haz clic para editar", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Etiquetas de estaciones de tren", @@ -148,33 +136,28 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Crear grupos de trenes para organizar todos los trenes (por ejemplo, servicios regionales, servicios de larga distancia, ...). Los usuarios pueden decidir qué grupos quieren usar.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Lista negra de trenes", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Excluir trenes, por ejemplo, trenes de carga, trenes especiales, etc., para que no se utilicen en las sugerencias de rutas.", - - "gui.createrailwaysnavigator.station_tags.title": "Configuración de etiquetas de estaciones de tren", "gui.createrailwaysnavigator.station_tags.summary": "Contiene %s estaciones de tren", "gui.createrailwaysnavigator.station_tags.editor": "Última edición por %s el %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Crear nueva entrada", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Eliminar etiqueta", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Eliminar estación", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Haga clic para cambiar", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Agregar estación", "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nombre de la estación de tren", "gui.createrailwaysnavigator.station_tags.hint.platform": "Andén", "gui.createrailwaysnavigator.station_tags.enter_name": "Introduce el nombre aquí", - "gui.createrailwaysnavigator.train_group_settings.title": "Configuración de grupos de trenes", "gui.createrailwaysnavigator.train_group_settings.summary": "Contiene %s trenes", "gui.createrailwaysnavigator.train_group_settings.editor": "Última edición por %s el %s", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Eliminar grupo", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Eliminar tren", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Agregar tren", - "gui.createrailwaysnavigator.blacklist.title": "Lista negra de estaciones de tren", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Agregar a la lista negra", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Eliminar de la lista negra", - "gui.createrailwaysnavigator.train_blacklist.title": "Lista negra de trenes", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Agregar a la lista negra", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Eliminar de la lista negra", - "gui.createrailwaysnavigator.search_settings.title": "Configuración de búsqueda", "gui.createrailwaysnavigator.search_settings.transfer_time": "Tiempo mínimo de transbordo", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "El tiempo mínimo que debería estar disponible para cambiar de tren. (1h ~ 50 segundos en tiempo real)", @@ -184,38 +167,32 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Todos", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Ninguno", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Restablecer filtro", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Agregar", - "gui.createrailwaysnavigator.time": "Hora: %s", "gui.createrailwaysnavigator.time.now": "ahora", "gui.createrailwaysnavigator.time_format.dhm": "%s días %s h. %s min.", "gui.createrailwaysnavigator.time_format.hm": "%s h. %s min.", - "gui.createrailwaysnavigator.time_format.m": "%s min.", - + "gui.createrailwaysnavigator.time_format.m": "%s minutos.", "gui.createrailwaysnavigator.platform": "Andén", "gui.createrailwaysnavigator.departure": "Salida", "gui.createrailwaysnavigator.destination": "Destino", "gui.createrailwaysnavigator.line": "Línea", "gui.createrailwaysnavigator.following_trains": "Trenes siguientes:", "gui.createrailwaysnavigator.via": "a través de", - "gui.createrailwaysnavigator.advanced_display_settings.title": "Configuración avanzada de la pantalla", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Tipo de pantalla", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Determina la información mostrada.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Tipo de información", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Determina el detalle de la información.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Doble cara", - "enum.createrailwaysnavigator.display_info_type": "Tipo de información de la pantalla", "enum.createrailwaysnavigator.display_info_type.description": "Determina cuánta información se debe mostrar en tu pantalla de visualización.", - "enum.createrailwaysnavigator.display_info_type.simple": "Simple", + "enum.createrailwaysnavigator.display_info_type.simple": "Sencillo", "enum.createrailwaysnavigator.display_info_type.info.simple": "La pantalla solo mostrará la información más importante sin detalles adicionales.", "enum.createrailwaysnavigator.display_info_type.detailed": "Detallado", "enum.createrailwaysnavigator.display_info_type.info.detailed": "Se mostrará la información más importante con algunos detalles, como la velocidad del tren, las paradas intermedias, etc.", "enum.createrailwaysnavigator.display_info_type.informative": "Informativo", "enum.createrailwaysnavigator.display_info_type.info.informative": "Muestra toda la información que podría ser interesante y la presenta en un diseño elegante. No se recomienda para pantallas pequeñas, ya que el texto podría ser muy pequeño.", - "enum.createrailwaysnavigator.display_type": "Tipo de pantalla", "enum.createrailwaysnavigator.display_type.description": "El tipo de pantalla depende de su propósito.", "enum.createrailwaysnavigator.display_type.train_destination": "Destino del tren", @@ -224,26 +201,44 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Representa las pantallas que se encuentran dentro de los trenes. Estas pantallas mostrarán la próxima parada, la dirección de salida y (si se selecciona) la velocidad del tren y una vista general de la ruta.", "enum.createrailwaysnavigator.display_type.platform": "Pantalla de plataforma", "enum.createrailwaysnavigator.display_type.info.platform": "Estas pantallas deben usarse en las plataformas de las estaciones de tren y muestran los próximos trenes que llegan con detalles adicionales si se seleccionan. ¡No se pueden usar en los trenes!", - "enum.createrailwaysnavigator.side": "Lado", "enum.createrailwaysnavigator.side.description": "El lado del bloque donde se debe renderizar la información.", "enum.createrailwaysnavigator.side.front": "Lado frontal", "enum.createrailwaysnavigator.side.info.front": "La información solo se renderizará en el lado frontal. Este es el comportamiento predeterminado.", "enum.createrailwaysnavigator.side.both": "Ambos lados", "enum.createrailwaysnavigator.side.info.both": "La información se renderizará en ambos lados.", - "enum.createrailwaysnavigator.time_display": "Visualización del tiempo", "enum.createrailwaysnavigator.time_display.description": "Determina cómo se debe mostrar el tiempo.", - "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.abs": "Absoluto", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absoluto)", - "enum.createrailwaysnavigator.time_display.eta": "ETA", + "enum.createrailwaysnavigator.time_display.eta": "Hora estimada de llegada", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (tiempo estimado de llegada)", - "create.display_source.advanced_display": "Pantallas avanzadas", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Ancho de columna del nombre del tren", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "en píxeles de bloque. (Por defecto: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Ancho de columna de plataforma", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "en píxeles de bloque. (Por defecto: Automático)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.section_settings.title": "Configuración de las secciones", + "gui.createrailwaysnavigator.section_settings.train_groups": "Asignar el grupo del tren", + "gui.createrailwaysnavigator.section_settings.train_lines": "Asignar la línea del tren", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Incluye inicio de la próxima sección", + "gui.createrailwaysnavigator.section_settings.usable": "Navegable", + "gui.createrailwaysnavigator.section_settings.none": "(Ninguna Opción)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Retraso dinámico", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Duración mínima", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Espero: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "o al menos %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Nueva Sección de Horario", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Inicio de la nueva sección de horario.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Configuraciones...", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " -Navegable: ", + "gui.createrailwaysnavigator.station_tags.title": "Configuración de etiquetas de estaciones de tren", + "gui.createrailwaysnavigator.journey_info.title": "Información del viaje", + "gui.createrailwaysnavigator.journey_info.date": "Día %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) a %s", + "gui.createrailwaysnavigator.color_picker.custom": "Personalizada...", + "gui.createrailwaysnavigator.color_picker.no_color": "Sin color", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Todos", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Todos", + "gui.createrailwaysnavigator.saved_routes.today": "Hoy" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/eu_es.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/eu_es.json new file mode 100644 index 00000000..67896baf --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/eu_es.json @@ -0,0 +1,315 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Eskerrik asko bidaiatzeagatik", + "advancement.createrailwaysnavigator.navigator.description": "Nabigatzaile bat sortu tren geltoki batetik beste batera ibilbide bat bilatzeko.", + "advancement.createrailwaysnavigator.advanced_display": "4k nahiko ez", + "advancement.createrailwaysnavigator.advanced_display.description": "Zeure pantaila-taulak hobetu informazio gehiago erakusteko, edota zeure trenetan kokatzeko.", + "item.createrailwaysnavigator.navigator": "Trenbide Nabigatzailea", + "item.createrailwaysnavigator.navigator.tooltip.summary": "_Nabigatzaileak_ erabilgarri diren _tren loturak_ erakusten ditu baita gainerazko informazioa ere, hala nola: _tren aldaketak_, _denbora errealeko datuak_, _munduko ordua_, etab.", + "block.createrailwaysnavigator.train_station_clock": "Geltokiko Erlojua", + "block.createrailwaysnavigator.advanced_display_block": "Geltokiko Erlojua", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display": "Pantaila-Taula Aurreratua", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display_small": "Pantaila Aurreratu Txikia", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display_panel": "Pantaila Aurreratu - Panela", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Pantaila Aurreratu - Panel-erdia", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display_sloped": "Pantaila Aurreratu Makurtua", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display_slab": "Pantaila Aurreratu - Lauza", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "_Trenetan_ erabili _Helmuga-pantaila_ edo _bidaiarientzako informazio pantaila_ gisa, edo _Tren Geltokietan_ ere, _Nasako pantaila aurreratu_ gisa, ohiko pantaila-taulak baino xehetasun gehiago ere erakusten dituena.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "Giltza zabalgarri batez eskuineko klika eginez", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "_Pantaila konfiguratzeko_ menua ireki.", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Zerbitzuz kanpo!", + "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "key.createrailwaysnavigator.route_overlay_options": "Erakutsi ibilbide gainjarriaren aukeraj", + "enum.createrailwaysnavigator.overlay_position": "Erakutsi Kokapena", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Goi-ezkerreko Izkina", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Goi-eskuineko Izkina", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Behe-ezkerreko Izkina", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Behe-eskuineko Izkina", + "gui.createrailwaysnavigator.loading.title": "Serbitzariarengandik datuak eskuratzen...", + "gui.createrailwaysnavigator.overlay_settings.title": "Ibilbide Jaingarriaren Ezarpenak", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Erakutsi Xehetasunak", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Ibilbide Jaingarria Kendu", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Narratzailearen iragarkiak gaituak.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Narratzailearen iragarkiak ezgaituak.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Jakinarazpenak gaituak", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Jakinarazpenak ezgaituak", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Interfazearen Eskala", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Narratzailearen iragarkiak", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Narratzaileak ibilbidean zeharreko gertakizunak iragartzen ditu, hala-nola: hurrengo geralekua, lotuneak, etab.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Jakinarazpenak", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Ibilbideari buruzko jakinarazpen gainerakorrak jaso, hala-nola: hurrengo geralekua, lotuneak, etab.", + "gui.createrailwaysnavigator.common.expand": "Erakutsi Xehetasunak", + "gui.createrailwaysnavigator.common.collapse": "Ezkutatu Xehetasunak", + "gui.createrailwaysnavigator.common.go_back": "Atzera bota", + "gui.createrailwaysnavigator.common.go_to_top": "Goikoenera Joan", + "gui.createrailwaysnavigator.common.reset_defaults": "Lehentesietara berrezarri", + "gui.createrailwaysnavigator.common.count": "Kopurua", + "gui.createrailwaysnavigator.common.true": "Bai", + "gui.createrailwaysnavigator.common.false": "Ez", + "gui.createrailwaysnavigator.common.search": "Bilatu", + "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.server_error": "Serbitzariaren errorea ekintza eragiterakoan. Behatu detaileak kontsolan.", + "gui.createrailwaysnavigator.common.delete": "Ezabatu", + "gui.createrailwaysnavigator.common.add": "Gehitu", + "gui.createrailwaysnavigator.common.help": "Laguntza Eskuratu", + "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.navigator.no_connections": "Ez da Ibilbide egokirik aurkitu.", + "gui.createrailwaysnavigator.navigator.not_searched": "Ez da ezer bilatu oraingoz.", + "gui.createrailwaysnavigator.navigator.searching": "Bilatu Ibilbideak", + "gui.createrailwaysnavigator.navigator.error_title": "Ezin izan da ibilbiderik egin.", + "gui.createrailwaysnavigator.navigator.start_end_null": "Jatorria edo helmuga ez da zehaztu.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Jatorria eta helmuga berdinak dira.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Trena jadanik abiatu da", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Lotunea", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "%s-(e)tik", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Ezarpen Globalak", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Ezarpenak Bilatu", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Bilatu", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Zugandik hurbilen dagoen geltokia", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Errefreskatu", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Zelaia aldatu", + "gui.createrailwaysnavigator.route_details.title": "Ibilbidearen Xehetasunak", + "gui.createrailwaysnavigator.route_details.departure": "Irteera:", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Hurrengo trena irteteko: ", + "gui.createrailwaysnavigator.route_details.transfer": "Lotunea", + "gui.createrailwaysnavigator.route_overview.title": "Bidai Laguna", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Zure bidaia hastear dago! %s-(e)tik %s-(e)ra, irteera %s-(e)tan", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Zure bidaia hastear dago! %s-(e)tik %s-(e)ra, irteera %s-(e)tan %s nasatik", + "gui.createrailwaysnavigator.route_overview.train_details": "%s-(e)tik %s-(e)ra", + "gui.createrailwaysnavigator.route_overview.next_stop": "Hurrengo Geralekua: %s", + "gui.createrailwaysnavigator.route_overview.transfer": "Aldaketa: %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Aldaketa: %s → %s, %s nasan", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Bidaia amaitu da", + "gui.createrailwaysnavigator.route_overview.after_journey": "%s-(e)ra heldu zara. Eskerrik asko bidaiatzeagatik eta egun on bat izan.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Hurrengo Loturak", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Lotunea", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Trena ezeztatua", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "%s-ri buruzko informazioa", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Ezeztatua", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "Lotura arriskutsua", + "gui.createrailwaysnavigator.route_overview.connection_missed": "Lotura galdua", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Trena ezeztatua", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "%s-(e)rantzeko bidaia ezin da jarraitu.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Zure trenaren atzerapena dela eta, lotura galdu duzu. Nabigatzailean bidaia alternatibo bat bila dezakezu. Eragozpenengatik barkamena eskatzen dizugu.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "%s -ri buruzko informazioa: Tren hau ezeztatua izan da! Eragozpenengatik barkamena eskatzen dizugu. Nabigatzailean bidaia alternatibo bat bila dezakezu.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Trena ezeztatua izan da", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "%s -(e)rantzeko bidaia ezin da jarraitu. Nabigatzailean bidaia alternatibo bat bila dezakezu.", + "gui.createrailwaysnavigator.route_overview.options": "%s sakatu aukerak ikusteko", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Zure %s-(e)rantzeko bidaia hasi da!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s-(e)tik %s-(e)ra, irteera %s-(e)tan", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s-(e)tik %s-(e)ra, irteera %s-(e)tan %s nasatik", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Zure trenaren nasa aldatu da!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Zure trena %s-(e)n %s nasatk abiatuko da.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: etorrera %sz atzeratu da", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s %s-(r)en ordez %s-(e)n", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Trena ezeztatua", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s %s-(e)rantz ezeztatua izan da gaur.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "%s: etorrera %s-z atzeratu da.", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Aldaketa: %s → %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Aldaketa: %s → %s, %s nasan", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Lotura arriskutsua!", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Ziurrenik ezin izango duzu %s %s-(e)ra garaiz hartu.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Trena galdua", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": " %s trena %s-ra galdu duzu.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Helmugara iritsi zara!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Eskerrik asko bidaiatzeagatik eta egun on bat izan.", + "gui.createrailwaysnavigator.route_overview.date": "%s eguna, %s-(e)tan", + "gui.createrailwaysnavigator.global_settings.title": "Ezarpen Globalak", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Egin klik aldatzeko", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Geltokien Etiketak", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Geltoki Etiketak definitu nasa ugariko geltokiak (adb. MyStation 1, MyStation 2, ...) geltoki bakartzat hartzeko (adb. MyStation) hautaturiko izenarekin, nabigatzaileak, besteak beste, loturak gomendatu ditzan.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Geltokien Zerrenda Beltza", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Nabigatzailean agertu beharko ez liratekeen geltokiak baztertu. Geltoki hauek ez dira kontuan hartuko ibilbideak sortzerakoan.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Tren Taldeak", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Trenak antolatzeko taldeak sortu (adb. eskualdeko serbitzuak, distantzia-handiko serbitzuak, etab.).", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Trenen Zerrenda Beltza", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Trenak beztartu, hala nola, kargo trenak, tren bereziak, etab., ibilbide proposamenetan ez agertzearren.", + "gui.createrailwaysnavigator.station_tags.summary": "%s geltoki dauzka", + "gui.createrailwaysnavigator.station_tags.editor": "%s-k aldatudu azkenengoz %s-an", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Sarrera berri bat sortu", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Ezabatu Etiketa", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Ezabatu Geltokia", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Klikatu aldatzeko", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Geltokia gehitu", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Geltokiaren izena", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Nasaren Izena", + "gui.createrailwaysnavigator.station_tags.enter_name": "Izena idatzi", + "gui.createrailwaysnavigator.train_group_settings.title": "Tren Taldeen Ezarpenak", + "gui.createrailwaysnavigator.train_group_settings.summary": "%s tren dauzka", + "gui.createrailwaysnavigator.train_group_settings.editor": "%s-k aldatudu azkenengoz %s-an", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Taldea Ezabatu", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Trena Ezabatu", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Trena Gehitu", + "gui.createrailwaysnavigator.blacklist.title": "Geltokien Zerrenda Beltza", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Gehitu zerrenda beltzera", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Zerrenda beltzetik kendu", + "gui.createrailwaysnavigator.train_blacklist.title": "Trenen Zerrenda Beltza", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Zerrenda beltzera gehitu", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Zerrenda beltzetik kendu", + "gui.createrailwaysnavigator.search_settings.title": "Ezarpenak Bilatu", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Gutxienezko tren-aldaketa denbora tartea", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Gutxienezko denbora trenez aldatzeko. (Munduko 1h = Bizitza errealeko 50 segundu)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Tren Kategoria Filtroa", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Erabaki zein kategoriatatik zer tren erabili nahi duzun.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "Aukeratutako kategoriak: %s", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Denak", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Bat ere ez", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Filtroa berrezarri", + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Gehitu", + "gui.createrailwaysnavigator.time": "Ordua: %s", + "gui.createrailwaysnavigator.time.now": "orain", + "gui.createrailwaysnavigator.time_format.dhm": "%s egun %s ordu %s min.", + "gui.createrailwaysnavigator.time_format.hm": "%s ordu %s min.", + "gui.createrailwaysnavigator.time_format.m": "%s minutu", + "gui.createrailwaysnavigator.platform": "Nasa", + "gui.createrailwaysnavigator.departure": "Abiapuntua", + "gui.createrailwaysnavigator.destination": "Helmuga", + "gui.createrailwaysnavigator.line": "Linea", + "gui.createrailwaysnavigator.following_trains": "Hurrengo trenak:", + "gui.createrailwaysnavigator.via": "via", + "gui.createrailwaysnavigator.advanced_display_settings.title": "Pantaila Aurreratuen Ezarpenak", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Pantaila Mota", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Bistaratzen den informazioa ezartzen du.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informazio Mota", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Informazioaren xehetasuna ezartzen du.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Aldebikoa", + "enum.createrailwaysnavigator.display_info_type": "Informazio Mota Bistaratu", + "enum.createrailwaysnavigator.display_info_type.description": "Pantaila-tauletan bistaratzen den informazio kopurua ezartzen du.", + "enum.createrailwaysnavigator.display_info_type.simple": "Bakuna", + "enum.createrailwaysnavigator.display_info_type.info.simple": "Pantailan inforaziorik garrantzitsuena baino ez da bistaratuko.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Xehetsua", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "Informazio garrantzitsu ugari bistaratzen dira, hala-nola: abiadura, geralekuak, etab.", + "enum.createrailwaysnavigator.display_info_type.informative": "Informatiboa", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Interesgarria den edozein informazio bistaratzen da diseinu ikusgarri batez. Ez da gomendagarria pantaila txikientzako, textua irakurtezina bilaka daitekelako.", + "enum.createrailwaysnavigator.display_type": "Pantaila Mota", + "enum.createrailwaysnavigator.display_type.description": "Pantailaren erabileraren araberako mota.", + "enum.createrailwaysnavigator.display_type.train_destination": "Helburua", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Trenen kanpokaldean erabiltzeko egokia da, izena, helmuga edota (aukeratzen bada) geltokiak, besteak beste, bezalako trenari buruzko informazioa, besteen artean, bistaratzen baita.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Bidaiarientzako Informazioa", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Trenaren barrukaldean erabiltzeko egokia da. Pantaila hauetan, hurrengo geralekua, irteera aldea eta (aukeratzen bada), abiadura eta ibilbidearen eskema bistaratuko da.", + "enum.createrailwaysnavigator.display_type.platform": "Nasako Pantaila", + "enum.createrailwaysnavigator.display_type.info.platform": "Pantaila hauek tren-geltokietan erabiltzeko egokiak dira. Helduko diren trenak eta, aukeratzen bada, haiei buruzko xehetasun gehigarriak bistaratzen dira. Ezin dira trenetan erabili.", + "enum.createrailwaysnavigator.side": "Aldea", + "enum.createrailwaysnavigator.side.description": "Blokearen zer aldetan informazioa bistaratuko den aukeratu.", + "enum.createrailwaysnavigator.side.front": "Aurrekaldea", + "enum.createrailwaysnavigator.side.info.front": "Informazioa aurrekaldean baino ez da bistaratuko. (Lehentesia)", + "enum.createrailwaysnavigator.side.both": "Alde biak", + "enum.createrailwaysnavigator.side.info.both": "Informazioa alde bietan bistaratuko da.", + "enum.createrailwaysnavigator.time_display": "Denbora Bistaratzea", + "enum.createrailwaysnavigator.time_display.description": "Denbora nola bistaratzen den ezarri.", + "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absolutua)", + "enum.createrailwaysnavigator.time_display.eta": "HDE", + "enum.createrailwaysnavigator.time_display.info.eta": "HDE (Heldura Denboa Estimatua)", + "create.display_source.advanced_display": "Pantaila Aurreratua", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Tren Izena zutabearen zabalera", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "bloke pixeletan. (Lehentesia: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Nasa zutabearen zabalera", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "bloke pixeletan. (Lehentesia: Auto)", + "gui.createrailwaysnavigator.section_settings.title": "Atal Ezarpenak", + "gui.createrailwaysnavigator.section_settings.train_groups": "Tren Taldea Ezarri", + "gui.createrailwaysnavigator.section_settings.train_lines": "Tren Linea Ezarri", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Hurrengo atalaren hasiera barne", + "gui.createrailwaysnavigator.section_settings.usable": "Irisgarria", + "gui.createrailwaysnavigator.section_settings.none": "(Bat ere ez)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Atzerapen Dinamikoa", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Gutxienezko Iraupena", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Itxaron: %s.. %s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "edo gutxienez %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Ordutegi Atal Berria", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Ordutegi atal berri baten hasiera.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Konfiguratu...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": "- Tren Taldea Ezarri:", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Tren Linea Ezarri: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Hurrengo atalaren hasiera barnean sartu: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Irisgarri: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Denbora berrezarri", + "display.createrailwaysnavigator.train_destination.simple": "Txikitu", + "display.createrailwaysnavigator.train_destination.extended": "Luzatua", + "display.createrailwaysnavigator.train_destination.detailed": "Xehetsua", + "display.createrailwaysnavigator.passenger_information.running_text": "Testu Mugikorra", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Ordutegiarekin Xehetsua", + "display.createrailwaysnavigator.platform.running_text": "Testu Mugikorra", + "display.createrailwaysnavigator.platform.table": "Taula", + "display.createrailwaysnavigator.platform.focus": "Fokua", + "gui.createrailwaysnavigator.saved_routes.title": "Nire ibilbideak", + "gui.createrailwaysnavigator.schedule_board.title": "Ordutegia", + "gui.createrailwaysnavigator.station_tags.title": "Geltoki Etiketak", + "gui.createrailwaysnavigator.journey_info.title": "Ibilbideari buruzko informazioak", + "gui.createrailwaysnavigator.journey_info.date": "s%. egunean", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) %s-(e)rantz", + "gui.createrailwaysnavigator.color_picker.custom": "Pertsonalizatua...", + "gui.createrailwaysnavigator.color_picker.no_color": "Kolorerik ez", + "gui.createrailwaysnavigator.schedule_board.view_details": "Ikusi Xehetasunak", + "gui.createrailwaysnavigator.schedule_board.train_from": "%s-(e)tik", + "gui.createrailwaysnavigator.search_options.departure_in": "Irteera:", + "gui.createrailwaysnavigator.search_options.train_groups": "Tren Taldeak", + "gui.createrailwaysnavigator.search_options.transfer_time": "Aldatzeko Denbora Tartea", + "gui.createrailwaysnavigator.search_options.advanced_options": "Aukera Aurreratuak", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Denak", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s gabe", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Denak", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s gabe", + "gui.createrailwaysnavigator.empty_list": "Zerrenda hutsik dago", + "gui.createrailwaysnavigator.new_entry.add": "Gehitu sarrera berria", + "gui.createrailwaysnavigator.new_entry.new": "Berria:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s gorde da", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Ordutegia aldatu da!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "Gorderik zenuen ibilbide baten ordutegia aldatu egin da! Mesedez, aldaketak nabigatzailean behatu.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Aldaketa", + "gui.createrailwaysnavigator.route_overview.cancelled": "Ezeztatua", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Gordetako Ibilbidea", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Helduera", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Ezeztatua", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "%s minutuz atzeratua gutxi-gora-behera", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "%s-(e)ri buruzko informazioa: Trena ezeztatua", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "%s-(e)ri buruzko informazioa: %s minutuz atzeratua gutxi-gora-behera", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", atzeratu da gaur. Barkatu eragozpenak.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", gaur %s minutuz atzeratuta dago gutxi-gora-behera.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Zergatia: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Erakutsi Xehetasunak", + "gui.createrailwaysnavigator.route_widget.save": "Gorde", + "gui.createrailwaysnavigator.route_widget.remove": "Kendu", + "gui.createrailwaysnavigator.route_widget.share": "Partekatu...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "Pasatu da", + "gui.createrailwaysnavigator.saved_routes.today": "Gaur", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Bihar", + "gui.createrailwaysnavigator.saved_routes.in_days": "%s egunen barru", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Erakutsi Xehetasunak", + "gui.createrailwaysnavigator.saved_route_widget.share": "Partekatu...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Erakutssi Jakinarazpenak", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Eragiketen atzerapena", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Aurretiko ibilbide batean atzeratua", + "gui.createrailwaysnavigator.train_status.red_signal": "Seinale gorria", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Beste tren batek du lehentasuna.", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Beste tren batek eragindako atzerapena.", + "gui.createrailwaysnavigator.train_status.track_closed": "Trenbidea zarratua.", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Langile falta", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Etenaldi teknikoa", + "gui.createrailwaysnavigator.train_status.special_trip": "Bidai berezia", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Ibilbidea gorde", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Ibilbidea ezabatu", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Popup-a Erakutsi", + "gui.createrailwaysnavigator.navigator.my_profile": "Nire perfila", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Zenbait ibilbide osatugabeak edo ez gomendatuak izan daitezke Create Railways Navigator-ek ez baititu tren guztiak hasieratu.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Tren Lineak", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Hautatu Kolorea", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Tren lineak sortu hainbat tren ezberdin talde berean sartzeko. Pantailetan eta euren ezarpen orokorretan erreflexatuko da. (Ordutegian dauden trenetan soilik ezarri daiteke)" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json index 33f6046e..e76f303b 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/fr_fr.json @@ -1,59 +1,49 @@ { "advancement.createrailwaysnavigator.navigator": "Merci d'avoir voyagé", - "advancement.createrailwaysnavigator.navigator.description": "Crafter un Navigateur pour trouver une connection entre une station et une autre.", + "advancement.createrailwaysnavigator.navigator.description": "Crafter un Navigateur pour trouver une connection entre une station et une autre.", "advancement.createrailwaysnavigator.advanced_display": "Pas vraiment 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Améliorer vos tableaux d'affichage pour afficher plus d'information et même les placer dans vos trains.", - - "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator": "Create Navigateur Ferroviaire", - "item.createrailwaysnavigator.navigator.tooltip.summary": "Le _navigateur_ montre les _correspondances_ possibles avec des informations supplémentaires comme les _escales_, des _informations en temps réel_ et plus encore.", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "Le _navigateur_ montre les _correspondances_ possibles avec des informations supplémentaires comme les _escales_, des _informations en temps réel_ et plus encore.", "block.createrailwaysnavigator.train_station_clock": "Horloge de Gare", "block.createrailwaysnavigator.advanced_display_block": "Bloc d'Affichage Avancé", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Quand click-droit en utilisant une clé", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", "block.createrailwaysnavigator.advanced_display": "Tableau d'Affichage Avancé", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Quand click-droit en utilisant une clé", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", "block.createrailwaysnavigator.advanced_display_small": "Petit Tableau d'Affichage Avancé", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Quand click-droit en utilisant une clé", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", "block.createrailwaysnavigator.advanced_display_panel": "Panneau d'Affichage Avancé", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Quand click-droit en utilisant une clé", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Demi-Panneau d'Affichage Avancé", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Quand click-droit en utilisant une clé", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", "block.createrailwaysnavigator.advanced_display_sloped": "Affichage Incliné Avancé", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Utilisez-le sur des _trains_, comme _affichage de la destination_ ou _affichage pour les passagers_, ou dans des _stations de trains_ comme des _affichage de quai_ amélioré qui montre plus d'informations que des tableaux d'affichage classique.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Quand click-droit en utilisant une clé", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Ouvre un menu pour _configurer_ l'_affichage_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Hors service!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "train sans passagers", - - "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "category.createrailwaysnavigator.crn": "Créer un navigateur ferroviaire", "key.createrailwaysnavigator.route_overlay_options": "Afficher les Options de l'Overlay d'Itinéraire", - "enum.createrailwaysnavigator.overlay_position": "Position de l'Affichage", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Coin Supérieur Gauche", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Coin Supérieur Droit", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Coin Inférieur Gauche", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Coin Inférieur Droit", - "gui.createrailwaysnavigator.loading.title": "Téléchargement des données du serveur...", - "gui.createrailwaysnavigator.overlay_settings.title": "Paramètres de l'Overlay d'Itinéraire", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Afficher les détails", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Enlever l'overlay d'itinéraire", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Annonces du narrateur activées.", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Annonces du narrateur désactivées.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Annonces du narrateur désactivées.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Notifications activées", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Notifications désactivées", "gui.createrailwaysnavigator.route_overlay_settings.scale": "Taille de l'interface", @@ -61,7 +51,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Le Narrateur annonce des évènements importants pendant votre voyage, ex. le prochain arrêt, des changements, etc.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notifications", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Recevez des notifications pop-up sur les évènements importants de votre voyage, ex. le prochain arrêt, des changements, etc.", - "gui.createrailwaysnavigator.common.expand": "Afficher les détails", "gui.createrailwaysnavigator.common.collapse": "Cacher les détails", "gui.createrailwaysnavigator.common.go_back": "Retour", @@ -71,10 +60,11 @@ "gui.createrailwaysnavigator.common.true": "Oui", "gui.createrailwaysnavigator.common.false": "Non", "gui.createrailwaysnavigator.common.search": "Rechercher", - "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.auto": "Automatique", "gui.createrailwaysnavigator.common.server_error": "Erreur serveur lors de l'éxecution de la tâche. Regarder la console pour en savoir plus.", - - "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.common.delete": "Supprimer", + "gui.createrailwaysnavigator.common.add": "Ajouter", + "gui.createrailwaysnavigator.common.help": "Obtenir de l’aide", "gui.createrailwaysnavigator.navigator.no_connections": "Aucun itinéraire trouvé.", "gui.createrailwaysnavigator.navigator.not_searched": "Rien n'a encore été recherché.", "gui.createrailwaysnavigator.navigator.searching": "Recherche d'un itinéraire...", @@ -89,13 +79,10 @@ "gui.createrailwaysnavigator.navigator.search.tooltip": "Recherche", "gui.createrailwaysnavigator.navigator.location.tooltip": "Gare la plus proche de votre position", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Inverser le point de départ et la destination", - "gui.createrailwaysnavigator.route_details.title": "Détail de l'itinéraire", "gui.createrailwaysnavigator.route_details.departure": "Départ dans", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Correspondance dans", "gui.createrailwaysnavigator.route_details.transfer": "Correspondance", - "gui.createrailwaysnavigator.route_details.save_route": "Enregistrer l'itinéraire'", - "gui.createrailwaysnavigator.route_overview.title": "Détail de l'itinéraire", "gui.createrailwaysnavigator.route_overview.journey_begins": "Votre voyage commence! %s à %s, départ %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Votre voyage commence! %s à %s, départ %s sur la voie %s", @@ -138,7 +125,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Vous avez atteint votre destination !", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Merci d'avoir voyagé et passez une bonne journée", "gui.createrailwaysnavigator.route_overview.date": "Jour %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Paramètres globaux", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Cliquez ici pour modifier", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Balises de Gare", @@ -149,8 +135,6 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Créez des groupes de trains pour les organiser (ex. services régionaux, services longue distance, ...). Les utilisateurs peuvent décider quels groupes ils souhaitent utiliser.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Liste Noire de Train", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Exclure des trains, ex. trains de marchandises, trains spéciaux, etc., de manière à ce qu'ils ne puissent être utilisé dans des itinéraires.", - - "gui.createrailwaysnavigator.station_tags.title": "Paramètres des Balises de Gares", "gui.createrailwaysnavigator.station_tags.summary": "Contient %s Gares", "gui.createrailwaysnavigator.station_tags.editor": "Modifié par %s le %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Créer une nouvelle entrée", @@ -160,22 +144,18 @@ "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nom de la Station", "gui.createrailwaysnavigator.station_tags.hint.platform": "Quai", "gui.createrailwaysnavigator.station_tags.enter_name": "Entrer un nom ici", - "gui.createrailwaysnavigator.train_group_settings.title": "Paramètres des Groupes de Trains", "gui.createrailwaysnavigator.train_group_settings.summary": "Contient %s Trains", "gui.createrailwaysnavigator.train_group_settings.editor": "Modifié par %s le %s", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Supprimer le Groupe", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Retirer le Train", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Ajouter un Train", - "gui.createrailwaysnavigator.blacklist.title": "Liste noire des Gares", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Ajouter à la liste noire", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Retirer de la liste noire", - "gui.createrailwaysnavigator.train_blacklist.title": "Liste noire des Trains", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Ajouter à la liste noire", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Retirer de la liste noire", - "gui.createrailwaysnavigator.search_settings.title": "Paramètres de recherche", "gui.createrailwaysnavigator.search_settings.transfer_time": "Temps de Correspondance Minimum", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Le temps minimum qui devrait être disponible pour changer de train. (1h ~ 50 secondes réelles)", @@ -185,38 +165,29 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Tous", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Aucun", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Réinitialiser les filtres", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Ajouter", - "gui.createrailwaysnavigator.time": "Temps: %s", "gui.createrailwaysnavigator.time.now": "maintenant", "gui.createrailwaysnavigator.time_format.dhm": "%s jours %s h. %s m.", "gui.createrailwaysnavigator.time_format.hm": "%s h. %s min.", "gui.createrailwaysnavigator.time_format.m": "%s m.", - "gui.createrailwaysnavigator.platform": "Voie", "gui.createrailwaysnavigator.departure": "Départ", - "gui.createrailwaysnavigator.destination": "Destination", "gui.createrailwaysnavigator.line": "Ligne", "gui.createrailwaysnavigator.following_trains": "Trains suivants:", - "gui.createrailwaysnavigator.via": "via", - "gui.createrailwaysnavigator.advanced_display_settings.title": "Paramètres des Affichages Avancés", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Type d'Affichage", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Détermine les informations affichés.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Type d'Information", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Détermine le degré de détail des informations.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Double-face", - "enum.createrailwaysnavigator.display_info_type": "Type d'Affiche d'Information", "enum.createrailwaysnavigator.display_info_type.description": "Détermine combien d'informations devraient être affichés sur votre tableau d'affichage.", - "enum.createrailwaysnavigator.display_info_type.simple": "Simple", "enum.createrailwaysnavigator.display_info_type.info.simple": "L'écran va seulement afficher les informations les plus importantes sans détails supplémentaires.", "enum.createrailwaysnavigator.display_info_type.detailed": "Détaillé", "enum.createrailwaysnavigator.display_info_type.info.detailed": "Les informations les plus importantes avec quelques détails seront affichés, comme la vitesse du trains, les escales, etc.", "enum.createrailwaysnavigator.display_info_type.informative": "Informatif", "enum.createrailwaysnavigator.display_info_type.info.informative": "Affiche toutes les informations qui pourraient être intéressantes et les affiches dans une disposition agréable. Déconseillé pour les petits petits affichages car le texte peu devenir très petit.", - "enum.createrailwaysnavigator.display_type": "Type d'Affichage", "enum.createrailwaysnavigator.display_type.description": "Le type d'affiche en fonction de son objectif.", "enum.createrailwaysnavigator.display_type.train_destination": "Destination du Train", @@ -225,26 +196,20 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Représentes les écrans présents à l'interieur des trains. Ces écrans afficheront les prochains arrêts, la direction de la sortie et (si séléctionés) la vitesse du train et une vue d'ensemble de la route", "enum.createrailwaysnavigator.display_type.platform": "Affichage de quai", "enum.createrailwaysnavigator.display_type.info.platform": "Ces écrans doivent être utilisés sur les quais des gares et montre les prochains trains arrivant avec quelques détails en plus si séléctionés. Ne peut pas être utilisés à l'intérieur des trains!", - "enum.createrailwaysnavigator.side": "Face", "enum.createrailwaysnavigator.side.description": "La face du bloc où les informations devraient être affichés.", "enum.createrailwaysnavigator.side.front": "Face avant", "enum.createrailwaysnavigator.side.info.front": "Les informations seront affichés uniquement sur la face avant du bloc. C'est le comportement par défaut.", "enum.createrailwaysnavigator.side.both": "Toutes les faces", "enum.createrailwaysnavigator.side.info.both": "Les informations seront affichés sur toutes les faces.", - "enum.createrailwaysnavigator.time_display": "Affichage des horaires", "enum.createrailwaysnavigator.time_display.description": "Détermines comment les horaires devraient être affichés", - "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absolu)", - "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (estimation du temps d'arrivée)", - "create.display_source.advanced_display": "Affichage Avancé", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Largeur de la colonne du nom du train", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "en pixels bloc. (Defaut: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Largeur de la colonne du quai", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "en pixel bloc. (Defaut: Auto)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.station_tags.title": "Paramètres des Balises de Gares" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/it_it.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/it_it.json new file mode 100644 index 00000000..4716a854 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/it_it.json @@ -0,0 +1,315 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Grazie per il viaggio", + "advancement.createrailwaysnavigator.navigator.description": "Abbozza viaggio onde cercare collegamenti ferroviari tra le stazioni.", + "advancement.createrailwaysnavigator.advanced_display": "Non abbastanza 4k", + "advancement.createrailwaysnavigator.advanced_display.description": "Aggiorna le schede di visione per vedere più informazioni e posizionarle nei treni.", + "item.createrailwaysnavigator.navigator": "Crea viaggio in ferrovia", + "item.createrailwaysnavigator.navigator.tooltip.summary": "Il _navigatore_ mostra possibili _collegamenti via treno_ con informazioni aggiuntive come _tappe_, _dati in tempo reale_ e altro ancora.", + "block.createrailwaysnavigator.train_station_clock": "Orologio stazione", + "block.createrailwaysnavigator.advanced_display_block": "Visione avanzata", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display": "Tavola schermo avanzato", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display_small": "Display avanzato piccolo", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display_panel": "Pannello visione avanzata", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Pannello schermo avanzato dimezzato", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Pannello visione avanzata inclinato", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display_slab": "Visione avanzata a soletta", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Usalo su _treni_, come _schermata di destinazione_ del treno o _schermo informativo per il passeggero_, o alle _stazioni_ come _schermo_ migliorato, che mostra anche più informazioni rispetto alle normali schede di visualizzazione.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "Se cliccato col destro usando chiave inglese", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Apre un menu' per _configurare_ lo _schermo_.", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Fuori servizio!", + "category.createrailwaysnavigator.crn": "Crea viaggio in ferrovia", + "key.createrailwaysnavigator.route_overlay_options": "Mostra opzioni sovrapposizione percorso", + "enum.createrailwaysnavigator.overlay_position": "Posizione schermo", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Angolo superiore sinistro", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Angolo superiore destro", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Angolo inferiore sinistro", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Angolo inferiore destro", + "gui.createrailwaysnavigator.loading.title": "Scaricamento dati dal server…", + "gui.createrailwaysnavigator.overlay_settings.title": "Impostazioni sovrapposizione percorso", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Mostra dettagli", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Rimuovi sovrapposizione percorso", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Annunci narrati abilitati.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Annunci narrati disabilitati.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Notifiche abilitate", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Notifiche disabilitate", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Scala GUI", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Annunci narrati", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Il narratore annuncia eventi importanti nel tuo viaggio, es. prossima fermata, cambiamenti, ecc.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notifiche", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Ricevi notifiche sugli eventi importanti durante il tuo viaggio, es. prossima fermata, modifiche, ecc.", + "gui.createrailwaysnavigator.common.expand": "Mostra dettagli", + "gui.createrailwaysnavigator.common.collapse": "Nascondi dettagli", + "gui.createrailwaysnavigator.common.go_back": "Torna indietro", + "gui.createrailwaysnavigator.common.go_to_top": "Scorri in alto", + "gui.createrailwaysnavigator.common.reset_defaults": "Ripristina predefinito", + "gui.createrailwaysnavigator.common.count": "Conta", + "gui.createrailwaysnavigator.common.true": "Sì", + "gui.createrailwaysnavigator.common.false": "No", + "gui.createrailwaysnavigator.common.search": "Cerca", + "gui.createrailwaysnavigator.common.auto": "Auto", + "gui.createrailwaysnavigator.common.server_error": "Errore server durante esecuzione attività. Guarda console per dettagli.", + "gui.createrailwaysnavigator.common.delete": "Elimina", + "gui.createrailwaysnavigator.common.add": "Aggiungi", + "gui.createrailwaysnavigator.common.help": "Chiedi aiuto", + "gui.createrailwaysnavigator.navigator.title": "Crea viaggio in ferrovia", + "gui.createrailwaysnavigator.navigator.no_connections": "Nessuna connessione trovata.", + "gui.createrailwaysnavigator.navigator.not_searched": "Ancora nessuna ricerca.", + "gui.createrailwaysnavigator.navigator.searching": "Cerca connessioni…", + "gui.createrailwaysnavigator.navigator.error_title": "Impossibile percorrere!", + "gui.createrailwaysnavigator.navigator.start_end_null": "Partenza o destinazione vuoti.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Partenza e destinazione uguali.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Connessione in passato", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Trasf.", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "da %s", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Impostazioni globali", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Impostazioni di ricerca", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Cerca", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Stazione più vicina dalla posizione corrente", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Ricarica", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Cambia campi", + "gui.createrailwaysnavigator.route_details.title": "Dettagli percorso", + "gui.createrailwaysnavigator.route_details.departure": "Partenza tra", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Trasferimento in", + "gui.createrailwaysnavigator.route_details.transfer": "Navetta", + "gui.createrailwaysnavigator.route_overview.title": "Compagno di viaggio", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Il tuo viaggio inizia! Da %s a %s, partenza %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Il tuo viaggio inizia! Treno da %s a %s, partenza %s dal binario %s", + "gui.createrailwaysnavigator.route_overview.train_details": "Da %s a %s", + "gui.createrailwaysnavigator.route_overview.next_stop": "Prossima fermata:%s", + "gui.createrailwaysnavigator.route_overview.transfer": "Cambia %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Cambia %s → %s sulla piattaforma %s", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Viaggio completato", + "gui.createrailwaysnavigator.route_overview.after_journey": "Hai raggiunto %s. Grazie per aver viaggiato con noi e buona giornata.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Prossime connessioni", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Navetta", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Treno cancellato", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informazioni su %s", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Cancellata", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "Connessione in stato di pericolo", + "gui.createrailwaysnavigator.route_overview.connection_missed": "Connessione mancata", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Treno cancellato", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Il tuo viaggio verso %s non può continuare.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "A causa di un ritardo del treno hai perso il tuo treno di collegamento. Cerca un'alternativa nel navigatore. Ci scusiamo per l'inconveniente.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informazioni su %s: Questo treno è oggi cancellato! Ci scusiamo per l'inconveniente. Cerca un'alternativa nel navigatore.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Il treno è stato cancellato", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Il tuo viaggio per %s non può continuare. Cerca un'alternativa nel navigatore.", + "gui.createrailwaysnavigator.route_overview.options": "Premi %s per opzioni.", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Il tuo viaggio verso %s ha inizio!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "Da %s a %s, partenza %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "Da %s a %s, partenza %s dal binario %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Il binario è cambiato!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Il tuo treno in %s parte oggi dal binario %s.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: arrivo a %s in ritardo.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s invece di %s in %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: treno cancellato", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "Treno da %s a %s oggi cancellato.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Trasferimento in arrivo", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Cambia %s → %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Cambia %s → %s sulla piattaforma %s", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Collegamento in pericolo!", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Probabilmente non potrai raggiungere %s a %s.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Collegamento perso", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Hai perso il tuo treno di collegamento da %s per %s.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Hai raggiunto la tua destinazione!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Grazie per aver viaggiato con noi e buona giornata", + "gui.createrailwaysnavigator.route_overview.date": "Giorno %s, %s", + "gui.createrailwaysnavigator.global_settings.title": "Impostazioni globali", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Clicca per modificare", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Etichette stazione", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Combinare le singole stazioni ferroviarie in un'unica stazione a più binari con il proprio nome in modo che il navigatore possa suggerire trasferimenti e altro ancora.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Lista nera delle stazioni", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Escludi le stazioni ferroviarie che non devono apparire nei risultati di navigazione. Queste stazioni saranno ignorate quando si generano itinerari.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Gruppi di treni", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Crea gruppi di treni per organizzare tutti i treni (es. servizi regionali, servizi a lunga distanza...). Gli utenti possono decidere quali gruppi di treni vogliono usare.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Lista nera dei treni", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Escludi i treni, es. treni merci, treni speciali, ecc., in modo che non siano usati nelle indicazioni del percorso.", + "gui.createrailwaysnavigator.station_tags.summary": "Contiene %s stazioni ferroviarie", + "gui.createrailwaysnavigator.station_tags.editor": "Ultima modifica di %s il %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Crea nuova voce", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Elimina etichetta", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Rimuovi stazione", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Clicca per modificare", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Aggiungi stazione", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nome stazione ferroviaria", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Etichetta binario", + "gui.createrailwaysnavigator.station_tags.enter_name": "Immetti nome qui", + "gui.createrailwaysnavigator.train_group_settings.title": "Impostazioni gruppo treni", + "gui.createrailwaysnavigator.train_group_settings.summary": "Contiene %s treni", + "gui.createrailwaysnavigator.train_group_settings.editor": "Ultima modifica di %s il %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Elimina gruppo", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Rimuovi treno", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Aggiungi treno", + "gui.createrailwaysnavigator.blacklist.title": "Lista nera delle stazioni", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Aggiungi a lista nera", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Rimuovi dalla lista nera", + "gui.createrailwaysnavigator.train_blacklist.title": "Lista nera dei treni", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Aggiungi a lista nera", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Rimuovi dalla lista nera", + "gui.createrailwaysnavigator.search_settings.title": "Impostazioni di ricerca", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Tempo minimo trasferimento", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Il tempo minimo che dovrebbe essere disponibile per cambiare treno. (1o ~ 50 secondi di vita reale)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Filtro categorie treno", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Decidi quali treni di quali categorie di treni si desidera usare.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s categorie selezionate", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Tutti", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "No", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Reimposta filtro", + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Aggiungi", + "gui.createrailwaysnavigator.time": "Tempo: %s", + "gui.createrailwaysnavigator.time.now": "ora", + "gui.createrailwaysnavigator.time_format.dhm": "%s giorni %s ore %s min", + "gui.createrailwaysnavigator.time_format.hm": "%s ore %s min", + "gui.createrailwaysnavigator.time_format.m": "%s min", + "gui.createrailwaysnavigator.platform": "Binario", + "gui.createrailwaysnavigator.departure": "Partenza", + "gui.createrailwaysnavigator.destination": "Destinazione", + "gui.createrailwaysnavigator.line": "Linea", + "gui.createrailwaysnavigator.following_trains": "Seguono i treni:", + "gui.createrailwaysnavigator.via": "via", + "gui.createrailwaysnavigator.advanced_display_settings.title": "Impostazioni visualizzazione avanzata", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Tipo visualizzazione", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Determina le informazioni visualizzate.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Tipo informazioni", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Determina come sono dettagliate le informazioni.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Due parti", + "enum.createrailwaysnavigator.display_info_type": "Tipo visore informazioni", + "enum.createrailwaysnavigator.display_info_type.description": "Determina quante informazioni devono essere visualizzate nello schermo.", + "enum.createrailwaysnavigator.display_info_type.simple": "Semplice", + "enum.createrailwaysnavigator.display_info_type.info.simple": "Lo schermo mostrerà solo le informazioni più importanti senza ulteriori dettagli.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Dettagliate", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "Saranno mostrate le informazioni più importanti con alcuni dettagli, come velocità del treno, tappe, ecc.", + "enum.createrailwaysnavigator.display_info_type.informative": "Informativa", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Mostra tutte le informazioni che potrebbero essere interessanti e le visualizza in una composizione fantasia. Sconsigliato per piccoli schermi in quanto il testo potrebbe diventare molto piccolo.", + "enum.createrailwaysnavigator.display_type": "Tipo schermo", + "enum.createrailwaysnavigator.display_type.description": "Il tipo di schermo che dipende dal suo scopo.", + "enum.createrailwaysnavigator.display_type.train_destination": "Destinazione treno", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Inteso per essere usato al di fuori dei treni in quanto mostra informazioni sul treno stesso come nome, destinazione e (se selezionati) tappe e altre informazioni.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Informazioni passeggero", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Rappresenta i visori trovati all'interno dei treni. Questi visori mostreranno prossima fermata, direzione di uscita e (se selezionati) la velocità del treno e una panoramica dell'itinerario.", + "enum.createrailwaysnavigator.display_type.platform": "Visore a piattaforma", + "enum.createrailwaysnavigator.display_type.info.platform": "Questi visori dovrebbero essere usati sui binari della stazione ferroviaria e mostra i prossimi treni in arrivo con dettagli aggiuntivi se selezionati. Non può essere usato nei treni!", + "enum.createrailwaysnavigator.side": "Lato", + "enum.createrailwaysnavigator.side.description": "Il lato del blocco su cui le informazioni devono essere visualizzate.", + "enum.createrailwaysnavigator.side.front": "Lato anteriore", + "enum.createrailwaysnavigator.side.info.front": "Le informazioni saranno visibili solo sul lato anteriore. Questo è il comportamento predefinito.", + "enum.createrailwaysnavigator.side.both": "Entrambi i lati", + "enum.createrailwaysnavigator.side.info.both": "Le informazioni saranno visibili su entrambi i lati.", + "enum.createrailwaysnavigator.time_display": "Visore tempo", + "enum.createrailwaysnavigator.time_display.description": "Determina come il tempo deve essere visualizzato.", + "enum.createrailwaysnavigator.time_display.abs": "ASS", + "enum.createrailwaysnavigator.time_display.info.abs": "ASS (assoluto)", + "enum.createrailwaysnavigator.time_display.eta": "TAS", + "enum.createrailwaysnavigator.time_display.info.eta": "TAS (Tempo di Arrivo Stimato)", + "create.display_source.advanced_display": "Visualizzazioni avanzate", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Larghezza colonna Nome treno", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "in pixel a blocchi. (predefinito: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Larghezza colonna Binario", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "in pixel a blocchi. (predefinito: Auto)", + "gui.createrailwaysnavigator.section_settings.title": "Impostazioni sezione", + "gui.createrailwaysnavigator.section_settings.train_groups": "Assegna gruppo treni", + "gui.createrailwaysnavigator.section_settings.train_lines": "Assegna linea treni", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Includi inizio prossima sezione", + "gui.createrailwaysnavigator.section_settings.usable": "Percorribile", + "gui.createrailwaysnavigator.section_settings.none": "(Nessuna)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Ritardo dinamico", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Durata minima", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Attesa: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "o almeno %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Sezione Nuovi viaggi", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Inizio di una nuova sezione di pianificazione.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Configura…", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Imposta gruppo treni: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Imposta linea treni: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Includi inizio sezione seguente: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Usabile: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Reimposta tempi", + "display.createrailwaysnavigator.train_destination.simple": "Compatto", + "display.createrailwaysnavigator.train_destination.extended": "Esteso", + "display.createrailwaysnavigator.train_destination.detailed": "Dettagliato", + "display.createrailwaysnavigator.passenger_information.running_text": "Testo scorrevole", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Dettagliato con pianificazione", + "display.createrailwaysnavigator.platform.running_text": "Testo scorrevole", + "display.createrailwaysnavigator.platform.table": "Tabella", + "display.createrailwaysnavigator.platform.focus": "Focalizza", + "gui.createrailwaysnavigator.saved_routes.title": "Itinerari salvati", + "gui.createrailwaysnavigator.schedule_board.title": "Tavola pianificazioni", + "gui.createrailwaysnavigator.station_tags.title": "Etichette stazione", + "gui.createrailwaysnavigator.journey_info.title": "Informazioni viaggio", + "gui.createrailwaysnavigator.journey_info.date": "Giorno %s", + "gui.createrailwaysnavigator.journey_info.train": "Da %s (%s) a %s", + "gui.createrailwaysnavigator.color_picker.custom": "Personalizza...", + "gui.createrailwaysnavigator.color_picker.no_color": "Nessun colore", + "gui.createrailwaysnavigator.schedule_board.view_details": "Vedi dettagli", + "gui.createrailwaysnavigator.schedule_board.train_from": "da %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Partenza tra", + "gui.createrailwaysnavigator.search_options.train_groups": "Gruppi di treni", + "gui.createrailwaysnavigator.search_options.transfer_time": "Tempo di trasferimento", + "gui.createrailwaysnavigator.search_options.advanced_options": "Opzioni avanzate", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Tutti", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s esclusi", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Tutti", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s esclusi", + "gui.createrailwaysnavigator.empty_list": "La lista è vuota", + "gui.createrailwaysnavigator.new_entry.add": "Aggiungi nuovo", + "gui.createrailwaysnavigator.new_entry.new": "Nuovo:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s salvati", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Pianificazione cambiata!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "La pianificazione del percorso salvato è cambiata. Controlla il navigatore per queste modifiche.", + "gui.createrailwaysnavigator.route_overview.transfers": "Trasferimenti %s", + "gui.createrailwaysnavigator.route_overview.cancelled": "Cancellato", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Itinerario salvato", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Arrivo", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Cancellato", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Ritardo circa %s minuti", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Informazioni su %s: Questo treno è stato cancellato", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Informazioni su %s: Ritardo circa %s minuti", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", è oggi cancellato. Ci scusiamo per l'inconveniente.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", oggi circa %s minuti in ritardo.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Ragione: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Mostra dettagli", + "gui.createrailwaysnavigator.route_widget.save": "Salva", + "gui.createrailwaysnavigator.route_widget.remove": "Rimuovi", + "gui.createrailwaysnavigator.route_widget.share": "Condividi…", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "In passato", + "gui.createrailwaysnavigator.saved_routes.today": "Oggi", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Domani", + "gui.createrailwaysnavigator.saved_routes.in_days": "Fra %s giorni", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Mostra dettagli", + "gui.createrailwaysnavigator.saved_route_widget.share": "Condividi…", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Mostra notifiche", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Ritardo nelle operazioni", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Ritardo nel viaggio precedente", + "gui.createrailwaysnavigator.train_status.red_signal": "Segnale rosso", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Priorità di un altro treno", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Ritardo di un altro treno", + "gui.createrailwaysnavigator.train_status.track_closed": "Tracciato chiuso", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Carenza di personale", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Interruzione operativa", + "gui.createrailwaysnavigator.train_status.special_trip": "Viaggio speciale", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Salva percorso", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Rimuovi percorso", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Mostra suggerimenti", + "gui.createrailwaysnavigator.navigator.my_profile": "Il mio profilo", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Alcuni percorsi potrebbero essere incompleti o non suggeriti perché Create Railways Navigator non ha ancora inizializzato tutti i treni.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Linee treni", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Seleziona colore", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Crea linee ferroviarie per raggruppare diversi treni insieme, sovrascrivere il loro nome visualizzato e regola altre impostazioni per ogni linea. (Può essere assegnato solo ai treni nella pianificazione)" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/ja_jp.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/ja_jp.json new file mode 100644 index 00000000..8008ec6f --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/ja_jp.json @@ -0,0 +1,315 @@ +{ + "advancement.createrailwaysnavigator.navigator": "ご乗車ありがとうございました。", + "advancement.createrailwaysnavigator.navigator.description": "ある駅から別の駅への列車を検索するナビゲーターを作る。", + "advancement.createrailwaysnavigator.advanced_display": "4kには及ばない", + "advancement.createrailwaysnavigator.advanced_display.description": "ディスプレイボードを強化して、より多くの情報を表示できるようにするほか、からくり化した列車内にも設置できます。", + "item.createrailwaysnavigator.navigator": "ナビゲーター", + "item.createrailwaysnavigator.navigator.tooltip.summary": "ナビゲーターは乗車可能な列車を示し、停車駅・リアルタイムデータなどの追加情報を提供します。", + "block.createrailwaysnavigator.train_station_clock": "駅の時計", + "block.createrailwaysnavigator.advanced_display_block": "高度なブロック型ディスプレイ", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display": "高度なボード型ディスプレイ", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display_small": "高度な小型ディスプレイ", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display_panel": "高度なパネル型ディスプレイ", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display_half_panel": "高度なハーフパネル型ディスプレイ", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display_sloped": "高度な傾斜型ディスプレイ", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display_slab": "高度なハーフブロック型ディスプレイ", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "からくり化した列車に取りつけると行き先や旅客案内を表示したり、駅のホームで通常のディスプレイボードより多くの情報を表示できるディスプレイボードとして使えます", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "レンチで右クリックしたとき", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "ディスプレイの設定メニューを開きます。", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "回送", + "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "key.createrailwaysnavigator.route_overlay_options": "ルートオーバーレイの設定を表示", + "enum.createrailwaysnavigator.overlay_position": "表示位置", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "左上", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "右上", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "左下", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "右下", + "gui.createrailwaysnavigator.loading.title": "データをダウンロードしています", + "gui.createrailwaysnavigator.overlay_settings.title": "ルートオーバーレイ設定", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "詳細を表示", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "ルートオーバーレイを削除", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "ナレーターアナウンス有効", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "ナレーターアナウンス無効", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "通知有効", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "通知無効", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "GUI スケール", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "ナレーターのアナウンス", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "ナレーターは、次の停車駅や変更などをアナウンスします。", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "通知", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "トーストは、次の停車駅や変更などをアナウンスします。", + "gui.createrailwaysnavigator.common.expand": "詳細を表示", + "gui.createrailwaysnavigator.common.collapse": "詳細を非表示", + "gui.createrailwaysnavigator.common.go_back": "戻る", + "gui.createrailwaysnavigator.common.go_to_top": "一番上へ戻る", + "gui.createrailwaysnavigator.common.reset_defaults": "デフォルトに戻す", + "gui.createrailwaysnavigator.common.count": "カウント", + "gui.createrailwaysnavigator.common.true": "はい", + "gui.createrailwaysnavigator.common.false": "いいえ", + "gui.createrailwaysnavigator.common.search": "検索", + "gui.createrailwaysnavigator.common.auto": "オート", + "gui.createrailwaysnavigator.common.server_error": "タスク実行中にサーバーエラーが発生しました。詳細はコンソールを確認してください。", + "gui.createrailwaysnavigator.common.delete": "削除", + "gui.createrailwaysnavigator.common.add": "追加", + "gui.createrailwaysnavigator.common.help": "ヘルプ", + "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.navigator.no_connections": "接続が見つかりません。", + "gui.createrailwaysnavigator.navigator.not_searched": "まだ検索されていません。", + "gui.createrailwaysnavigator.navigator.searching": "接続を検索中...", + "gui.createrailwaysnavigator.navigator.error_title": "ナビゲートできません!", + "gui.createrailwaysnavigator.navigator.start_end_null": "出発地または目的地が空です。", + "gui.createrailwaysnavigator.navigator.start_end_equal": "出発地と目的地が同じです。", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ 接続を逃しました", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "乗換。", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "%s から", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "グローバル設定", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "検索設定", + "gui.createrailwaysnavigator.navigator.search.tooltip": "検索", + "gui.createrailwaysnavigator.navigator.location.tooltip": "最寄り駅", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "リフレッシュ", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "上下を切り替える", + "gui.createrailwaysnavigator.route_details.title": "ルート詳細", + "gui.createrailwaysnavigator.route_details.departure": "出発まで", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "乗り換えまで", + "gui.createrailwaysnavigator.route_details.transfer": "乗り換え", + "gui.createrailwaysnavigator.route_overview.title": "ルート詳細", + "gui.createrailwaysnavigator.route_overview.journey_begins": "あなたの旅が始まります %s から %s まで、出発 %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "あなたの旅が始まります %s から %s まで、出発 %s、 %s 番線", + "gui.createrailwaysnavigator.route_overview.train_details": "%s から %s まで", + "gui.createrailwaysnavigator.route_overview.next_stop": "次は %s です", + "gui.createrailwaysnavigator.route_overview.transfer": "%s から %s にお乗り換えです", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "%s から%s へ %s 番線でお乗り換えです", + "gui.createrailwaysnavigator.route_overview.journey_completed": "旅が完了しました", + "gui.createrailwaysnavigator.route_overview.after_journey": "ご乗車ありがとうございました。 %s に到着です", + "gui.createrailwaysnavigator.route_overview.next_connections": "次の接続", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "乗り換え", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "この列車は運休になりました", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "%s に関する情報", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌運休", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "接続が危険", + "gui.createrailwaysnavigator.route_overview.connection_missed": "接続を逃しました", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "この列車は運休になりました", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "%s への旅を続けることができません。", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "列車の遅延により、接続列車を逃しました。ナビゲーターで代替の手段を探してください。ご不便をおかけして申し訳ありません。", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "%s についての情報: 本日この列車は運休になりました。誠に申し訳ございません。他の選択をナビゲーターで探してください。", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "本日この列車は運休になりました", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "%s への旅を続けることができません。ナビゲーターで代替の手段を探してください。", + "gui.createrailwaysnavigator.route_overview.options": "%s を押してオプションを表示します", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "%s への旅が始まります!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s から %s まで、出発 %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s から %s まで、出発 %s、%s 番線から", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "あなたのプラットフォームが変更されました! ", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "%s のあなたの列車は、本日 %s 番線から出発します。", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: 到着 %s が遅れています。", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s が %s の代わりに %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: この列車は運休になりました", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "本日 %s から %s は運休になりました。", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "乗り換えが近づいています", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "%s から %s へ、%s 番線で乗り換え", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "%s から %s へ、 %s 番線で乗り換え", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "%s から %s へ、%s 番線で乗り換え", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "おそらく %s から %s まで到達することができないでしょう。", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "接続を逃しました", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "接続列車 %s から %s を逃しました。", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "目的地に到着しました!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "ご乗車ありがとうございました。", + "gui.createrailwaysnavigator.route_overview.date": "日 %s、%s ", + "gui.createrailwaysnavigator.global_settings.title": "グローバル設定", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "クリックで編集", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "駅タグ", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "駅タグを定義して、複数のプラットフォームを持つ駅(例:MyStation 1、MyStation 2、...)を、カスタム名で単一の駅(例:MyStation)として扱うようにします。", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "駅ブラックリスト", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "ナビゲーション結果に表示されるべきでない駅を除外します。これらの駅はルート生成時に無視されます。", + "gui.createrailwaysnavigator.global_settings.train_group.title": "列車グループ", + "gui.createrailwaysnavigator.global_settings.train_group.description": "列車グループを作成して、すべての列車を整理しましょう(例:地域サービス、長距離サービスなど)。ユーザーは、どのグループを使うかを決めることができます。", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "列車ブラックリスト", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "ルート検索の結果から、貨物列車や臨時列車などの特定の列車を除外してください。", + "gui.createrailwaysnavigator.station_tags.summary": "%s 駅は含まれています", + "gui.createrailwaysnavigator.station_tags.editor": "最後に %s が %s に編集しました", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "新しいエントリを作成する", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "タグを削除", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "駅を削除", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "クリックで変えます", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "駅を追加", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "駅名", + "gui.createrailwaysnavigator.station_tags.hint.platform": "プラットフォームのラベル", + "gui.createrailwaysnavigator.station_tags.enter_name": "名前をここに入力してください", + "gui.createrailwaysnavigator.train_group_settings.title": "列車グループ設定", + "gui.createrailwaysnavigator.train_group_settings.summary": "%s 編成の列車が含まれています", + "gui.createrailwaysnavigator.train_group_settings.editor": "最終編集者: %s、編集日時: %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "グループを削除", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "列車を削除", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "列車を追加", + "gui.createrailwaysnavigator.blacklist.title": "鉄道駅ブラックリスト", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "ブラックリストに追加", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "ブラックリストから削除", + "gui.createrailwaysnavigator.train_blacklist.title": "列車ブラックリスト", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "ブラックリストに追加", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "ブラックリストから削除", + "gui.createrailwaysnavigator.search_settings.title": "検索設定", + "gui.createrailwaysnavigator.search_settings.transfer_time": "最小乗り換え時間", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "列車を乗り換えるために確保すべき最小の時間(1時間 ~ 実際の50秒)", + "gui.createrailwaysnavigator.search_settings.train_groups": "列車カテゴリフィルター", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "どの列車カテゴリからどの列車を使用するかを決定してください。", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s カテゴリが選択されました", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "すべて", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "いいえ", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "リセットフィルター", + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "追加", + "gui.createrailwaysnavigator.time": "時間: %s", + "gui.createrailwaysnavigator.time.now": "今", + "gui.createrailwaysnavigator.time_format.dhm": "%s 日 %s 時間 %s 分", + "gui.createrailwaysnavigator.time_format.hm": "%s 時間 %s 分", + "gui.createrailwaysnavigator.time_format.m": "%s 分", + "gui.createrailwaysnavigator.platform": "ホーム", + "gui.createrailwaysnavigator.departure": "出発", + "gui.createrailwaysnavigator.destination": "目的地", + "gui.createrailwaysnavigator.line": "線 ", + "gui.createrailwaysnavigator.following_trains": "次の電車は: ", + "gui.createrailwaysnavigator.via": "経由", + "gui.createrailwaysnavigator.advanced_display_settings.title": "発展型ディスプレイの設定", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "ディスプレイタイプ", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "表示される情報を決定します。", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "情報の種類", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "情報がどれほど詳細であるかを決定します。", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "両面", + "enum.createrailwaysnavigator.display_info_type": "表示情報の種類", + "enum.createrailwaysnavigator.display_info_type.description": "表示ボードにどれだけの情報を表示するかを決定します。", + "enum.createrailwaysnavigator.display_info_type.simple": "シンプル", + "enum.createrailwaysnavigator.display_info_type.info.simple": "ディスプレイには、追加の詳細なしで最も重要な情報のみが表示されます", + "enum.createrailwaysnavigator.display_info_type.detailed": "詳細", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "最も重要な情報に加えて、列車の速度や停車駅などの詳細が表示されます", + "enum.createrailwaysnavigator.display_info_type.informative": "より詳細", + "enum.createrailwaysnavigator.display_info_type.info.informative": "興味深い可能性のあるすべての情報を表示し、おしゃれなレイアウトで表示します。テキストが非常に小さくなる可能性があるため、小さなディスプレイにはおすすめしません。", + "enum.createrailwaysnavigator.display_type": "表示タイプ", + "enum.createrailwaysnavigator.display_type.description": "表示の目的に応じた表示タイプ", + "enum.createrailwaysnavigator.display_type.train_destination": "列車の目的地", + "enum.createrailwaysnavigator.display_type.info.train_destination": "列車そのものに関する情報、例えば列車の名前、目的地、(選択された場合は)停車駅やその他の情報などを表示するため、列車の外側で使用することを意図しています。", + "enum.createrailwaysnavigator.display_type.passenger_information": "旅客案内", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "列車内に見られる表示を表します。これらの表示には、次の停車駅、出口の方向、(選択された場合は)列車の速度やルート概要が表示されます。", + "enum.createrailwaysnavigator.display_type.platform": "ホームのディスプレイ(発車標)", + "enum.createrailwaysnavigator.display_type.info.platform": "この表示は駅のホームで使用するべきもので、次に到着する列車を表示し、選択された場合は追加の詳細も表示されます。列車内では使用できません", + "enum.createrailwaysnavigator.side": "片方 ", + "enum.createrailwaysnavigator.side.description": "情報が表示されるブロックの側面", + "enum.createrailwaysnavigator.side.front": "正面", + "enum.createrailwaysnavigator.side.info.front": "情報は前面にのみ表示されます。これがデフォルトの動作です。", + "enum.createrailwaysnavigator.side.both": "両面", + "enum.createrailwaysnavigator.side.info.both": "情報は両面に表示されます。", + "enum.createrailwaysnavigator.time_display": "時刻表示", + "enum.createrailwaysnavigator.time_display.description": "時刻がどのように表示されるべきかを決定します。", + "enum.createrailwaysnavigator.time_display.abs": "ABS (絶対時刻)", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS(絶対時刻)", + "enum.createrailwaysnavigator.time_display.eta": "ETA (到着予定時刻)", + "enum.createrailwaysnavigator.time_display.info.eta": "ETA(到着予定時刻)", + "create.display_source.advanced_display": "高度なディスプレイ", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "列車名の列幅", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "ブロックピクセル単位。(デフォルト: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "ホームの列幅", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "ブロックピクセル単位。(デフォルト: 自動)", + "gui.createrailwaysnavigator.section_settings.title": "セクション設定", + "gui.createrailwaysnavigator.section_settings.train_groups": "列車グループを割り当てる", + "gui.createrailwaysnavigator.section_settings.train_lines": "路線を割り当てる", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "次のセクションの初めを含む", + "gui.createrailwaysnavigator.section_settings.usable": "移動可能", + "gui.createrailwaysnavigator.section_settings.none": "(なし)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "動的遅延", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "最短所要時間", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "待つ: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "少なくとも %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "新しい時刻表セクション", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "新しい時刻表セクションの初め", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "コンフィグ...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - 列車グル ープを設定する: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - 路線を設定する: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - 次のセクションの初めを含む: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - 移動可能: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "タイミングをリセット", + "display.createrailwaysnavigator.train_destination.simple": "コンパクト", + "display.createrailwaysnavigator.train_destination.extended": "延長", + "display.createrailwaysnavigator.train_destination.detailed": "詳細", + "display.createrailwaysnavigator.passenger_information.running_text": "スクロールテキスト", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "詳細な時刻表", + "display.createrailwaysnavigator.platform.running_text": "スクロールテキスト", + "display.createrailwaysnavigator.platform.table": "テーブル表示", + "display.createrailwaysnavigator.platform.focus": "フォーカス表示", + "gui.createrailwaysnavigator.saved_routes.title": "共有したルート", + "gui.createrailwaysnavigator.schedule_board.title": "時刻表", + "gui.createrailwaysnavigator.station_tags.title": "駅のタグ", + "gui.createrailwaysnavigator.journey_info.title": "旅の情報", + "gui.createrailwaysnavigator.journey_info.date": "%s日", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) から %s", + "gui.createrailwaysnavigator.color_picker.custom": "カスタム...", + "gui.createrailwaysnavigator.color_picker.no_color": "色なし", + "gui.createrailwaysnavigator.schedule_board.view_details": "詳細を表示", + "gui.createrailwaysnavigator.schedule_board.train_from": "%s から", + "gui.createrailwaysnavigator.search_options.departure_in": "出発は", + "gui.createrailwaysnavigator.search_options.train_groups": "列車グループ", + "gui.createrailwaysnavigator.search_options.transfer_time": "乗り換え時間", + "gui.createrailwaysnavigator.search_options.advanced_options": "詳細設定", + "gui.createrailwaysnavigator.search_options.train_groups.all": "全て", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s は除外されました", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "全て", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s は除外されました", + "gui.createrailwaysnavigator.empty_list": "このリストは空っぽです", + "gui.createrailwaysnavigator.new_entry.add": "エントリを追加する", + "gui.createrailwaysnavigator.new_entry.new": "新:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s を保存", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "時刻表は変わりました", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "共有したルートの時刻表が変わりました。変更を確認したい方はナビゲーターをご覧ください。", + "gui.createrailwaysnavigator.route_overview.transfers": "%s 乗り換え", + "gui.createrailwaysnavigator.route_overview.cancelled": "運休", + "gui.createrailwaysnavigator.saved_routes.saved_route": "ルートを共有", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "到着", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "運休", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "遅延約%s分", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "%s に関する情報: この列車は運休になりました", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "%sについての情報: 遅延約%s分", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": "は運休になりました。申し訳ありません。", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": "、今日約%s分遅延", + "block.createrailwaysnavigator.advanced_display.ber.reason": "理由: ", + "gui.createrailwaysnavigator.route_widget.show_details": "詳細を表示", + "gui.createrailwaysnavigator.route_widget.save": "保存", + "gui.createrailwaysnavigator.route_widget.remove": "削除", + "gui.createrailwaysnavigator.route_widget.share": "共有", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "過去に", + "gui.createrailwaysnavigator.saved_routes.today": "今日", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "明日", + "gui.createrailwaysnavigator.saved_routes.in_days": "後%s日", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "詳細を表示", + "gui.createrailwaysnavigator.saved_route_widget.share": "共有", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "通知を表示", + "gui.createrailwaysnavigator.train_status.unknown_delay": "業務の遅延", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "前の旅の遅延", + "gui.createrailwaysnavigator.train_status.red_signal": "赤信号", + "gui.createrailwaysnavigator.train_status.priority_other_train": "別の列車は優先されました", + "gui.createrailwaysnavigator.train_status.delay_other_train": "別の列車は遅延があります", + "gui.createrailwaysnavigator.train_status.track_closed": "線路閉鎖", + "gui.createrailwaysnavigator.train_status.staff_shortage": "スタッフ不足", + "gui.createrailwaysnavigator.train_status.operational_disruption": "業務の中断", + "gui.createrailwaysnavigator.train_status.special_trip": "特別な旅", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "ルートを共有", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "ルートを削除", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "ポップアップを表示", + "gui.createrailwaysnavigator.navigator.my_profile": "プロフィール", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Create Railways Navigator がまだすべての列車を初期化していないため、一部のルートが不完全であるか、まったく提案されない可能性があります。", + "gui.createrailwaysnavigator.global_settings.train_line.title": "鉄道路線", + "gui.createrailwaysnavigator.global_settings.train_line.color": "色の選択", + "gui.createrailwaysnavigator.global_settings.train_line.description": "路線を作成したら、異なる列車をまとめたり、表示名を上書きしたり、各路線の他のの設定を調整することができます。(時刻表内の列車のみ割り当て可能)" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json index 87d4f882..2d32f96b 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/ko_kr.json @@ -1,59 +1,49 @@ { "advancement.createrailwaysnavigator.navigator": "오늘도 열차를 이용해주셔서 고맙습니다", - "advancement.createrailwaysnavigator.navigator.description": "기차역 사이를 잇는 열차를 검색하기 위한 탐색기를 제작하세요.", + "advancement.createrailwaysnavigator.navigator.description": "기차역 사이를 잇는 열차를 검색하기 위한 탐색기를 제작하세요.", "advancement.createrailwaysnavigator.advanced_display": "4K 화질은 아니로군", "advancement.createrailwaysnavigator.advanced_display.description": "더 많은 정보를 표시하기 위해 전광판을 업그레이드하고 무려 기차 안에다가도 설치해보세요.", - - "itemGroup.createrailwaysnavigator.tab": "Create 철도 탐색기", - "item.createrailwaysnavigator.navigator": "Create 철도 탐색기", - "item.createrailwaysnavigator.navigator.tooltip.summary": "_탐색기_는 추가적인 _열차 경로_와 함께 _경유지_, _실시간 데이터_ 등 다양한 정보를 보여줍니다.", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "_탐색기_는 추가적인 _열차 경로_와 함께 _경유지_, _실시간 데이터_ 등 다양한 정보를 보여줍니다.", "block.createrailwaysnavigator.train_station_clock": "승강장 시계", "block.createrailwaysnavigator.advanced_display_block": "고급 전광판 블록", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", "block.createrailwaysnavigator.advanced_display": "고급 전광판", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", "block.createrailwaysnavigator.advanced_display_small": "소형 고급 전광판", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", "block.createrailwaysnavigator.advanced_display_panel": "고급 전광판 판넬", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", "block.createrailwaysnavigator.advanced_display_half_panel": "고급 전광판 반블록", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", "block.createrailwaysnavigator.advanced_display_sloped": "역경사형 고급 전광판", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "_도착역 정보_와 _승객 안내용 정보_를 표시하도록 _차내전광판_으로 사용하거나, _열차역_에서 사용하여 일반 전광판보다 더 많은 정보를 표시합니다.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "렌치를 사용하여 우클릭할 경우", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "_전광판_을 _설정_하는 메뉴를 엽니다.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "점검 중", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "무승객열차", - "category.createrailwaysnavigator.crn": "Create 철도 탐색기", "key.createrailwaysnavigator.route_overlay_options": "경로 오버레이 옵션 보이기", - "enum.createrailwaysnavigator.overlay_position": "안내판 위치", "enum.createrailwaysnavigator.overlay_position.info.top_left": "좌측 상단", "enum.createrailwaysnavigator.overlay_position.info.top_right": "우측 상단", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "좌측 하단", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "우측 하단", - "gui.createrailwaysnavigator.loading.title": "서버에서 데이터를 가져옵니다...", - "gui.createrailwaysnavigator.overlay_settings.title": "경로 오버레이 설정", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "자세히 보기", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "경로 오버레이 삭제", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "안내방송 활성화", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "안내방송 비활성화", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "안내방송 비활성화", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "알림 활성화", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "알림 비활성화", "gui.createrailwaysnavigator.route_overlay_settings.scale": "안내창 크기", @@ -61,7 +51,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "열차 운행 중 중요한 정보 (도착역, 변경사항 등)에 대한 음성 안내를 받습니다.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "알림", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "열차 운행 중 중요한 정보 (도착역, 변경사항 등)에 대한 알림을 받습니다.", - "gui.createrailwaysnavigator.common.expand": "자세히 보기", "gui.createrailwaysnavigator.common.collapse": "숨기기", "gui.createrailwaysnavigator.common.go_back": "뒤로", @@ -73,7 +62,6 @@ "gui.createrailwaysnavigator.common.search": "검색", "gui.createrailwaysnavigator.common.auto": "자동", "gui.createrailwaysnavigator.common.server_error": "작업을 수행하던 중 서버 오류가 발생했습니다. 자세한 정보는 콘솔을 확인하십시오.", - "gui.createrailwaysnavigator.navigator.title": "Create 철도 탐색기", "gui.createrailwaysnavigator.navigator.no_connections": "연결된 열차가 없습니다.", "gui.createrailwaysnavigator.navigator.not_searched": "아직 검색되지 않았습니다.", @@ -89,13 +77,10 @@ "gui.createrailwaysnavigator.navigator.search.tooltip": "검색", "gui.createrailwaysnavigator.navigator.location.tooltip": "현재 위치에서 가장 가까운 열차", "gui.createrailwaysnavigator.navigator.switch.tooltip": "영역 전환", - "gui.createrailwaysnavigator.route_details.title": "경로 상세", "gui.createrailwaysnavigator.route_details.departure": "출발 시각", "gui.createrailwaysnavigator.route_details.next_transfer_time": "환승 시각", "gui.createrailwaysnavigator.route_details.transfer": "환승", - "gui.createrailwaysnavigator.route_details.save_route": "연결 저장", - "gui.createrailwaysnavigator.route_overview.title": "경로 상세", "gui.createrailwaysnavigator.route_overview.journey_begins": "%s 열차, %s 행, 출발시각 %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "%s 열차, %s 행, 출발시각 %s, 타는 곳 %s", @@ -125,7 +110,7 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "타는 곳이 변경되었습니다!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "%s의 열차는 오늘 타는 곳 %s으로 들어올 예정입니다.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s의 %s 지연에 관한 안내", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s 도착 예정, 기존 시각 %s (%s)", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s 도착 예정, 기존 시각 %s (%s)", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s 열차 취소에 관한 안내", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s에서 탑승해야 할 %s행 열차가 취소되었습니다.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "곧 환승해야 합니다.", @@ -138,7 +123,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "운행 완료.", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "오늘도 철도를 이용해주셔서 고맙습니다. 가시는 목적지까지 안녕히 가십시오.", "gui.createrailwaysnavigator.route_overview.date": "%s일, %s", - "gui.createrailwaysnavigator.global_settings.title": "광역 설정", "gui.createrailwaysnavigator.global_settings.option.tooltip": "클릭하여 수정하기", "gui.createrailwaysnavigator.global_settings.option_alias.title": "기차역 태그", @@ -149,8 +133,6 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "기차 그룹을 만들어 다양한 목적의 열차(지역열차, 광역열차 등)를 관리합니다. 원하는 그룹을 사용할 수도 있습니다.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "기차 블랙리스트", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "기차 블랙리스트에 등록된 기차(예: 화물열차, 특수열차)는 경로 추천에 사용되지 않습니다.", - - "gui.createrailwaysnavigator.station_tags.title": "기차역 태그 설정", "gui.createrailwaysnavigator.station_tags.summary": "%s개의 타는 곳 포함", "gui.createrailwaysnavigator.station_tags.editor": "%s님이 %s에 마지막으로 수정함", "gui.createrailwaysnavigator.station_tags.add.tooltip": "새 태그 추가", @@ -160,22 +142,18 @@ "gui.createrailwaysnavigator.station_tags.hint.station_name": "정거장 이름", "gui.createrailwaysnavigator.station_tags.hint.platform": "타는 곳", "gui.createrailwaysnavigator.station_tags.enter_name": "이름 입력", - "gui.createrailwaysnavigator.train_group_settings.title": "기차 그룹 설정", "gui.createrailwaysnavigator.train_group_settings.summary": "%s개의 기차 포함", "gui.createrailwaysnavigator.train_group_settings.editor": "%s이/가 %s에 마지막으로 편집", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "그룹 삭제", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "기차 삭제", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "기차 추가", - "gui.createrailwaysnavigator.blacklist.title": "기차역 블랙리스트", "gui.createrailwaysnavigator.blacklist.add.tooltip": "블랙리스트에 추가", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "블랙리스트에서 삭제", - "gui.createrailwaysnavigator.train_blacklist.title": "기차 블랙리스트", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "블랙리스트에 추가", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "블랙리스트에서 삭제", - "gui.createrailwaysnavigator.search_settings.title": "검색 설정", "gui.createrailwaysnavigator.search_settings.transfer_time": "최소 환승 시간", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "환승하는 데 걸릴 최소 시간을 설정합니다. (실제 시간 기준 1시간 ~ 50초)", @@ -185,29 +163,24 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "모두선택", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "선택취소", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "필터 초기화", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "추가하기", - "gui.createrailwaysnavigator.time": "현재시각 %s", "gui.createrailwaysnavigator.time.now": "지금", "gui.createrailwaysnavigator.time_format.dhm": "%s일 %s시간 %s분", "gui.createrailwaysnavigator.time_format.hm": "%s시간 %s분", "gui.createrailwaysnavigator.time_format.m": "%s분", - "gui.createrailwaysnavigator.platform": "타는 곳", "gui.createrailwaysnavigator.departure": "출발시간", "gui.createrailwaysnavigator.destination": "도착역", "gui.createrailwaysnavigator.line": "열차종류", "gui.createrailwaysnavigator.following_trains": "다음 열차", "gui.createrailwaysnavigator.via": "경유: ", - "gui.createrailwaysnavigator.advanced_display_settings.title": "고급 전광판 설정", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "전광판 종류", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "어디에 사용할 전광판인지 결정합니다.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "표시 내용", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "내용의 상세 정도를 결정합니다.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "양면 표기", - "enum.createrailwaysnavigator.display_info_type": "전광판 표시 내용", "enum.createrailwaysnavigator.display_info_type.description": "전광판에 어느정도 내용까지 표시할지 결정합니다.", "enum.createrailwaysnavigator.display_info_type.simple": "간단하게", @@ -216,7 +189,6 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "기차 속도, 경유지와 같은 중요한 정보들도 표시합니다.", "enum.createrailwaysnavigator.display_info_type.informative": "풍부하게", "enum.createrailwaysnavigator.display_info_type.info.informative": "흥미로울 수 있는 모든 정보를 멋지게 표시합니다. 작은 전광판에선 글씨가 작아질 수 있습니다.", - "enum.createrailwaysnavigator.display_type": "전광판 종류", "enum.createrailwaysnavigator.display_type.description": "각각의 목적에 맞는 전광판을 선택합니다.", "enum.createrailwaysnavigator.display_type.train_destination": "열차 행선지", @@ -225,26 +197,22 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "기차 내부에 설치되는 전광판으로, 다음 역, 내릴 문, 기차 속도(선택한 경우), 기차 경로 정보 등을 표시합니다.", "enum.createrailwaysnavigator.display_type.platform": "승강장 전광판", "enum.createrailwaysnavigator.display_type.info.platform": "승강장에 설치되는 전광판으로, 다음에 도착할 열차와 그 열차의 정보 등을 표시합니다. 열차에는 사용할 수 없습니다!", - "enum.createrailwaysnavigator.side": "전광판 위치", "enum.createrailwaysnavigator.side.description": "전광판이 어느 쪽에 표시될지 결정합니다.", "enum.createrailwaysnavigator.side.front": "앞쪽", "enum.createrailwaysnavigator.side.info.front": "전광판의 앞면에 표시됩니다. (기본)", "enum.createrailwaysnavigator.side.both": "양쪽", "enum.createrailwaysnavigator.side.info.both": "전광판의 양면에 표시됩니다.", - "enum.createrailwaysnavigator.time_display": "시간 표기법", "enum.createrailwaysnavigator.time_display.description": "시간을 표시할 방법을 결정합니다.", "enum.createrailwaysnavigator.time_display.abs": "절대", "enum.createrailwaysnavigator.time_display.info.abs": "정확한 시간을 띄웁니다. (기본)", "enum.createrailwaysnavigator.time_display.eta": "상대", "enum.createrailwaysnavigator.time_display.info.eta": "남은 시간을 띄웁니다.", - "create.display_source.advanced_display": "고급 전광판", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "기차 이름 폭", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "픽셀 단위. (기본: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "타는 곳 이름 폭", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "픽셀 단위. (기본: 자동)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.station_tags.title": "기차역 태그 설정" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json index 58006b95..7a5ff9d9 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/nl_nl.json @@ -3,51 +3,46 @@ "advancement.createrailwaysnavigator.navigator.description": "Craft een Navigator om treinverbindingen van het ene treinstation naar het andere te zoeken.", "advancement.createrailwaysnavigator.advanced_display": "Nog net geen 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Upgrade je displayborden om meer informatie weer te geven en plaats ze zelfs in je treinen.", - - "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator": "Create Spoorweg Navigator", - "item.createrailwaysnavigator.navigator.tooltip.summary": "De navigator toont de mogelijke _treinverbindingen_ met extra informatie zoals _tussenstops_, _real-time data_ en meer.", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "De navigator toont de mogelijke _treinverbindingen_ met extra informatie zoals _tussenstops_, _real-time data_ en meer.", "block.createrailwaysnavigator.train_station_clock": "Treinstation Klok", "block.createrailwaysnavigator.advanced_display_block": "Geavanceerd Display Blok", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Als je R-Clickt met een Wrench", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display": "Geavanceerd Display", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Als je R-Clickt met een Wrench", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display_small": "Klein Geavanceerd Display", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Als je R-Clickt met een Wrench", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display_panel": "Geadvanceerd Display Paneel", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Als je R-Clickt met een Wrench", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Half Geadvanceerd Display Paneel", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Als je R-Clickt met een Wrench", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display_sloped": "Schuine Geavanceerde Display", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Gebruik het op _treinen_, als een _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _platformdisplays_ die ook meer informatie tonen dan reguliere displayborden.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Als je R-Clickt met een Wrench", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Opent een menu om het _display_ te _configureren_.", + "block.createrailwaysnavigator.advanced_display_slab": "Geavanceerde Display-plaat", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Gebruik het op _treinen_, als _treinbestemmingsdisplay_ of _passagiersinformatiedisplay_, of op _treinstations_ als verbeterde _perrondisplays_ die ook meer informatie tonen dan de normale displayborden.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "Als je R-Klikt met een Moersleutel", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Open een menu om het _display_ te _configureren_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Buiten dienst!", - "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Toon Route Overlay Opties", - "enum.createrailwaysnavigator.overlay_position": "Display Positie", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Bovenste Linkerhoek", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Bovenste Rechterhoek", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Onderste Linkerhoek", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Onderste Rechterhoek", - "gui.createrailwaysnavigator.loading.title": "Gegevens downloaden van server...", - "gui.createrailwaysnavigator.overlay_settings.title": "Route Overlay Instellingen", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Toon details", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Verwijder route overlay", @@ -60,7 +55,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "De verteller kondigt belangrijke gebeurtenissen aan tijdens uw reis, bijv. de volgende stop, wijzigingen, etc.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Meldingen", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Ontvang toast meldingen over belangrijke gebeurtenissen tijdens uw reis, bijv. de volgende stop, wijzigingen, etc.", - "gui.createrailwaysnavigator.common.expand": "Toon Details", "gui.createrailwaysnavigator.common.collapse": "Verberg Details", "gui.createrailwaysnavigator.common.go_back": "Ga Terug", @@ -72,7 +66,9 @@ "gui.createrailwaysnavigator.common.search": "Zoeken", "gui.createrailwaysnavigator.common.auto": "Auto", "gui.createrailwaysnavigator.common.server_error": "Serverfout tijdens het uitvoeren van taak. Kijk console voor details.", - + "gui.createrailwaysnavigator.common.delete": "Verwijder", + "gui.createrailwaysnavigator.common.add": "Voeg toe", + "gui.createrailwaysnavigator.common.help": "Krijg Hulp", "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "Geen verbindingen gevonden.", "gui.createrailwaysnavigator.navigator.not_searched": "Nog niets gezocht.", @@ -87,14 +83,12 @@ "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Zoekinstellingen", "gui.createrailwaysnavigator.navigator.search.tooltip": "Zoeken", "gui.createrailwaysnavigator.navigator.location.tooltip": "Dichtstbijzijnde station vanaf huidige positie", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Vernieuwen", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Velden wisselen", - - "gui.createrailwaysnavigator.route_details.title": "Route Details", + "gui.createrailwaysnavigator.route_details.title": "Routedetails", "gui.createrailwaysnavigator.route_details.departure": "Vertrek in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Overstap in", "gui.createrailwaysnavigator.route_details.transfer": "Overstap", - "gui.createrailwaysnavigator.route_details.save_route": "Verbinding opslaan", - "gui.createrailwaysnavigator.route_overview.title": "Route Details", "gui.createrailwaysnavigator.route_overview.journey_begins": "Uw reis begint! %s naar %s, vertrek %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Uw reis begint! %s naar %s, vertrek %s op platform %s", @@ -137,7 +131,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "U heeft uw bestemming bereikt!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Bedankt voor het reizen en een fijne dag", "gui.createrailwaysnavigator.route_overview.date": "Dag %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Globale Instellingen", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Klik om te bewerken", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Treinstation Tags", @@ -148,33 +141,28 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Maak treingroepen om alle treinen te organiseren (bijv. regionale diensten, langeafstandsdiensten, ...). Gebruikers kunnen beslissen welke groepen ze willen gebruiken.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Trein Zwarte Lijst", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Sluit treinen uit, bijv. vrachttreinen, speciale treinen, etc., zodat ze niet worden gebruikt in de routesuggesties.", - - "gui.createrailwaysnavigator.station_tags.title": "Treinstation Tag Instellingen", "gui.createrailwaysnavigator.station_tags.summary": "Bevat %s Spoor Stations", "gui.createrailwaysnavigator.station_tags.editor": "Laatst bewerkt door %s op %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Nieuwe invoer maken", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Tag verwijderen", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Station verwijderen", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Klik om te veranderen", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Station toevoegen", "gui.createrailwaysnavigator.station_tags.hint.station_name": "Spoor Station Naam", "gui.createrailwaysnavigator.station_tags.hint.platform": "Platform", "gui.createrailwaysnavigator.station_tags.enter_name": "Voer hier de naam in", - "gui.createrailwaysnavigator.train_group_settings.title": "Trein Groep Instellingen", "gui.createrailwaysnavigator.train_group_settings.summary": "Bevat %s Treinen", "gui.createrailwaysnavigator.train_group_settings.editor": "Laatst bewerkt door %s op %s", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Groep verwijderen", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Trein verwijderen", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Trein toevoegen", - "gui.createrailwaysnavigator.blacklist.title": "Treinstation Zwarte Lijst", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Toevoegen aan zwarte lijst", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Verwijderen van zwarte lijst", - "gui.createrailwaysnavigator.train_blacklist.title": "Trein Zwarte Lijst", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Toevoegen aan zwarte lijst", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Verwijderen van zwarte lijst", - "gui.createrailwaysnavigator.search_settings.title": "Zoekinstellingen", "gui.createrailwaysnavigator.search_settings.transfer_time": "Minimale Overstaptijd", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "De minimale tijd die beschikbaar moet zijn om van trein te wisselen. (1u ~ 50 Echte Leven Seconden)", @@ -184,29 +172,25 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Alle", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Geen", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Filter resetten", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Toevoegen", - "gui.createrailwaysnavigator.time": "Tijd: %s", "gui.createrailwaysnavigator.time.now": "nu", "gui.createrailwaysnavigator.time_format.dhm": "%s dagen %s uur. %s min.", "gui.createrailwaysnavigator.time_format.hm": "%s uur. %s min.", "gui.createrailwaysnavigator.time_format.m": "%s min.", - - "gui.createrailwaysnavigator.platform": "Platform", + "gui.createrailwaysnavigator.platform": "Perron", "gui.createrailwaysnavigator.departure": "Vertrek", "gui.createrailwaysnavigator.destination": "Bestemming", "gui.createrailwaysnavigator.line": "Lijn", "gui.createrailwaysnavigator.following_trains": "Volgende treinen:", - + "gui.createrailwaysnavigator.via": "via", "gui.createrailwaysnavigator.advanced_display_settings.title": "Geavanceerde Display Instellingen", - "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Display Type", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Type Display", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Bepaalt de weergegeven informatie.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informatie Type", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Bepaalt hoe gedetailleerd de informatie is.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dubbelzijdig", - - "enum.createrailwaysnavigator.display_info_type": "Display Info Type", + "enum.createrailwaysnavigator.display_info_type": "Type Info Display", "enum.createrailwaysnavigator.display_info_type.description": "Bepaalt hoeveel informatie er op uw displaybord moet worden weergegeven.", "enum.createrailwaysnavigator.display_info_type.simple": "Eenvoudig", "enum.createrailwaysnavigator.display_info_type.info.simple": "Het display toont alleen de belangrijkste informatie zonder extra details.", @@ -214,35 +198,118 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "De belangrijkste informatie met enkele details wordt getoond, zoals treinsnelheid, tussenstops, etc.", "enum.createrailwaysnavigator.display_info_type.informative": "Informatief", "enum.createrailwaysnavigator.display_info_type.info.informative": "Toont alle informatie die interessant zou kunnen zijn en geeft deze weer in een fancy layout. Niet aanbevolen voor kleine displays omdat de tekst erg klein kan worden.", - - "enum.createrailwaysnavigator.display_type": "Display Type", + "enum.createrailwaysnavigator.display_type": "Type Display", "enum.createrailwaysnavigator.display_type.description": "Het type van uw display dat afhangt van hun doel.", "enum.createrailwaysnavigator.display_type.train_destination": "Trein Bestemming", "enum.createrailwaysnavigator.display_type.info.train_destination": "Bedoeld om buiten treinen te worden gebruikt, omdat het informatie toont over de trein zelf, zoals de naam, bestemming en (indien geselecteerd) tussenstops en andere informatie.", "enum.createrailwaysnavigator.display_type.passenger_information": "Passagiersinformatie", "enum.createrailwaysnavigator.display_type.info.passenger_information": "Vertegenwoordigt de displays die in treinen worden gevonden. Deze displays zullen de volgende stop, de uitgangsrichting en (indien geselecteerd) de treinsnelheid en een routeoverzicht tonen.", - "enum.createrailwaysnavigator.display_type.platform": "Platform Display", + "enum.createrailwaysnavigator.display_type.platform": "Perrondisplay", "enum.createrailwaysnavigator.display_type.info.platform": "Deze displays moeten worden gebruikt op treinstation platforms en tonen de volgende aankomende treinen met extra details indien geselecteerd. Kan niet worden gebruikt in treinen!", - "enum.createrailwaysnavigator.side": "Zijde", "enum.createrailwaysnavigator.side.description": "De zijde van het blok waar de informatie op moet worden gerenderd.", "enum.createrailwaysnavigator.side.front": "Voorkant", "enum.createrailwaysnavigator.side.info.front": "De informatie wordt alleen aan de voorkant gerenderd. Dit is het standaard gedrag.", "enum.createrailwaysnavigator.side.both": "Beide zijden", "enum.createrailwaysnavigator.side.info.both": "De informatie wordt aan beide zijden gerenderd.", - "enum.createrailwaysnavigator.time_display": "Tijdweergave", "enum.createrailwaysnavigator.time_display.description": "Bepaalt hoe de tijd moet worden weergegeven.", "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absoluut)", "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (geschatte aankomsttijd)", - "create.display_source.advanced_display": "Geavanceerde Displays", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Trein Naam Kolom Breedte", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "De breedte van de treinnaamkolom in blok pixels. (Standaard: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Platform Kolom Breedte", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "De breedte van de platformkolom in blok pixels. (Standaard: Auto)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.section_settings.title": "Sectie-instellingen", + "gui.createrailwaysnavigator.section_settings.train_groups": "Treingroep Toewijzen", + "gui.createrailwaysnavigator.section_settings.train_lines": "Treinlijn Toewijzen", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Voeg begin van het volgende gedeelte toe", + "gui.createrailwaysnavigator.section_settings.usable": "Navigeerbaar", + "gui.createrailwaysnavigator.section_settings.none": "(Geen)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Dynamische vertraging", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Minimale duur", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Wacht: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "of minstens %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Nieuwe Sectie in de Dienstregeling", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Begin van een nieuwe sectie van de dienstregeling.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Configureer...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Stel Treingroep in: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Stel Treinlijn in: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Voeg begin van de volgende sectie toe: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Navigeerbaar: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Tijdstippen opnieuw instellen", + "display.createrailwaysnavigator.train_destination.simple": "Compact", + "display.createrailwaysnavigator.train_destination.extended": "Uitgebreid", + "display.createrailwaysnavigator.train_destination.detailed": "Gedetailleerd", + "display.createrailwaysnavigator.passenger_information.running_text": "Bewegende Tekst", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Gedetailleerd met Dienstregeling", + "display.createrailwaysnavigator.platform.running_text": "Bewegende Tekst", + "display.createrailwaysnavigator.platform.table": "Tabel", + "display.createrailwaysnavigator.platform.focus": "Focus", + "gui.createrailwaysnavigator.saved_routes.title": "Opgeslagen Routes", + "gui.createrailwaysnavigator.schedule_board.title": "Dienstregelingsbord", + "gui.createrailwaysnavigator.station_tags.title": "Treinstation Tag Instellingen", + "gui.createrailwaysnavigator.journey_info.title": "Reisinformatie", + "gui.createrailwaysnavigator.journey_info.date": "Dag %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) naar %s", + "gui.createrailwaysnavigator.color_picker.custom": "Aangepast...", + "gui.createrailwaysnavigator.color_picker.no_color": "Geen Kleur", + "gui.createrailwaysnavigator.schedule_board.view_details": "Bekijk Details", + "gui.createrailwaysnavigator.schedule_board.train_from": "van %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Vertrekt over", + "gui.createrailwaysnavigator.search_options.train_groups": "Treingroepen", + "gui.createrailwaysnavigator.search_options.transfer_time": "Overstaptijd", + "gui.createrailwaysnavigator.search_options.advanced_options": "Geavanceerde Opties", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Alle", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s uitgezonderd", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Alle", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s uitgezonderd", + "gui.createrailwaysnavigator.empty_list": "De lijst is leeg", + "gui.createrailwaysnavigator.new_entry.add": "Voeg nieuw item toe", + "gui.createrailwaysnavigator.new_entry.new": "Nieuw:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s opgeslagen", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "De Dienstregeling is gewijzigd!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "De Dienstregeling van een van uw opgeslagen routes is gewijzigd. Raadpleeg de navigator voor deze wijzigingen.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Transfers", + "gui.createrailwaysnavigator.route_overview.cancelled": "Geannuleerd", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Opgeslagen Route", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Aankomst", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Geannuleerd", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Vertraging ca. %s minuten", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Informatie over %s: Deze trein is geannuleerd", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Informatie over %s: Vertraging ca. %s minuten", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", is geannuleerd vandaag. Onze excuses voor het ongemak.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", vandaag ca. %s minuten vertraagd.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Reden: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Toon details", + "gui.createrailwaysnavigator.route_widget.save": "Sla op", + "gui.createrailwaysnavigator.route_widget.remove": "Verwijder", + "gui.createrailwaysnavigator.route_widget.share": "Deel...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "In het verleden", + "gui.createrailwaysnavigator.saved_routes.today": "Vandaag", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Morgen", + "gui.createrailwaysnavigator.saved_routes.in_days": "Over %s dagen", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Toon details", + "gui.createrailwaysnavigator.saved_route_widget.share": "Deel...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Toon Notificaties", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Vertraging door operaties", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Vertraging door de vorige reis", + "gui.createrailwaysnavigator.train_status.red_signal": "Rood Signaal", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Prioriteit van een andere trein", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Vertraging van een andere trein", + "gui.createrailwaysnavigator.train_status.track_closed": "Baan gesloten", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Personeelstekort", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Operationele verstoring", + "gui.createrailwaysnavigator.train_status.special_trip": "Bijzondere reis", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Sla route op", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Verwijder route", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Toon Pop-up", + "gui.createrailwaysnavigator.navigator.my_profile": "Mijn Profiel", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Sommige routes zijn mogelijk onvolledig of helemaal niet voorgesteld, omdat Create Railways Navigator nog niet alle treinen heeft geïnitialiseerd.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Treinlijnen", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Selecteer Kleur", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Creëer treinlijnen om verschillende treinen te groeperen, overschrijf hun weergavenaam en pas andere instellingen voor elke lijn aan. (Kan alleen worden toegewezen aan de treinen in de dienstregeling)" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json index 1999b03d..b58ecd19 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/pl_pl.json @@ -1,53 +1,48 @@ { "advancement.createrailwaysnavigator.navigator": "Dziękujemy za podróż", - "advancement.createrailwaysnavigator.navigator.description": "Stwórz Rozkład aby znaleźć połączenie pomiędzy stacjami.", + "advancement.createrailwaysnavigator.navigator.description": "Stwórz Nawigator, aby znaleźć połączenie pomiędzy stacjami.", "advancement.createrailwaysnavigator.advanced_display": "Prawie 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Ulepsz swoje tablice wyświetlające aby wyświetlać jeszcze więcej informacji i umieszczać je w pociągach.", - - "itemGroup.createrailwaysnavigator,tab": "Create Railways Navigator", - - "item.createrailwaysnavigator.navigator": "Rozkład Kolei Create", + "item.createrailwaysnavigator.navigator": "Create Informacja Kolejowa", "item.createrailwaysnavigator.navigator.tooltip.summary": "_Rozkład_ wyświetla możliwe _połączenia_ z dodatkowymi informacjami takimi jak _przystanki_, _informacje na żywo_, itp.", - "block.createrailwaysnavigator.train_station_clock": "Zegar dworcowy", "block.createrailwaysnavigator.advanced_display_block": "Blok Zaawansowanego Wyświetlacza", "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Używaj na _pociągach_ jako _wyświetlacz celu podróży_ lub _ekranu informacji pasażerskiej_, albo na _stacjach kolejowych_ jako ulepszonych _wyświetlaczy peronowych_ które pokazują więcej informacji niż zwykłe wyświetlacze.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Kliknięcie PPM przy pomocy Klucza", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Otwórz menu, aby _skonfigurować_ _wyświetlacz_.", "block.createrailwaysnavigator.advanced_display": "Zaawansowany Wyświetlacz", "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Używaj na _pociągach_ jako _wyświetlacz celu podróży_ lub _ekranu informacji pasażerskiej_, albo na _stacjach kolejowych_ jako ulepszonych _wyświetlaczy peronowych_ które pokazują więcej informacji niż zwykłe wyświetlacze.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Kliknięcie PPM przy pomocy Klucza", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Otwórz menu, aby _skonfigurować_ _wyświetlacz_.", "block.createrailwaysnavigator.advanced_display_small": "Mały Zaawansowany Wyświetlacz", "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Używaj na _pociągach_ jako _wyświetlacz celu podróży_ lub _ekranu informacji pasażerskiej_, albo na _stacjach kolejowych_ jako ulepszonych _wyświetlaczy peronowych_ które pokazują więcej informacji niż zwykłe wyświetlacze.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Kliknięcie PPM przy pomocy Klucza", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Otwórz menu, aby _skonfigurować_ _wyświetlacz_.", "block.createrailwaysnavigator.advanced_display_panel": "Panel Zaawansowanego Wyświetlacza", "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Używaj na _pociągach_ jako _wyświetlacz celu podróży_ lub _ekranu informacji pasażerskiej_, albo na _stacjach kolejowych_ jako ulepszonych _wyświetlaczy peronowych_ które pokazują więcej informacji niż zwykłe wyświetlacze.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Kliknięcie PPM przy pomocy Klucza", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Otwórz menu, aby _skonfigurować_ _wyświetlacz_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Pół zaawansowany panel wyświetlacza", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Używaj na _pociągach_ jako _wyświetlacz celu podróży_ lub _ekranu informacji pasażerskiej_, albo na _stacjach kolejowych_ jako ulepszonych _wyświetlaczy peronowych_ które pokazują więcej informacji niż zwykłe wyświetlacze.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Kliknięcie PPM przy pomocy Klucza", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", - "block.createrailwaysnavigator.advanced_display_sloped": "Pochylony, Zaawansowanego Wyświetlacza", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Otwórz menu, aby _skonfigurować_ _wyświetlacz_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Pochyły Zaawansowany Wyświetlacz", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Używaj na _pociągach_ jako _wyświetlacz celu podróży_ lub _ekranu informacji pasażerskiej_, albo na _stacjach kolejowych_ jako ulepszonych _wyświetlaczy peronowych_ które pokazują więcej informacji niż zwykłe wyświetlacze.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Kliknięcie PPM przy pomocy Klucza", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Otwórz menu _konfiguracji_ _wyświetlacza_.", - + "block.createrailwaysnavigator.advanced_display_slab": "Półblokowy zaawansowany Wyświetlacz", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Użyj na pociągach jako wyświetlacz stacji docelowej, jako wyświetlacz informacji pasażerskiej lub na stacjach jako ulepszony wyświetlacz peronowy, który pokazuje więcej informacji.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "Gdy Kliknięty PPM przy pomocy Klucza", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Otwórz menu, by skonfigurować wyświetlacz.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Nieczynny!", - - "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "category.createrailwaysnavigator.crn": "Create Informacja Kolejowa", "key.createrailwaysnavigator.route_overlay_options": "Pokaż opcje nakładki trasy", - "enum.createrailwaysnavigator.overlay_position": "Pozycja wyświetlania", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Lewy górny róg", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Prawy górny róg", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Dolny lewy róg", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Dolny prawy róg", - "gui.createrailwaysnavigator.loading.title": "Pobieranie danych z serwera...", - "gui.createrailwaysnavigator.overlay_settings.title": "Opcje nakładki trasy", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Pokaż szczegóły", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Usuń nakładkę trasy", @@ -55,12 +50,11 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Narrator wyłączony.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Powiadomienie włączone", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Powiadomienie wyłączone", - "gui.createrailwaysnavigator.route_overlay_settings.scale": "Skala nakładki", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Skala interfejsu", "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Narrator", "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Narrator zapowiada wydarzenia na twojej trasie, np. przystanki, przesiadki, itp.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Powiadomienia", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Otrzymuj powiadomienia o wydarzeniach w podróży, np. przystanki, przesiadki, itp.", - "gui.createrailwaysnavigator.common.expand": "Pokaż szczegóły", "gui.createrailwaysnavigator.common.collapse": "Ukryj szczegóły", "gui.createrailwaysnavigator.common.go_back": "Wstecz", @@ -70,35 +64,35 @@ "gui.createrailwaysnavigator.common.true": "Tak", "gui.createrailwaysnavigator.common.false": "Nie", "gui.createrailwaysnavigator.common.search": "Szukaj", - "gui.createrailwaysnavigator.common.auto": "Auto", - "gui.createrailwaysnavigator.common.server_error": "Nastąpił błąd podczas wykonywania zadania. Sprawdź konsolę aby otrzymać szczegóły.", - - "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.common.auto": "Automatycznie", + "gui.createrailwaysnavigator.common.server_error": "Nastąpił błąd podczas wykonywania zadania. Sprawdź konsolę, aby zobaczyć szczegóły.", + "gui.createrailwaysnavigator.common.delete": "Usuń", + "gui.createrailwaysnavigator.common.add": "Dodaj", + "gui.createrailwaysnavigator.common.help": "Uzyskaj pomoc", + "gui.createrailwaysnavigator.navigator.title": "Create Informacja Kolejowa", "gui.createrailwaysnavigator.navigator.no_connections": "Nie znaleziono połączeń.", "gui.createrailwaysnavigator.navigator.not_searched": "Jeszcze nic nie wyszukano.", - "gui.createrailwaysnavigator.navigator.searching": "Szukanie połączenia...", + "gui.createrailwaysnavigator.navigator.searching": "Wyszukaj połączenia...", "gui.createrailwaysnavigator.navigator.error_title": "Nie udało się nawigować!", "gui.createrailwaysnavigator.navigator.start_end_null": "Początek lub cel podróży jest pusty.", - "gui.createrailwaysnavigator.navigator.start_end_equal": "Początek i cel podróży jest identyczny.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Początek i cel podróży są identyczne.", "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Połączenie z przeszłości", - "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Prze.", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Przesiadka.", "gui.createrailwaysnavigator.navigator.route_entry.station_start": "z %s", "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Ustawienia globalne", "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Ustawienia wyszukiwania", - "gui.createrailwaysnavigator.navigator.search.tooltip": "Search", - "gui.createrailwaysnavigator.navigator.location.tooltip": "Nearest station from current position", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Szukaj", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Najbliższa stacja od bieżącej pozycji", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Odśwież", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Zamień pola", - "gui.createrailwaysnavigator.route_details.title": "Szczegóły podróży", "gui.createrailwaysnavigator.route_details.departure": "Odjazd za", - "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transfer in", - "gui.createrailwaysnavigator.route_details.transfer": "Transfer", - "gui.createrailwaysnavigator.route_details.save_route": "Zapisz połączenie", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Przesiadka w", + "gui.createrailwaysnavigator.route_details.transfer": "Przesiadka", "gui.createrailwaysnavigator.route_overview.title": "Szczegóły podróży", - "gui.createrailwaysnavigator.route_overview.journey_begins": "Twoja podróż rozpoczyna się! %s do %s, odjazd %s", - "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Twoja podróż rozpoczyna się! %s do %s, odjazd %s na peronie %s", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Twoja podróż się rozpoczęła! %s do %s, odjazd %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Twoja podróż się rozpoczęła! %s do %s, odjazd %s na peronie %s", "gui.createrailwaysnavigator.route_overview.train_details": "%s do %s", - "gui.createrailwaysnavigator.route_overview.train_speed": "%s km/h", "gui.createrailwaysnavigator.route_overview.next_stop": "Następna stacja: %s", "gui.createrailwaysnavigator.route_overview.transfer": "Przesiadka do %s → %s", "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Przesiadka do %s → %s na peronie %s", @@ -113,138 +107,209 @@ "gui.createrailwaysnavigator.route_overview.connection_missed": "Połączenie przegapione", "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Pociąg odwołany", "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Twoja podróż do %s jest niemożliwa do kontynuowania.", - "gui.createrailwaysnavigator.route_overview.connection_missed_info": "W związku z opóżnieniem pociągu, przegapiłeś połączenie. Wyszukaj inne połączenie w Nawigatorze. Za utrudnienia przepraszamy.", - "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informacja o %s: Ten pociąg został odwołany! Za utrudnienia przepraszamy. Wyszukaj inne połączenie w Nawigatorze.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "W związku z opóźnieniem pociągu przegapiłeś połączenie. Wyszukaj inne połączenie w Nawigatorze. Za utrudnienia przepraszamy.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informacja o %s: Ten pociąg został dzisiaj odwołany! Za utrudnienia przepraszamy. Wyszukaj inne połączenie w Nawigatorze.", "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Pociąg został odwołany", "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Twoja podróż do %s jest niemożliwa do kontynuowania. Wyszukaj inne połączenie w Nawigatorze.", "gui.createrailwaysnavigator.route_overview.options": "Wciśnij %s aby zobaczyć opcje.", - "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Twoja podróż do %s rozpoczyna się!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Twoja podróż do %s została rozpoczęta!", "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s do %s, odjazd %s", "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s do %s, odjazd %s z peronu %s", "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Peron uległ zmianie!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Twój pociąg w %s wyrusza dzisiaj z peronu %s.", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Przyjazd %s opóżniony.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Przyjazd %s opóźniony.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s zamiast %s w %s", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Pociąg odwołany", - "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s do %s został odwołany.", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s do %s został dzisiaj odwołany.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Nadchodzi przesiadka", - "gui.createrailwaysnavigator.route_overview.notification.transfer": "Przesiadka %s → %s na peronie %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Przesiadka %s → %s", "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Przesiadka %s → %s na peronie %s", "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Twoje połączenie jest zagrożone!", "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Prawdopodobnie nie dotrzesz %s do %s.", "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Przegapione połączenie", "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Przegapiłeś swoje połączenie %s do %s.", "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Dotarłeś do swojego celu!", - "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Dziękujemy za podróż i życzymy miłego dnia.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Dziękujemy za podróż i życzymy miłego dnia", "gui.createrailwaysnavigator.route_overview.date": "Dzień %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Ustawienia Globalne", - "gui.createrailwaysnavigator.global_settings.option.tooltip": "Kliknij aby edytować", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Kliknij, aby edytować", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Tagi stacji", - "gui.createrailwaysnavigator.global_settings.option_alias.description": "Zdefiniuj tagi stacji aby traktować wieloperonowe stacje (np. MyStation 1, MyStation 2, ...) jako jedną stację (np. MyStation) z niestandardową nazwą.", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Zdefiniuj tagi stacji, aby traktować wieloperonowe stacje (np. MojaStacja 1, MojaStacja 2, ...) jako jedną stację (np. MojaStacja) z niestandardową nazwą.", "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Czarna lista stacji", - "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Wyklucz stacje aby nie pojawiały się one w wynikach wyszukiwania. Te stacje będą ignorowane podczas generowania wyników.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Wyklucz stacje, aby nie pojawiały się one w wynikach wyszukiwania. Te stacje będą ignorowane podczas generowania wyników.", "gui.createrailwaysnavigator.global_settings.train_group.title": "Grupy pociągów", "gui.createrailwaysnavigator.global_settings.train_group.description": "Utwórz grupy pociągów aby podzielić wszystkie pociągi (np. Regio, Krajowy, ...). Użytkownicy mogą decydować którą grupą chcą się poruszać.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Czarna lista pociągów", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Wyklucz pociągi, np. towarowe, specjalne, itp., aby nie pojawiały się one w wynikach wyszukiwania.", - - "gui.createrailwaysnavigator.station_tags.title": "Ustawienia tagów stacji", "gui.createrailwaysnavigator.station_tags.summary": "Zawiera %s stacji", "gui.createrailwaysnavigator.station_tags.editor": "Ostatnio edytowane przez %s o %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Utwórz nowy wpis", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Usuń tag", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Usuń stację", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Kliknij, aby zmienić", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Dodaj stację", "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nazwa stacji", - "gui.createrailwaysnavigator.station_tags.hint.platform": "Peron", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Numer peronu", "gui.createrailwaysnavigator.station_tags.enter_name": "Wprowadź nazwę", - "gui.createrailwaysnavigator.train_group_settings.title": "Ustawienia grupy pociągów", "gui.createrailwaysnavigator.train_group_settings.summary": "Zawiera %s pociągów", "gui.createrailwaysnavigator.train_group_settings.editor": "Ostatnio edytowane przez %s o %s", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Usuń grupę", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Usuń pociąg", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Dodaj pociąg", - "gui.createrailwaysnavigator.blacklist.title": "Czarna lista stacji", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Dodaj do czarnej listy", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Usuń z czarnej listy", - "gui.createrailwaysnavigator.train_blacklist.title": "Czarna lista pociągów", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Dodaj do czarnej listy", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Usuń z czarnej listy", - "gui.createrailwaysnavigator.search_settings.title": "Ustawienia wyszukiwania", "gui.createrailwaysnavigator.search_settings.transfer_time": "Minimalny czas przesiadki", - "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Minimalny czas który powinien być dostępny aby się przesiąść (1godz. w grze ~ 50 sekund)", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Minimalny czas na przesiadkę (1 godzina w grze ~ 50 sekund)", "gui.createrailwaysnavigator.search_settings.train_groups": "Filtr kategorii pociągów", - "gui.createrailwaysnavigator.search_settings.train_groups.description": "Zdecyduj z których kategorii chcesz korzystać.", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Zdecyduj, z których pociągów, jakich kategorii chcesz korzystać.", "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s wybranych kategorii", "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Wszystkie", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Brak", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Resetuj filtr", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Dodaj", - "gui.createrailwaysnavigator.time": "Godzina: %s", "gui.createrailwaysnavigator.time.now": "teraz", "gui.createrailwaysnavigator.time_format.dhm": "%s dni %s godz. %s min.", "gui.createrailwaysnavigator.time_format.hm": "%s godz. %s min.", "gui.createrailwaysnavigator.time_format.m": "%s min.", - "gui.createrailwaysnavigator.platform": "Peron", "gui.createrailwaysnavigator.departure": "Odjazd", "gui.createrailwaysnavigator.destination": "Stacja docelowa", "gui.createrailwaysnavigator.line": "Linia", "gui.createrailwaysnavigator.following_trains": "Następne pociągi:", "gui.createrailwaysnavigator.via": "przez", - "gui.createrailwaysnavigator.advanced_display_settings.title": "Ustawienia Zaawansowanego Wyświetlacza", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Rodzaj wyświetlacza", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Decyduje o wyświetlanej informacji.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Rodzaj informacji", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Określa stopień szczegółowości informacji.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dwustronna", - "enum.createrailwaysnavigator.display_info_type": "Rodzaj informacji", - "enum.createrailwaysnavigator.display_info_type.description": "Decyduje o tym jak dużo informacji powinno być wyświetlanych.", + "enum.createrailwaysnavigator.display_info_type.description": "Decyduje o tym, jak dużo informacji powinno być wyświetlanych.", "enum.createrailwaysnavigator.display_info_type.simple": "Proste", - "enum.createrailwaysnavigator.display_info_type.info.simple": "Pokazuje tylko najważnejsze informacje bez dodatkowych szczegółów.", - "enum.createrailwaysnavigator.display_info_type.detailed": "Złożone", + "enum.createrailwaysnavigator.display_info_type.info.simple": "Pokazuje tylko najważniejsze informacje bez dodatkowych szczegółów.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Rozszerzone", "enum.createrailwaysnavigator.display_info_type.info.detailed": "Pokazuje tylko najważniejsze informacje takie jak prędkość pociągu, przystanki, itp.", "enum.createrailwaysnavigator.display_info_type.informative": "Szczegółowe", - "enum.createrailwaysnavigator.display_info_type.info.informative": "Pokazuje wszystkie informacji w ładnym układzie. Nie zalecane dla małych wyświelaczy ponieważ tekst może stać się bardzo mały.", - + "enum.createrailwaysnavigator.display_info_type.info.informative": "Pokazuje wszystkie informacje w ładnym układzie. Nie zalecane dla małych wyświetlaczy, ponieważ tekst może stać się bardzo mały.", "enum.createrailwaysnavigator.display_type": "Typ wyświetlacza", - "enum.createrailwaysnavigator.display_type.description": "Rodzaj wyświetlacza który zależy od jego przeznaczenia.", + "enum.createrailwaysnavigator.display_type.description": "Rodzaj wyświetlacza, który zależy od jego przeznaczenia.", "enum.createrailwaysnavigator.display_type.train_destination": "Cel pociągu", - "enum.createrailwaysnavigator.display_type.info.train_destination": "Używany na zewnątrz pociągu w celu pokazywa informacji takich jak nazwa pociągu, jego cel, i (jeśli zaznaczono) przystanków i innych informacji.", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Przeznaczony do użycia na zewnątrz pociągu w celu wyświetlania informacji takich jak: nazwa pociągu, jego cel, i (jeśli zaznaczono) przystanków oraz innych informacji.", "enum.createrailwaysnavigator.display_type.passenger_information": "Informacja pasażerska", - "enum.createrailwaysnavigator.display_type.info.passenger_information": "Wyświetlacze wewnątrz pociągu. Te wyświetlacze będą pokazywać następny przystanek, kierunek wyjścia i (jeśli zaznaczono) prędkość pociągu i przegląd trasy.", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Wyświetlacze wewnątrz pociągu. Te wyświetlacze będą pokazywać następny przystanek, kierunek wyjścia i (jeśli zaznaczono) prędkość i trasę pociągu.", "enum.createrailwaysnavigator.display_type.platform": "Wyświetlacz peronowy", - "enum.createrailwaysnavigator.display_type.info.platform": "Te wyświetlacze powinny być używane na peronach stacji i pokazywać pociągi i informacje o nich. Nie można używać w pociągach!", - + "enum.createrailwaysnavigator.display_type.info.platform": "Te wyświetlacze powinny być używane na peronach stacji i pokazywać pociągi oraz dodatkowe informacje o nich. Nie można ich używać w pociągach!", "enum.createrailwaysnavigator.side": "Strona", - "enum.createrailwaysnavigator.side.description": "Strona bloku po której powinna być wyświetlana treść.", + "enum.createrailwaysnavigator.side.description": "Strona bloku, po której powinna być wyświetlana treść.", "enum.createrailwaysnavigator.side.front": "Przód", "enum.createrailwaysnavigator.side.info.front": "Informacje będą wyświetlane tylko na przedniej stronie. To jest domyślne zachowanie.", "enum.createrailwaysnavigator.side.both": "Obie strony", - "enum.createrailwaysnavigator.side.info.both": "Informacje będą wyświetlane tylko na obu stronach.", - - "enum.createrailwaysnavigator.time_display": "Wyświetlanie czasu", + "enum.createrailwaysnavigator.side.info.both": "Informacje będą wyświetlane na obu stronach.", + "enum.createrailwaysnavigator.time_display": "Wyświetlacz czasu", "enum.createrailwaysnavigator.time_display.description": "Określa sposób wyświetlania czasu.", "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absolutny)", "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (Przewidywany czas przybycia)", - "create.display_source.advanced_display": "Zaawansowane Wyświetlacze", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Szerokość kolumny nazw pociągów", - "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "Szerokość kolumny w pixelach blokowych. (Domyślnie: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "Szerokość kolumny w pixelach. (Domyślnie: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Szerokość kolumny peronu", - "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "Szerokość kolumny w pixelach blokowych. (-1 = Auto, Domyślnie: -1)", - - "createrailwaysnavigator.moin": "moin" - } - \ No newline at end of file + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "Szerokość kolumny w pixelach. (Domyślnie: Automatycznie)", + "gui.createrailwaysnavigator.section_settings.title": "Ustawienia sekcji", + "gui.createrailwaysnavigator.section_settings.train_groups": "Przypisz grupę pociągów", + "gui.createrailwaysnavigator.section_settings.train_lines": "Przypisz linię pociągów", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Uwzględnij początek następnej sekcji", + "gui.createrailwaysnavigator.section_settings.usable": "Przejazd Pasażerski", + "gui.createrailwaysnavigator.section_settings.none": "(Brak)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Dynamiczne Opóźnienie", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Minimalny czas trwania", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Czekaj: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "lub przynajmniej %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Nowy bieg pociągu", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Początek nowego biegu pociągu.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Konfiguruj...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Ustaw grupę pociągu: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Ustaw linię pociągu: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Uwzględnij początek następnej sekcji: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Przejazd Pasażerski: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Zresetuj Opóźnienia", + "display.createrailwaysnavigator.train_destination.simple": "Skrócony", + "display.createrailwaysnavigator.train_destination.extended": "Rozszerzony", + "display.createrailwaysnavigator.train_destination.detailed": "Szczegółowy", + "display.createrailwaysnavigator.passenger_information.running_text": "Tekst przewijający", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Szczegółowe z Rozkładem", + "display.createrailwaysnavigator.platform.running_text": "Tekst przewijający", + "display.createrailwaysnavigator.platform.table": "Tabela", + "display.createrailwaysnavigator.platform.focus": "Skupiony", + "gui.createrailwaysnavigator.saved_routes.title": "Zapisane trasy", + "gui.createrailwaysnavigator.schedule_board.title": "Tablica Odjazdów", + "gui.createrailwaysnavigator.station_tags.title": "Ustawienia tagów stacji", + "gui.createrailwaysnavigator.journey_info.title": "Informacje o podróży", + "gui.createrailwaysnavigator.journey_info.date": "Dzień, %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) do %s", + "gui.createrailwaysnavigator.color_picker.custom": "Niestandardowy...", + "gui.createrailwaysnavigator.color_picker.no_color": "Brak koloru", + "gui.createrailwaysnavigator.schedule_board.view_details": "Wyświetl szczegóły", + "gui.createrailwaysnavigator.schedule_board.train_from": "z %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Odjazd za", + "gui.createrailwaysnavigator.search_options.train_groups": "Grupy pociągów", + "gui.createrailwaysnavigator.search_options.transfer_time": "Czas przesiadki", + "gui.createrailwaysnavigator.search_options.advanced_options": "Opcje zaawansowane", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Wszystkie", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s wykluczonych", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Wszystkie", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s wykluczonych", + "gui.createrailwaysnavigator.empty_list": "Lista jest pusta", + "gui.createrailwaysnavigator.new_entry.add": "Dodaj nowy wpis", + "gui.createrailwaysnavigator.new_entry.new": "Nowe:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s zapisane", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Rozkład został zmieniony!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "Rozkład zapisanej trasy uległ zmianie. Sprawdź nawigator, aby je zobaczyć.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Przesiadki", + "gui.createrailwaysnavigator.route_overview.cancelled": "Odwołano", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Zapisane trasy", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Czas przyjazdu", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Odwołano", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Opóźnienie około %s minut", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Informacje o %s: Ten pociąg został odwołany", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Informacje o %s: Opóźnienie około %s minut", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", został dzisiaj odwołany. Przepraszamy za niedogodności.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", dzisiaj około %s minut opóźnienia.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Powód: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Pokaż szczegóły", + "gui.createrailwaysnavigator.route_widget.save": "Zapisz", + "gui.createrailwaysnavigator.route_widget.remove": "Usuń", + "gui.createrailwaysnavigator.route_widget.share": "Udostępnij...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "W przeszłości", + "gui.createrailwaysnavigator.saved_routes.today": "Dziś", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Jutro", + "gui.createrailwaysnavigator.saved_routes.in_days": "Za %s dni", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Pokaż szczegóły", + "gui.createrailwaysnavigator.saved_route_widget.share": "Udostępnij...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Wyświetl powiadomienia", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Opóźnienie w jeździe", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Opóźnienie w poprzedniej podróży", + "gui.createrailwaysnavigator.train_status.red_signal": "Sygnał czerwony", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Priorytet innego pociągu", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Opóźnienie innego pociągu", + "gui.createrailwaysnavigator.train_status.track_closed": "Tor zamknięty", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Niedobór pracowników", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Zakłócenie w funkcjonowaniu pociągu", + "gui.createrailwaysnavigator.train_status.special_trip": "Podróż Specjalna", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Zapisz trasę", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Usuń Przejazd", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Wyświetl wyskakujące okienko", + "gui.createrailwaysnavigator.navigator.my_profile": "Mój profil", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Niektóre pociągi mogą jeszcze się nie pokazywać, bo ich rozkład jazdy nie został jeszcze obliczony.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Linie pociągowe", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Wybierz kolor", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Utwórz linię pociągową, aby zgrupować różne pociągi razem pod jedną nazwą i zmienić różne ustawienia. (Mogą tylko być ustawione w Rozkładzie Jazdy)" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_br.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_br.json new file mode 100644 index 00000000..0605fbfb --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_br.json @@ -0,0 +1,315 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Um viajante!", + "advancement.createrailwaysnavigator.navigator.description": "Fabrique um Navegador para buscar por conexões de trem de uma estação para a outra.", + "advancement.createrailwaysnavigator.advanced_display": "Não de última geração", + "advancement.createrailwaysnavigator.advanced_display.description": "Atualize suas placas de exibição e coloque-as em seu trem.", + "item.createrailwaysnavigator.navigator": "Create Railways Navigator", + "item.createrailwaysnavigator.navigator.tooltip.summary": "O _navegador_ exibe possíveis _conexões de trem_ com informações adicionais como _escalas_, _dados em tempo real_ e mais.", + "block.createrailwaysnavigator.train_station_clock": "Relógio de estação de trem", + "block.createrailwaysnavigator.advanced_display_block": "Bloco de exibição avançada", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display": "Placa de exibição avançada", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display_small": "Pequena exibição avançada", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display_panel": "Painel de exibição avançada", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Painel de exibição meio avançada", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Exibição avançada inclinada", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display_slab": "Laje de exibição avançada", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Use em _trens_, como _exibição de destino de trem_ ou _exibição de informação do passageiro_, ou em _estações de trem_ como _exibições de plataforma_ melhorada em que exibe mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "Ao clicar com botão direito do mouse usando uma chave-inglesa", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Abra o menu para _ajustar_ a _exibição_.", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Fora de serviço!", + "category.createrailwaysnavigator.crn": "Create Railways Navigator", + "key.createrailwaysnavigator.route_overlay_options": "Mostrar opções de sobreposição de rota", + "enum.createrailwaysnavigator.overlay_position": "Posição da exibição", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Canto superior esquerdo", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Canto superior direito", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Canto inferior esquerdo", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Canto inferior direito", + "gui.createrailwaysnavigator.loading.title": "Baixando dados do servidor...", + "gui.createrailwaysnavigator.overlay_settings.title": "Opções de sobreposição da rota", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Exibir detalhes", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Remover sobreposição da rota", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Anunciados do narrador ativado.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Anunciados do narrador desativado.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Notificações ativadas", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Notificações desativadas", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Escala da interface", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Anunciados do narrador", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "O narrador anuncia eventos importantes na sua jornada, p. e.x. a próxima parada, alterações, etc.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notificações", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Obtenha notificações quentinhas sobre eventos importantes na sua jornada, p. e.x. a próxima parada, alterações, etc.", + "gui.createrailwaysnavigator.common.expand": "Exibir detalhes", + "gui.createrailwaysnavigator.common.collapse": "Ocultar detalhes", + "gui.createrailwaysnavigator.common.go_back": "Voltar", + "gui.createrailwaysnavigator.common.go_to_top": "Voltar ao topo", + "gui.createrailwaysnavigator.common.reset_defaults": "Redefinir ao padrão", + "gui.createrailwaysnavigator.common.count": "Contar", + "gui.createrailwaysnavigator.common.true": "Sim", + "gui.createrailwaysnavigator.common.false": "Não", + "gui.createrailwaysnavigator.common.search": "Buscar", + "gui.createrailwaysnavigator.common.auto": "Automático", + "gui.createrailwaysnavigator.common.server_error": "Erro do servidor ao executar tarefa. Confira o console para mais detalhes.", + "gui.createrailwaysnavigator.common.delete": "Excluir", + "gui.createrailwaysnavigator.common.add": "Adicionar", + "gui.createrailwaysnavigator.common.help": "Obter ajuda", + "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.navigator.no_connections": "Nenhuma conexão encontrada.", + "gui.createrailwaysnavigator.navigator.not_searched": "Nada buscado ainda.", + "gui.createrailwaysnavigator.navigator.searching": "Procurando por conexões...", + "gui.createrailwaysnavigator.navigator.error_title": "Não foi possível navegar!", + "gui.createrailwaysnavigator.navigator.start_end_null": "Iniciar ou o destino está vazio.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Iniciar e o destino são iguais.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Conexões com o passado", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Transferir", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "de %s", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Opções globais", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Opções de busca", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Buscar", + "gui.createrailwaysnavigator.navigator.location.tooltip": "A estação mais próxima da posição atual", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Atualizar", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Alterar campos", + "gui.createrailwaysnavigator.route_details.title": "Detalhes da rota", + "gui.createrailwaysnavigator.route_details.departure": "Partida em", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transferir em", + "gui.createrailwaysnavigator.route_details.transfer": "Transferir", + "gui.createrailwaysnavigator.route_overview.title": "Empresa de viagem", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Sua jornada inicia! %s para %s, partida %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Sua jornada inicia! %s para %s, partida %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.train_details": "%s para %s", + "gui.createrailwaysnavigator.route_overview.next_stop": "Próxima parada: %s", + "gui.createrailwaysnavigator.route_overview.transfer": "Alterar para %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Alterar para %s → %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Jornada concluída", + "gui.createrailwaysnavigator.route_overview.after_journey": "Você alcançou %s. Obrigada por viajar e tenha um ótimo dia.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Próximas conexões", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Transferir", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Trem cancelado", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Informação sobre %s", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ Cancelado", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "Conexão ameaçada", + "gui.createrailwaysnavigator.route_overview.connection_missed": "Conexão perdida", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Trem cancelado", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Sua jornada à %s não pôde ser continuada.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Devido ao atraso no trem, você perdeu sua ligação ao trem. Busque por uma alternativa no navegador. Perdão pela inconveniência.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Informações sobre %s: Este trem foi cancelado hoje! Perdão pela inconveniência. Busque por uma alternativa no navegador.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "O trem foi cancelado", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Sua jornada à %s não pôde ser continuada. Busque por uma alternativa no navegador.", + "gui.createrailwaysnavigator.route_overview.options": "Pressione %s para opções.", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Sua jornada à %s se inicia!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s para %s, partida %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s para %s, partida %s da plataforma %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Sua plataforma mudou!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Seu trem em %s sai da plataforma %s hoje.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Chegada %s atrasada.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s ao invés de %s em %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Trem cancelado", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s para %s foi cancelado hoje.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "A transferência está vindo", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Alterar para %s → %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Alterar para %s → %s na plataforma %s", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "Sua conexão está em perigo!", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Você provavelmente não poderá chegar a %s para %s.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Conexão perdida", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Você perdeu seu trem de conexão de %s para %s.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Você chegou ao seu destino!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Obrigada por viajar e tenha um ótimo dia", + "gui.createrailwaysnavigator.route_overview.date": "Dia %s, %s", + "gui.createrailwaysnavigator.global_settings.title": "Opções globais", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Clique para editar", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Marcações de estação de trem", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Combine estações de trem individuais em uma estação multi-plataforma com um nome próprio para que o navegador possa sugerir transferências e mais.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Lista negra de estações de trem", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Exclua estações de trem que não devem aparecer nos resultados da navegação. Estas estações serão ignoradas ao gerar rotas.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Grupos de trem", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Crie grupos de trem para organizar todos os trens (p. e.x.: serviços regionais, serviços de longa distância, ...) Os usuários podem decidir quais grupos eles querem usar.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Lista negra de trens", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Exclui trens, p. e.x.: trens de carga, ou especiais, etc., para que eles não sejam usados nas sugestões de rota.", + "gui.createrailwaysnavigator.station_tags.summary": "Contém %s estações de trem", + "gui.createrailwaysnavigator.station_tags.editor": "Última edição por %s em %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Criar entrada", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Excluir marcação", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Remover estação", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Clique para mudar", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Adicionar estação", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nome da estação de trem", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Rótulo de plataforma", + "gui.createrailwaysnavigator.station_tags.enter_name": "Insira o nome aqui", + "gui.createrailwaysnavigator.train_group_settings.title": "Opções de grupo de trens", + "gui.createrailwaysnavigator.train_group_settings.summary": "Contém %s trens", + "gui.createrailwaysnavigator.train_group_settings.editor": "Última edição por %s em %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Excluir grupo", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Remover trem", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Adicionar trem", + "gui.createrailwaysnavigator.blacklist.title": "Lista negra de estações de trem", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Adicionar à lista negra", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Remover da lista negra", + "gui.createrailwaysnavigator.train_blacklist.title": "Lista negra de trens", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Adicionar à lista negra", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Remover da lista negra", + "gui.createrailwaysnavigator.search_settings.title": "Opções de busca", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Tempo mínimo de transferência", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "O tempo mínimo que deve estar disponível para mudar de trem. (1h ~ 50 segundos)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Filtro de categoria de trem", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Decide que trens de quais categorias de trem você queira usar.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "%s categorias selecionadas", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Tudo", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Não", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Redefinir filtro", + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Adicionar", + "gui.createrailwaysnavigator.time": "Tempo: %s", + "gui.createrailwaysnavigator.time.now": "agora", + "gui.createrailwaysnavigator.time_format.dhm": "%s dias %s hrs. %s min.", + "gui.createrailwaysnavigator.time_format.hm": "%s hrs. %s min.", + "gui.createrailwaysnavigator.time_format.m": "%s min.", + "gui.createrailwaysnavigator.platform": "Plataforma", + "gui.createrailwaysnavigator.departure": "Partida", + "gui.createrailwaysnavigator.destination": "Destino", + "gui.createrailwaysnavigator.line": "Linha", + "gui.createrailwaysnavigator.following_trains": "Seguintes trens:", + "gui.createrailwaysnavigator.via": "através", + "gui.createrailwaysnavigator.advanced_display_settings.title": "Opções de exibição avançada", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Tipo de exibição", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Determina as informações exibidas.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Tipo de informação", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Determina o quão detalhadas são as informações.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dois lados", + "enum.createrailwaysnavigator.display_info_type": "Tipo de exibição de informações", + "enum.createrailwaysnavigator.display_info_type.description": "Determina quantas informações devem ser exibidas no seu quadro de exibição.", + "enum.createrailwaysnavigator.display_info_type.simple": "Simples", + "enum.createrailwaysnavigator.display_info_type.info.simple": "A exibição só mostrará informações mais importantes sem detalhes adicionais.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Detalhado", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "As informações mais importantes com alguns detalhes serão exibidos, como a velocidade do trem, escalas, etc.", + "enum.createrailwaysnavigator.display_info_type.informative": "Informativo", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Exibe todas as informações que podem ser interessante e exibe-os numa interface chique. Não recomendado para pequenas exibições porque o texto talvez fique bastante pequeno.", + "enum.createrailwaysnavigator.display_type": "Tipo de exibição", + "enum.createrailwaysnavigator.display_type.description": "O tipo de sua exibição que depende na sua finalidade.", + "enum.createrailwaysnavigator.display_type.train_destination": "Destino do trem", + "enum.createrailwaysnavigator.display_type.info.train_destination": "Pretendido ser usado fora de trens já que ele exibe as informações sobre o trem como o nome, destino e (se selecionado) escalas e outras informações.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Informações do passageiro", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Representa as exibições encontradas nos trens. Estas exibições apareceram na próxima parada, a direção da saída e (se selecionado) a velocidade do trem e uma visão geral da rota.", + "enum.createrailwaysnavigator.display_type.platform": "Exibição da plataforma", + "enum.createrailwaysnavigator.display_type.info.platform": "Estas exibições podem ser usadas em plataforma de estações de trem e exibe os trens seguintes com detalhes adicionais se selecionado. Não pode ser usado em trens!", + "enum.createrailwaysnavigator.side": "Lateral", + "enum.createrailwaysnavigator.side.description": "A lateral do bloco onde a informação será renderizada.", + "enum.createrailwaysnavigator.side.front": "Parte frontal", + "enum.createrailwaysnavigator.side.info.front": "As informações serão renderizadas no lado frontal. Este é o comportamento padrão.", + "enum.createrailwaysnavigator.side.both": "Ambos os lados", + "enum.createrailwaysnavigator.side.info.both": "As informações serão renderizadas nos dois lados.", + "enum.createrailwaysnavigator.time_display": "Exibição de hora", + "enum.createrailwaysnavigator.time_display.description": "Determina como a hora deve ser exibida.", + "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absoluto)", + "enum.createrailwaysnavigator.time_display.eta": "ETA", + "enum.createrailwaysnavigator.time_display.info.eta": "ETA (hora estimada de chegada)", + "create.display_source.advanced_display": "Exibições avançadas", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Largura da coluna do nome do trem", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "em píxeis de blocos. (Padrão: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Largura da coluna da plataforma", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "em píxeis de blocos. (Padrão: automático)", + "gui.createrailwaysnavigator.section_settings.title": "Opções de seção", + "gui.createrailwaysnavigator.section_settings.train_groups": "Grupo de atribuição de trem", + "gui.createrailwaysnavigator.section_settings.train_lines": "Linha de atribuição de trem", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Incluir início da próxima seção", + "gui.createrailwaysnavigator.section_settings.usable": "Navegável", + "gui.createrailwaysnavigator.section_settings.none": "(Nenhum)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Atraso dinâmico", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Duração mínima", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Espera: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "ou no mínimo %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Nova seção de agendamentos", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "O começo de uma nova seção de agendamentos.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Ajustar...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Definir grupo de trem: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Definir linha de trem: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Incluir início da próxima seção: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Navegável: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Redefinir horários", + "display.createrailwaysnavigator.train_destination.simple": "Compacto", + "display.createrailwaysnavigator.train_destination.extended": "Estendido", + "display.createrailwaysnavigator.train_destination.detailed": "Detalhado", + "display.createrailwaysnavigator.passenger_information.running_text": "Texto rolante", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Detalhado com agendamento", + "display.createrailwaysnavigator.platform.running_text": "Texto rolante", + "display.createrailwaysnavigator.platform.table": "Tabela", + "display.createrailwaysnavigator.platform.focus": "Foco", + "gui.createrailwaysnavigator.saved_routes.title": "Rotas salvas", + "gui.createrailwaysnavigator.schedule_board.title": "Quadro de agendamentos", + "gui.createrailwaysnavigator.station_tags.title": "Etiquetas de trens", + "gui.createrailwaysnavigator.journey_info.title": "Informações da jornada", + "gui.createrailwaysnavigator.journey_info.date": "Dia %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) a %s", + "gui.createrailwaysnavigator.color_picker.custom": "Personalizado...", + "gui.createrailwaysnavigator.color_picker.no_color": "Sem cor", + "gui.createrailwaysnavigator.schedule_board.view_details": "Ver detalhes", + "gui.createrailwaysnavigator.schedule_board.train_from": "de %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Partida em", + "gui.createrailwaysnavigator.search_options.train_groups": "Grupos de trem", + "gui.createrailwaysnavigator.search_options.transfer_time": "Tempo de transferência", + "gui.createrailwaysnavigator.search_options.advanced_options": "Opções avançadas", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Todos", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s excluído", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Todas", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s excluído", + "gui.createrailwaysnavigator.empty_list": "A lista está vazia", + "gui.createrailwaysnavigator.new_entry.add": "Adicionar entrada", + "gui.createrailwaysnavigator.new_entry.new": "Nova:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s salva", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "O agendamento mudou!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "O agendamento da rota salva mudou. Verifique o navegador para ver estas mudanças.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s transferências", + "gui.createrailwaysnavigator.route_overview.cancelled": "Cancelada", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Rota salva", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Chegada", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Cancelada", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Atraso de aprox. %s minutos", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Informações sobre %s: Este trem foi cancelado", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Informações sobre %s: Atraso de aprox. %s minutos", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", está cancelado hoje. Perdão pela inconveniência.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", hoje aprox. %s minutos atrasados.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Causa: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Exibir detalhes", + "gui.createrailwaysnavigator.route_widget.save": "Salvar", + "gui.createrailwaysnavigator.route_widget.remove": "Remover", + "gui.createrailwaysnavigator.route_widget.share": "Compartilhar...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "No passado", + "gui.createrailwaysnavigator.saved_routes.today": "Hoje", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Amanhã", + "gui.createrailwaysnavigator.saved_routes.in_days": "Em %s dias", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Exibir detalhes", + "gui.createrailwaysnavigator.saved_route_widget.share": "Compartilhar...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Exibir notificações", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Atraso nas operações", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Atraso na jornada anterior", + "gui.createrailwaysnavigator.train_status.red_signal": "Sinal vermelho", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Prioridade de outro trem", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Atraso de outro trem", + "gui.createrailwaysnavigator.train_status.track_closed": "Trilha fechada", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Falta de funcionários", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Interrupção operacional", + "gui.createrailwaysnavigator.train_status.special_trip": "Viagem especial", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Salvar rota", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Remover rota", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Exibir pop-up", + "gui.createrailwaysnavigator.navigator.my_profile": "Meu perfil", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Algumas rotas podem estar incompletas ou nem sugeridas devido ao Create Railways Navigator não ter iniciado os trens ainda.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Linhas do trem", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Selecionar cor", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Crie linhas de trem para agrupar trens diferentes, substitua o nome de exibição e ajuste outras opções para cada linha. (Só pode ser atribuído aos trens no agendamento)" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json index 928591c4..b7d5075f 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/pt_pt.json @@ -1,59 +1,49 @@ { "advancement.createrailwaysnavigator.navigator": "Obrigado por viajar", - "advancement.createrailwaysnavigator.navigator.description": "Crie um navegador para procurar ligações ferroviárias de uma estação ferroviária para outra.", + "advancement.createrailwaysnavigator.navigator.description": "Crie um navegador para procurar ligações ferroviárias de uma estação ferroviária para outra.", "advancement.createrailwaysnavigator.advanced_display": "Não é bem 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Atualize as suas placas de exibição para exibir mais informações e até mesmo colocá-las nos seus comboios.", - - "itemGroup.createrailwaysnavigator.tab": "Criar Navegador Ferroviário", - "item.createrailwaysnavigator.navigator": "Criar Navegador Ferroviário", - "item.createrailwaysnavigator.navigator.tooltip.summary": "O _navegador_ mostra possíveis _ligações de comboios_ com informações adicionais, como _escalas_, data_ em tempo _real e muito mais.", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "O _navegador_ mostra possíveis _ligações de comboios_ com informações adicionais, como _escalas_, data_ em tempo _real e muito mais.", "block.createrailwaysnavigator.train_station_clock": "Relógio da Estação de Comboios", "block.createrailwaysnavigator.advanced_display_block": "Bloco de exibição avançada", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", "block.createrailwaysnavigator.advanced_display": "Placa de exibição avançada", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", "block.createrailwaysnavigator.advanced_display_small": "Ecrã de exibição pequena avançada", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", "block.createrailwaysnavigator.advanced_display_panel": "Painel de exibição avançada", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Painel de exibição meio avançado", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", "block.createrailwaysnavigator.advanced_display_sloped": "Ecrã avançado inclinado", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Use-o em _comboios_, como um ecrã_ de destino _comboio ou _passageiro informações ecrã_, ou em _estações de comboios_ como _ecrãs de plataformas_ melhorado que também mostra mais informações do que placas de exibição regulares.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "Quando lado direito do rato é clicado usando uma chave inglesa", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Abra um menu para _configurar_ o _ecrã_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Fora de Serviço!", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Viagem sem passageiros", - "category.createrailwaysnavigator.crn": "Criar Navegador Ferroviário", "key.createrailwaysnavigator.route_overlay_options": "Mostrar opções de sobreposição de rota", - "enum.createrailwaysnavigator.overlay_position": "Posição do ecrã", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Canto superior esquerdo", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Canto superior direito", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Canto inferior esquerdo", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Canto inferior direito", - "gui.createrailwaysnavigator.loading.title": "Baixando dados do servidor...", - "gui.createrailwaysnavigator.overlay_settings.title": "Configurações de sobreposição de rota", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Mostrar detalhes", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Remover sobreposicao de rota", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Anuncios do Narrador ativados.", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Anuncios do Narrador desativados.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Anuncios do Narrador desativados.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Notificações ativadas", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Notificações desativadas", "gui.createrailwaysnavigator.route_overlay_settings.scale": "Escala GUI", @@ -61,7 +51,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "O Narrador anuncia eventos importantes em sua jornada, por exemplo, a proxima parada, mudancas, etc.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Notificações", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Receba notificacoes do sistema sobre eventos importantes em sua jornada, por exemplo, a proxima parada, alteracoes, etc.", - "gui.createrailwaysnavigator.common.expand": "Mostrar detalhes", "gui.createrailwaysnavigator.common.collapse": "Ocultar detalhes", "gui.createrailwaysnavigator.common.go_back": "Voltar", @@ -73,8 +62,6 @@ "gui.createrailwaysnavigator.common.search": "Procurar", "gui.createrailwaysnavigator.common.auto": "Automático", "gui.createrailwaysnavigator.common.server_error": "Erro do servidor durante a execucao da tarefa. Procure detalhes no console.", - - "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "Nenhuma conexao encontrada.", "gui.createrailwaysnavigator.navigator.not_searched": "Nada pesquisado ainda.", "gui.createrailwaysnavigator.navigator.searching": "Procurar conexões ...", @@ -82,20 +69,16 @@ "gui.createrailwaysnavigator.navigator.start_end_null": "O início ou o destino estao vazios.", "gui.createrailwaysnavigator.navigator.start_end_equal": "Início e destino sao iguais.", "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Conexão no passado", - "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Trans.", "gui.createrailwaysnavigator.navigator.route_entry.station_start": "de %s", "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Configurações globais", "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Configurações de pesquisa", "gui.createrailwaysnavigator.navigator.search.tooltip": "Procurar", "gui.createrailwaysnavigator.navigator.location.tooltip": "Estacao mais proxima da posicao atual", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Alternar campos", - "gui.createrailwaysnavigator.route_details.title": "Detalhes da Rota", "gui.createrailwaysnavigator.route_details.departure": "Partida em", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Transferência em", "gui.createrailwaysnavigator.route_details.transfer": "Transferência", - "gui.createrailwaysnavigator.route_details.save_route": "Salvar conexão", - "gui.createrailwaysnavigator.route_overview.title": "Detalhes da Rota", "gui.createrailwaysnavigator.route_overview.journey_begins": "A sua jornada começa aqui! %s a %s, partida %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "A sua jornada começa aqui! %s a %s, partida %s na plataforma %s", @@ -125,7 +108,7 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Sua plataforma mudou!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "O seu comboio em %s sai da plataforma %s hoje.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Chegada %s atrasada.", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s em vez de %s em %s", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s em vez de %s em %s", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Comboio cancelado", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s a %s foi cancelado hoje.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Transferência esta chegando", @@ -138,7 +121,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Chegou ao seu destino!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Obrigado por viajar e tenha um bom dia", "gui.createrailwaysnavigator.route_overview.date": "Dia %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Configurações globais", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Clique para editar", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Tags da estaçao de comboio", @@ -149,8 +131,6 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Criar grupos de comboios para organizar todos os comboios (por exemplo, servicos regionais, servicos de longa distancia, ... ). Os usuarios podem decidir quais grupos desejam usar.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Lista negra de comboios", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Excluir comboios, por exemplo, comboios de mercadorias, comboios especiais, etc., para que nao sejam utilizados nas sugestoes de rota.", - - "gui.createrailwaysnavigator.station_tags.title": "Configuraçþes de tags da estação de comboios", "gui.createrailwaysnavigator.station_tags.summary": "ContĂŠm %s Track Stations", "gui.createrailwaysnavigator.station_tags.editor": "Última edição por %s em %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Criar nova entrada", @@ -160,22 +140,17 @@ "gui.createrailwaysnavigator.station_tags.hint.station_name": "Nome de estação de trilha", "gui.createrailwaysnavigator.station_tags.hint.platform": "Plataforma", "gui.createrailwaysnavigator.station_tags.enter_name": "Digite o nome aqui", - "gui.createrailwaysnavigator.train_group_settings.title": "Configuraçþes do grupo de Comboio", "gui.createrailwaysnavigator.train_group_settings.summary": "ContĂŠm %s Comboios", "gui.createrailwaysnavigator.train_group_settings.editor": "Ultima edição por %s em %s", - "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Delete Group", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Remover comboio", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Adicionar Comboio", - "gui.createrailwaysnavigator.blacklist.title": "Lista negra da estacao de comboios", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Adicionar a lista negra", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Remover da lista negra", - "gui.createrailwaysnavigator.train_blacklist.title": "Lista negra de comboios", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Adicionar a lista negra", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Remover da lista negra", - "gui.createrailwaysnavigator.search_settings.title": "Configuraçþes de pesquisa", "gui.createrailwaysnavigator.search_settings.transfer_time": "Tempo minimo de transferĂŞncia", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "O tempo minimo que deve estar disponivel para mudar de comboio. (1h ~ 50 segundos da vida real)", @@ -185,29 +160,22 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Todo", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "NĂŁo", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Redefinir filtro", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Adicionar", - "gui.createrailwaysnavigator.time": "Tempo: %s", "gui.createrailwaysnavigator.time.now": "agora", "gui.createrailwaysnavigator.time_format.dhm": "%s dias %s hrs. %s min.", - "gui.createrailwaysnavigator.time_format.hm": "%s hrs. %s min.", - "gui.createrailwaysnavigator.time_format.m": "%s min.", - "gui.createrailwaysnavigator.platform": "Plataforma", "gui.createrailwaysnavigator.departure": "Partida", "gui.createrailwaysnavigator.destination": "Destino", "gui.createrailwaysnavigator.line": "Linha", "gui.createrailwaysnavigator.following_trains": "Seguintes Comboios:", "gui.createrailwaysnavigator.via": "Via", - "gui.createrailwaysnavigator.advanced_display_settings.title": "Configuraçþes avançadas de exibição", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Tipo de exibição", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Determina as informaçþes exibidas.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Tipo de informação", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Determina o quĂŁo detalhadas sao as informaçþes.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dupla face", - "enum.createrailwaysnavigator.display_info_type": "Tipo de Informaçþes de Exibição", "enum.createrailwaysnavigator.display_info_type.description": "Determina a quantidade de informaçþes que devem ser exibidas no quadro de exibição.", "enum.createrailwaysnavigator.display_info_type.simple": "Simples", @@ -216,7 +184,6 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "As informaçþes mais importantes com alguns detalhes serĂŁo mostradas, como velocidade do comboio, escalas, etc.", "enum.createrailwaysnavigator.display_info_type.informative": "Informativo", "enum.createrailwaysnavigator.display_info_type.info.informative": "Mostra todas as informacoes que poderiam ser interessantes e as exibe em um layout sofisticado. Nao recomendado para telas pequenas, pois o texto pode ficar muito pequeno.", - "enum.createrailwaysnavigator.display_type": "Tipo de exibição", "enum.createrailwaysnavigator.display_type.description": "O tipo de exibicao que depende de sua finalidade.", "enum.createrailwaysnavigator.display_type.train_destination": "Destino do Comboio", @@ -225,26 +192,20 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Representa as telas encontradas dentro dos comboios. Essas exibiçþes mostrarao a proxima estação, a direção de saĂ­da e (se selecionada) a velocidade do comboio e uma visĂŁo geral da rota.", "enum.createrailwaysnavigator.display_type.platform": "Exibição da plataforma", "enum.createrailwaysnavigator.display_type.info.platform": "Esses displays devem ser usados nas plataformas das estaçþes de trem e mostram os prĂłximos comboios com detalhes adicionais, se selecionados. NĂŁo pode ser usado em Comboios!", - "enum.createrailwaysnavigator.side": "Lado", "enum.createrailwaysnavigator.side.description": "O lado do bloco onde as informaçþes devem ser renderizadas.", "enum.createrailwaysnavigator.side.front": "Parte Frontal", "enum.createrailwaysnavigator.side.info.front": "As informaçþes serĂŁo renderizadas apenas na parte frontal. Esse ĂŠ o comportamento padrĂŁo", "enum.createrailwaysnavigator.side.both": "Ambos os lados", "enum.createrailwaysnavigator.side.info.both": "As informaçþes serao prestadas em ambos os lados.", - "enum.createrailwaysnavigator.time_display": "Exibição de tempo", "enum.createrailwaysnavigator.time_display.description": "Determina como a hora deve ser exibida.", - "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (absoluto)", - "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (hora prevista de chegada)", - "create.display_source.advanced_display": "Monitores Avancados", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Largura da coluna do nome do comboio", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "em pixeis de bloco. (PadrĂŁo: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Largura da coluna da plataforma", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "em pĂ­xeis de bloco (PadrĂŁo: Auto)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.station_tags.title": "Configuraçþes de tags da estação de comboios" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json index 0a2d7a74..0f2178a8 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/ru_ru.json @@ -1,53 +1,46 @@ { "advancement.createrailwaysnavigator.navigator": "ХпасийО Са пОоСдку!", - "advancement.createrailwaysnavigator.navigator.description": "Создайте навигатор для пОиска маршрутов следования пОоСдОв.", + "advancement.createrailwaysnavigator.navigator.description": "Создайте навигатор для пОиска маршрутов следования пОоСдОв.", "advancement.createrailwaysnavigator.advanced_display": "Не сОвсоП 4k...", "advancement.createrailwaysnavigator.advanced_display.description": "Обновите свОи механические табло, чтобы на них отображалось больше информации, и даМо разместите их в своих поездах.", - - "itemGroup.createrailwaysnavigator.tab": "Create: Навигатор железных дорог", - "item.createrailwaysnavigator.navigator": "Железнодорожный навигатор", - "item.createrailwaysnavigator.navigator.tooltip.summary": "_Навигатор_ показывает возможные _пересадки Đş пОоСдаП_ и дополнительную информацию, такую как _остановки_, _данные в реальном времени_ и ПнОгОо другое.", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "_Навигатор_ показывает возможные _пересадки Đş пОоСдаП_ и дополнительную информацию, такую как _остановки_, _данные в реальном времени_ и ПнОгОо другое.", "block.createrailwaysnavigator.train_station_clock": "Станционные часы", "block.createrailwaysnavigator.advanced_display_block": "Блок улучшенного механического табло", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "ПКМ с гаечным ключом", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", "block.createrailwaysnavigator.advanced_display": "Улучшенное механическое табло", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "ПКМ с гаечным ключом", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", "block.createrailwaysnavigator.advanced_display_small": "Малое улучшенное механическое табло", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "ПКМ с гаечным ключом", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", "block.createrailwaysnavigator.advanced_display_panel": "Панель улучшенного механического табло", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "ПКМ с гаечным ключом", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", "block.createrailwaysnavigator.advanced_display_half_panel": "Полупанель улучшенного механического табло", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "ПКМ с гаечным ключом", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", "block.createrailwaysnavigator.advanced_display_sloped": "Наклонное улучшенное механическое табло", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Используйте в _поездах_ в качестве _табло пункта назначения пОоСда_ иНи _табло информации для пассажиров_, Đ° на станциях - в качестве _дисплея платформы,_ которые также отображают больше информации, чем обычные табло.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "ПКМ с гаечным ключом", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Открывает меню для _настройки дисплея._", + "block.createrailwaysnavigator.advanced_display_slab": "Расширенная Информационная Плита", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "При нажатии правой кнОпкОК мыши с использованием гаечного ключа", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Вышел иС строя!", - "category.createrailwaysnavigator.crn": "Create: Навигатор железных дорог", "key.createrailwaysnavigator.route_overlay_options": "Показать параметры наложения маршрута", - "enum.createrailwaysnavigator.overlay_position": "Позиция наложения", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Левый верхний угОН", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Правый верхний угОН", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Левый ниМниК угОН", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Правый ниМниК угОН", - "gui.createrailwaysnavigator.loading.title": "Загрузка данных с сервера...", - "gui.createrailwaysnavigator.overlay_settings.title": "Настройки наложения маршрута", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Показать детали", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Удалить наНОМонио маршрута", @@ -60,7 +53,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Диктор объявляет Đž важных событиях в вашоП путешествии, например, Đž следующей остановке, пересадках и т.Đ´.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Уведомления", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Получайте мгновенные уведомления Đž важных событиях в вашоП путешествии, например, Đž следующей остановке, пересадках и т.Đ´.", - "gui.createrailwaysnavigator.common.expand": "Показать детали", "gui.createrailwaysnavigator.common.collapse": "Скрыть детали", "gui.createrailwaysnavigator.common.go_back": "Вернуться", @@ -72,7 +64,9 @@ "gui.createrailwaysnavigator.common.search": "Поиск", "gui.createrailwaysnavigator.common.auto": "Авто", "gui.createrailwaysnavigator.common.server_error": "Ошибка сервера при выполнении задачи. Подробности смотрите в кОнсОНи.", - + "gui.createrailwaysnavigator.common.delete": "Удалить", + "gui.createrailwaysnavigator.common.add": "Добавить", + "gui.createrailwaysnavigator.common.help": "Получить помощь", "gui.createrailwaysnavigator.navigator.title": "Железнодорожный навигатор", "gui.createrailwaysnavigator.navigator.no_connections": "Никаких маршрутов но наКдонО.", "gui.createrailwaysnavigator.navigator.not_searched": "Пока ничего но наКдонО.", @@ -87,14 +81,12 @@ "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Поиск настроек", "gui.createrailwaysnavigator.navigator.search.tooltip": "Поиск", "gui.createrailwaysnavigator.navigator.location.tooltip": "Ближайшая станция", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Обновить", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Переключить поля", - "gui.createrailwaysnavigator.route_details.title": "Детали маршрута", "gui.createrailwaysnavigator.route_details.departure": "Отправление через", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Пересадка на", "gui.createrailwaysnavigator.route_details.transfer": "Пересадка", - "gui.createrailwaysnavigator.route_details.save_route": "Сохранить сОодинонио", - "gui.createrailwaysnavigator.route_overview.title": "Детали маршрута", "gui.createrailwaysnavigator.route_overview.journey_begins": "Ваше пОоСдка начинается! %s в сторону %s, отправление в %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Ваша пОоСдка начинается! %s в сторону %s, отправление в %s на платформе %s", @@ -124,7 +116,7 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Ваша платформа изменилась!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ваш пОоСд в %s отходит от платформы %s сегодня.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: прибытие %s задержка.", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s вместо %s на %s", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s вместо %s на %s", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Поезд отменён", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s в сторону %s отменён сегодня.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Пересадка скоро", @@ -137,7 +129,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Вы достигли места назначения!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "ХпасийО Са пОоСдку", "gui.createrailwaysnavigator.route_overview.date": "День %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Глобальные настройки", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Нажмите, чтобы изменить", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Тоги станции", @@ -148,33 +139,28 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Проведите классификацию пОоСдОв (например, региональные, междугородние и т.Đ´.). Игроки могут саПи выбирать группы.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Чёрный списОк пОоСдОв", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Исключите пОоСда, например, грузовые, специальные и т.Đ´., чтобы Они но использовались в предложениях пО маршруту.", - - "gui.createrailwaysnavigator.station_tags.title": "Настройки тегов станций", "gui.createrailwaysnavigator.station_tags.summary": "Количество станций: %s", "gui.createrailwaysnavigator.station_tags.editor": "Изменён %s в %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Создать новую запись", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Удалить тег", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Удалить станцию", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Нажмите, чтобы изменить", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Добавить станцию", "gui.createrailwaysnavigator.station_tags.hint.station_name": "Имя станции", "gui.createrailwaysnavigator.station_tags.hint.platform": "Платформа", "gui.createrailwaysnavigator.station_tags.enter_name": "Ввести имя здесь", - "gui.createrailwaysnavigator.train_group_settings.title": "Найстройки групп пОоСдОв", "gui.createrailwaysnavigator.train_group_settings.summary": "Включает %s TпОоСдОв", "gui.createrailwaysnavigator.train_group_settings.editor": "ИСПононО %s в %s", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Удалить группу", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Удалить пОоСд", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Добавить пОоСд", - "gui.createrailwaysnavigator.blacklist.title": "Чёрный списОк станций", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Добавить в ЧХ", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Удалить иС ЧХ", - "gui.createrailwaysnavigator.train_blacklist.title": "Чёрный списОк пОоСдОв", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Добавить в ЧХ", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Удалить иС ЧХ", - "gui.createrailwaysnavigator.search_settings.title": "Поиск настроек", "gui.createrailwaysnavigator.search_settings.transfer_time": "Минимальное время пересадки", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Минимальное время для пересадки на пОоСд. (1h ~ 50 реальных сокунд)", @@ -184,29 +170,24 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Выбраны всо категории", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Ни Одна иС категорий но выбрана", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Сбросить фильтры", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Добавить", - "gui.createrailwaysnavigator.time": "Время: %s", "gui.createrailwaysnavigator.time.now": "0 сок", "gui.createrailwaysnavigator.time_format.dhm": "%s дноК %s часов %s Пин.", "gui.createrailwaysnavigator.time_format.hm": "%s часов. %s Пин.", "gui.createrailwaysnavigator.time_format.m": "%s Пин.", - "gui.createrailwaysnavigator.platform": "Платформа", "gui.createrailwaysnavigator.departure": "Отправление", "gui.createrailwaysnavigator.destination": "Пункт назначения", "gui.createrailwaysnavigator.line": "Линия", "gui.createrailwaysnavigator.following_trains": "Следующие пОоСда:", "gui.createrailwaysnavigator.via": "Đş станции", - "gui.createrailwaysnavigator.advanced_display_settings.title": "Настройки улучшенного дисплея", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Тип дисплея", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Определяет отображаемую информацию.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Тип информации", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Определяет, насколько подробной является информация.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Двухсторонний", - "enum.createrailwaysnavigator.display_info_type": "Тип информации дисплея", "enum.createrailwaysnavigator.display_info_type.description": "Определяет, какОК объем информации дОНМон отображаться на вашоП диспНоо.", "enum.createrailwaysnavigator.display_info_type.simple": "Простой", @@ -215,7 +196,6 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "Будет пОкаСана наийОНоо важная информация с некоторыми подробностями, такими как скорость пОоСда, остановки в пути и т.Đ´.", "enum.createrailwaysnavigator.display_info_type.informative": "Информативный", "enum.createrailwaysnavigator.display_info_type.info.informative": "Отображает всю информацию, которая может быть интересна, и представляет оо в необычном формате. Не рекомендуется для небольших диспНоов, так как текст может стать очень ПоНкиП.", - "enum.createrailwaysnavigator.display_type": "Тип дисплея", "enum.createrailwaysnavigator.display_type.description": "Тип вашогО дисплея зависит от огО назначения.", "enum.createrailwaysnavigator.display_type.train_destination": "ТайНО пункта назначения пОоСда", @@ -224,28 +204,47 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Отображает диспНои, расположенные внутри пОоСдОв. На этих дисплеях будут отображаться следующая остановка, направление выхода и (осНи выбрано) скорость движения пОоСда, Đ° также обзор маршрута.", "enum.createrailwaysnavigator.display_type.platform": "Дисплей платформы", "enum.createrailwaysnavigator.display_type.info.platform": "Эти диспНои следует использовать на платформах железнодорожных вОкСаНОв, и на них отображаются следующие пОоСда с дополнительной информацией, осНи Они выбраны. Нельзя использовать в поездах!", - "enum.createrailwaysnavigator.side": "Сторона", "enum.createrailwaysnavigator.side.description": "Сторона йНОка, на которой дОНМна отображаться информация.", "enum.createrailwaysnavigator.side.front": "Лицевая сторона", "enum.createrailwaysnavigator.side.info.front": "Информация будет отображаться только на лицевой стороне. Это пОводонио пО умолчанию.", "enum.createrailwaysnavigator.side.both": "Обе стороны", "enum.createrailwaysnavigator.side.info.both": "Информация будет отображаться с обеих сторон.", - "enum.createrailwaysnavigator.time_display": "Отображение времени", "enum.createrailwaysnavigator.time_display.description": "Определяет, как дОНМнО отображаться время.", "enum.createrailwaysnavigator.time_display.abs": "АБС", "enum.createrailwaysnavigator.time_display.info.abs": "АБС (Абсолютное)", "enum.createrailwaysnavigator.time_display.eta": "ПВП", "enum.createrailwaysnavigator.time_display.info.eta": "ПВП (Предполагаемое время прибытия)", - "create.display_source.advanced_display": "Улучшенные диспНои", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Ширина столбца с наСваниоП пОоСда", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "в пикселях йНОка. (По умолчанию: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Ширина колонны платформы", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "в пикселях йНОка. (По умолчанию: Авто)", - - "createrailwaysnavigator.moin": "moin", - - "createrailwaysnavigator.credits": "Над переводом работал VGamerGroup" -} \ No newline at end of file + "gui.createrailwaysnavigator.section_settings.title": "Настройки раздела", + "gui.createrailwaysnavigator.section_settings.train_groups": "Назначить группу пОоСдОв", + "gui.createrailwaysnavigator.section_settings.train_lines": "Назначить линию пОоСда", + "gui.createrailwaysnavigator.section_settings.usable": "Проходимый", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Динамическая задержка", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Минимальная продолжительность", + "createrailwaysnavigator.schedule.instruction.travel_section": "Новый раздел расписания", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Начало нОвОгО раздела расписания.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Настроить...", + "display.createrailwaysnavigator.train_destination.simple": "Компактно", + "display.createrailwaysnavigator.train_destination.extended": "Расширенно", + "display.createrailwaysnavigator.train_destination.detailed": "Детально", + "gui.createrailwaysnavigator.station_tags.title": "Настройки тегов станций", + "gui.createrailwaysnavigator.empty_list": "ХписОк пуст", + "gui.createrailwaysnavigator.saved_routes.saved": "%s Сохранён", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "Расписанио сохранённого маршрута изменилось. Пожалуйста, проверьте навигатор для этих иСПонониК.", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Сохранённый маршрут", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Прибытие", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Задержка приблизительно %s минут", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Завтра", + "gui.createrailwaysnavigator.saved_route_widget.share": "Поделиться...", + "gui.createrailwaysnavigator.train_status.track_closed": "Путь закрыт", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Сохранить маршрут", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Удалить маршрут", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Показать всплывающее ОкнО", + "gui.createrailwaysnavigator.navigator.my_profile": "Мой профиль" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json index 57b9c4cf..7081ffa3 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/sv_se.json @@ -1,59 +1,53 @@ { "advancement.createrailwaysnavigator.navigator": "Tack fĂśr att du reser med oss!", - "advancement.createrailwaysnavigator.navigator.description": "Tillverka en Navigator fĂśr att sĂśka efter tĂĽgfĂśrbildelser frĂĽn en station till en annan.", + "advancement.createrailwaysnavigator.navigator.description": "Tillverka en Navigator fĂśr att sĂśka efter tĂĽgfĂśrbildelser frĂĽn en station till en annan.", "advancement.createrailwaysnavigator.advanced_display": "Inte riktigt 4k", "advancement.createrailwaysnavigator.advanced_display.description": "Upgradera dina displaytavlor till att visa mer information som även funkar i tĂĽg.", - - "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", - - "item.createrailwaysnavigator.navigator": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator.tooltip.summary": "The _navigator_ shows possible _train connections_ with additional information such as _stopovers_, _real-time data_ and more.", - + "item.createrailwaysnavigator.navigator": "Create Järnvägsnavigerare", + "item.createrailwaysnavigator.navigator.tooltip.summary": "_Navigatorn_ visar mĂśjliga _tĂĽgbyten_ med extra information, till exempel _mellanlandningar_, _data i realtid_ med mera.", "block.createrailwaysnavigator.train_station_clock": "TĂĽstationsklocka", "block.createrailwaysnavigator.advanced_display_block": "Avancerat Displayblock", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "När R-Klickats med en skiftnyckel", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", "block.createrailwaysnavigator.advanced_display": "Avancerad Displaytavla", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "När R-Klickats med en skiftnyckel", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", "block.createrailwaysnavigator.advanced_display_small": "Liten Avancerad Skärm", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "När R-Klickats med en skiftnyckel", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", "block.createrailwaysnavigator.advanced_display_panel": "Avancerad Skärmpanel", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "När R-Klickats med en skiftnyckel", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Halv Avancerad Skärmpanel", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "När R-Klickats med en skiftnyckel", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", "block.createrailwaysnavigator.advanced_display_sloped": "Lutande Avancerad Skärm", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "När R-Klickats med en skiftnyckel", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", + "block.createrailwaysnavigator.advanced_display_slab": "Avancerad visningsplatta", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Använd den pĂĽ _tĂĽg_, som en _tĂĽgdestinationsdisplay_ eller _passagerarinformationsdisplay_, eller pĂĽ _tĂĽgstationer_ som fĂśrbättrade _plattformsdisplayer_ som ocksĂĽ visar mer information än vanliga displaytavlor.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "När R-Klickats med en skiftnyckel", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Öppna en meny fĂśr att _konfigurera_ _displayen_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Ej i trafik", - "block.createrailwaysnavigator.advanced_display.ber.shunting_trip": "Abonnerad", - - "category.createrailwaysnavigator.crn": "Create Railways Navigator", - "key.createrailwaysnavigator.route_overlay_options": "Visa Alternativ FĂśr RuttĂśverlagring", - + "category.createrailwaysnavigator.crn": "Create Järnvägsnavigerare", + "key.createrailwaysnavigator.route_overlay_options": "Visa Alternativ FĂśr RuttĂśverlägg", "enum.createrailwaysnavigator.overlay_position": "Visa Position", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Övre Vänstra HĂśrnet", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Övre HĂśgra HĂśrnet", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Nedre Vänstra HĂśrnet", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Nedre HĂśgra HĂśrnet", - - "gui.createrailwaysnavigator.loading.title": "Downloading data from server...", - + "gui.createrailwaysnavigator.loading.title": "Hämtar data frĂĽn server...", "gui.createrailwaysnavigator.overlay_settings.title": "Inställningar fĂśr ruttĂśverlagring", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Visa detaljer", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Ta bort ruttĂśverlagring", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Berättarmeddelanden har aktiverats.", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Berättarmeddelanden har inaktiverats.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Berättarmeddelanden har inaktiverats.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Aviseringar har aktiverats", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Aviseringar har inaktiverats", "gui.createrailwaysnavigator.route_overlay_settings.scale": "GUI Storlek", @@ -61,7 +55,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Berättaren tillkännager viktiga händelser pĂĽ din resa, t.ex. nästa stopp, fĂśrändringar osv.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Aviseringar", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "FĂĽ toast-aviseringar om viktiga händelser pĂĽ din resa, t.ex. nästa stopp, fĂśrändringar osv.", - "gui.createrailwaysnavigator.common.expand": "Visa Detaljer", "gui.createrailwaysnavigator.common.collapse": "DĂślj Detaljer", "gui.createrailwaysnavigator.common.go_back": "GĂĽ Tillbaka", @@ -73,8 +66,10 @@ "gui.createrailwaysnavigator.common.search": "SĂśk", "gui.createrailwaysnavigator.common.auto": "Auto", "gui.createrailwaysnavigator.common.server_error": "Serverfel när uppgiften kĂśrs. Se konsolen fĂśr detaljer.", - - "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", + "gui.createrailwaysnavigator.common.delete": "Radera", + "gui.createrailwaysnavigator.common.add": "Lägg till", + "gui.createrailwaysnavigator.common.help": "FĂĽ hjälp", + "gui.createrailwaysnavigator.navigator.title": "Create Järnvägsnavigerare", "gui.createrailwaysnavigator.navigator.no_connections": "Inga anslutningar hittades.", "gui.createrailwaysnavigator.navigator.not_searched": "Inget sĂśkt ännu.", "gui.createrailwaysnavigator.navigator.searching": "SĂśk efter anslutningar...", @@ -82,20 +77,18 @@ "gui.createrailwaysnavigator.navigator.start_end_null": "Start eller destination är tom.", "gui.createrailwaysnavigator.navigator.start_end_equal": "Du har angett samma start och destination.", "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ Anslutning i det fĂśrflutna", - "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Trans.", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Byte", "gui.createrailwaysnavigator.navigator.route_entry.station_start": "frĂĽn %s", "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Globala Inställningar", "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "SĂśkinställningar", "gui.createrailwaysnavigator.navigator.search.tooltip": "SĂśk", - "gui.createrailwaysnavigator.navigator.location.tooltip": "Närmaste station frĂĽn nuvarande position", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Närmsta station frĂĽn nuvarande position", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Ladda om", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Växla fält", - "gui.createrailwaysnavigator.route_details.title": "Ruttdetaljer", "gui.createrailwaysnavigator.route_details.departure": "AvgĂĽr om", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Vidarekoppling om", "gui.createrailwaysnavigator.route_details.transfer": "Byte", - "gui.createrailwaysnavigator.route_details.save_route": "Spara Anslutning", - "gui.createrailwaysnavigator.route_overview.title": "Ruttdetaljer", "gui.createrailwaysnavigator.route_overview.journey_begins": "Din resa bĂśrjar! %s mot %s, avgĂĽr %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Din resa bĂśrjar! %s mot %s, avgĂĽr %s frĂĽn plattform %s", @@ -125,7 +118,7 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Din anslutning har ändrat plattform!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ditt tĂĽg i %s gĂĽr frĂĽn perrong %s idag.", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Ankomst %s fĂśrsenad.", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s istället fĂśr %s i %s", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s istället fĂśr %s i %s", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: AvgĂĽng inställd", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s mot %s är tyvärr inställd idag.", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Din anslutning är pĂĽ väg", @@ -138,44 +131,38 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Du är framme vid din destination!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Tack fĂśr att du reser med oss och ha en fortsatt trevlig dag!", "gui.createrailwaysnavigator.route_overview.date": "Dag %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Globala Inställningar", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Klicka fĂśr att ändra", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Stationstaggar", - "gui.createrailwaysnavigator.global_settings.option_alias.description": "Definiera stationstaggar fĂśr att definera multiplattformsstationer (t.ex. MinStation 1, MinStation 2, ...) som en enda station (t.ex. MinStation) med anpassade namn.", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "SlĂĽ samman enstaka tĂĽgstationer till en multiplattformsstation med sitt egna namn sĂĽ att navigatorn kan fĂśreslĂĽ byten, m.m.", "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Stationssvartlista", "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Uteslut stationer som inte ska visas i navigeringsresultaten. Dessa stationer kommer att ignoreras när rutter genereras.", "gui.createrailwaysnavigator.global_settings.train_group.title": "TĂĽggrupper", "gui.createrailwaysnavigator.global_settings.train_group.description": "Skapa tĂĽggrupper fĂśr att organisera alla tĂĽg (t.ex. regionaltrafik, fjärrtrafik, ...). Användare kan bestämma vilka grupper de vill använda.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "TĂĽgsvartlista", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Uteslut tĂĽg, t.ex. godstĂĽg, specialtĂĽg etc. sĂĽ att de inte används i ruttfĂśrslagen.", - - "gui.createrailwaysnavigator.station_tags.title": "Inställningar FĂśr Stationstaggar", "gui.createrailwaysnavigator.station_tags.summary": "InnehĂĽller %s stationer", "gui.createrailwaysnavigator.station_tags.editor": "Senast redigerad av %s den %s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Skapa ny Post", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Radera Tag", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Radera Station", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Klicka fĂśr att ändra", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Lägg till Station", "gui.createrailwaysnavigator.station_tags.hint.station_name": "Stationsnamn", "gui.createrailwaysnavigator.station_tags.hint.platform": "Plattform", "gui.createrailwaysnavigator.station_tags.enter_name": "Ange namn här", - "gui.createrailwaysnavigator.train_group_settings.title": "TĂĽggruppinställningar", "gui.createrailwaysnavigator.train_group_settings.summary": "InnehĂĽller %s tĂĽg", "gui.createrailwaysnavigator.train_group_settings.editor": "Senast redigerad av %s den %s", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Radera Grupp", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Radera TĂĽg", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Lägg till TĂĽg", - "gui.createrailwaysnavigator.blacklist.title": "Stationssvartlista", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Lägg till pĂĽ svartlistan", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Ta bort frĂĽn svartlistan", - "gui.createrailwaysnavigator.train_blacklist.title": "TĂĽgsvartlista", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Lägg till pĂĽ svartlistan", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Ta bort frĂĽn svartlistan", - "gui.createrailwaysnavigator.search_settings.title": "SĂśkinställningar", "gui.createrailwaysnavigator.search_settings.transfer_time": "Minsta ĂśverfĂśringstid", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Den minsta tid som bĂśr finnas tillgänglig fĂśr att byta tĂĽg. (1h ~ 50 Sekunder i Verkliga Livet)", @@ -185,29 +172,24 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Alla", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Inga", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Återställ filter", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Lägg till", - "gui.createrailwaysnavigator.time": "Tid: %s", "gui.createrailwaysnavigator.time.now": "Nu", "gui.createrailwaysnavigator.time_format.dhm": "%s dgr %s tim. %s min.", "gui.createrailwaysnavigator.time_format.hm": "%s tim. %s min.", "gui.createrailwaysnavigator.time_format.m": "%s min.", - "gui.createrailwaysnavigator.platform": "Plattform", - "gui.createrailwaysnavigator.departure": "Avgpng", + "gui.createrailwaysnavigator.departure": "AvgĂĽng", "gui.createrailwaysnavigator.destination": "Destination", "gui.createrailwaysnavigator.line": "Linje", "gui.createrailwaysnavigator.following_trains": "FĂśljande anslutningar:", "gui.createrailwaysnavigator.via": "via", - "gui.createrailwaysnavigator.advanced_display_settings.title": "Avancerade Skärminställningar", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Skärmtyp", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "AvgĂśr vilken information som visas.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informationstyp", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "AvgĂśr hur detaljerad informationen är.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Dubbelsidig", - "enum.createrailwaysnavigator.display_info_type": "Visa Infotyp", "enum.createrailwaysnavigator.display_info_type.description": "Bestämmer hur mycket information som ska visas pĂĽ skärmen.", "enum.createrailwaysnavigator.display_info_type.simple": "Enkel", @@ -216,7 +198,6 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "Den viktigaste informationen med vissa detaljer kommer att visas, sĂĽsom tĂĽghastighet, mellanstationer, osv.", "enum.createrailwaysnavigator.display_info_type.informative": "Informativ", "enum.createrailwaysnavigator.display_info_type.info.informative": "Visar all information som kan vara intressant och visar den i en snygg layout. Rekommenderas inte fĂśr smĂĽ skärmar eftersom texten kan bli väldigt liten.", - "enum.createrailwaysnavigator.display_type": "Bildskärmstyp", "enum.createrailwaysnavigator.display_type.description": "Typen av din skärm som beror pĂĽ deras syfte.", "enum.createrailwaysnavigator.display_type.train_destination": "TĂĽgdestination", @@ -225,26 +206,110 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Representerar skärmarna som finns inuti tĂĽgen. Dessa displayer visar nästa stopp, utfartsriktningen och (om valt) tĂĽghastighet och en ruttĂśversikt.", "enum.createrailwaysnavigator.display_type.platform": "Plattformsdisplay", "enum.createrailwaysnavigator.display_type.info.platform": "Dessa displayer bĂśr användas pĂĽ plattformar och perronger och visar nästa ankommande tĂĽg med ytterligare detaljer om de väljs. Kan inte användas i tĂĽg!", - "enum.createrailwaysnavigator.side": "Sida", "enum.createrailwaysnavigator.side.description": "Den sida av blocket där informationen ska ĂĽterges.", "enum.createrailwaysnavigator.side.front": "Framsida", "enum.createrailwaysnavigator.side.info.front": "Informationen ĂĽterges endast pĂĽ framsidan. Detta är standardbeteendet.", "enum.createrailwaysnavigator.side.both": "BĂĽda Sidor", "enum.createrailwaysnavigator.side.info.both": "Informationen kommer att ĂĽterges pĂĽ bĂĽda sidor.", - "enum.createrailwaysnavigator.time_display": "Tidsvisning", "enum.createrailwaysnavigator.time_display.description": "Bestämmer hur tiden ska visas.", "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (visar klockslaget dĂĽ fordonet ankommer)", "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (visar fordonets beräknade ankomsttid)", - "create.display_source.advanced_display": "Avancerade Skärmar", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "TĂĽgnamnets kolumnbredd", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "i blockpixlar. (Standard: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Plattformens kolumnbredd", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "i blockpixlar. (Standard: Auto)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.section_settings.title": "Sektionsinställningar", + "gui.createrailwaysnavigator.section_settings.train_groups": "Tilldela tĂĽggrupp", + "gui.createrailwaysnavigator.section_settings.train_lines": "Tilldela tĂĽglinje", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Inkludera start av nästa sektion", + "gui.createrailwaysnavigator.section_settings.usable": "Navigerbar", + "gui.createrailwaysnavigator.section_settings.none": "(Ingen)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Dynamisk fĂśrdrĂśjning", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Minimal Varaktighet", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Vänta: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "eller ĂĽtminstone %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Ny schemasektion", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "BĂśrjan av en ny schemasektion.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Hantera...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Tilldela tĂĽggrupp: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Tilldela TĂĽggrupp: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Inkludera start av nästa sektion: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Navigerbar: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Återställ tiderna", + "display.createrailwaysnavigator.train_destination.simple": "Kompakt", + "display.createrailwaysnavigator.train_destination.extended": "UtĂśkad", + "display.createrailwaysnavigator.train_destination.detailed": "UrspĂĽrad", + "display.createrailwaysnavigator.passenger_information.running_text": "Rullande Text", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Detaljerad med Schema", + "display.createrailwaysnavigator.platform.running_text": "Rullande Text", + "display.createrailwaysnavigator.platform.table": "Tabell", + "display.createrailwaysnavigator.platform.focus": "Fokus", + "gui.createrailwaysnavigator.saved_routes.title": "Sparade rutter", + "gui.createrailwaysnavigator.schedule_board.title": "Schematavla", + "gui.createrailwaysnavigator.station_tags.title": "Inställningar FĂśr Stationstaggar", + "gui.createrailwaysnavigator.journey_info.title": "Reseinformation", + "gui.createrailwaysnavigator.journey_info.date": "Dag %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) till %s", + "gui.createrailwaysnavigator.color_picker.custom": "Anpassad...", + "gui.createrailwaysnavigator.color_picker.no_color": "Ingen färg", + "gui.createrailwaysnavigator.schedule_board.view_details": "Visa Detaljer", + "gui.createrailwaysnavigator.schedule_board.train_from": "frĂĽn %s", + "gui.createrailwaysnavigator.search_options.departure_in": "AvgĂĽng om", + "gui.createrailwaysnavigator.search_options.train_groups": "TĂĽggrupper", + "gui.createrailwaysnavigator.search_options.transfer_time": "Bytestid", + "gui.createrailwaysnavigator.search_options.advanced_options": "Avancerade inställningar", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Alla", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s exkluderad", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Alla", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s exkluderad", + "gui.createrailwaysnavigator.empty_list": "Listan är tom", + "gui.createrailwaysnavigator.new_entry.add": "Lägg till ny post", + "gui.createrailwaysnavigator.new_entry.new": "Ny:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s sparad", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Schemat har ändrats!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "Schemat fĂśr din sparade rutt har ändrats. Vänligen kolla navigatorn fĂśr dessa ändringar.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Byten", + "gui.createrailwaysnavigator.route_overview.cancelled": "Inställd", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Sparad Rutt", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Ankomst", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Inställd", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "FĂśrsening cirka %s minuter", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information om %s: Detta tĂĽget blev inställt", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information om %s: FĂśrsening cirka %s minuter", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", är inställd idag. Vi ber om ursäkt fĂśr besväret.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", idag cirka %s minuter fĂśrsenad.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Anledning: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Visa detaljer", + "gui.createrailwaysnavigator.route_widget.save": "Spara", + "gui.createrailwaysnavigator.route_widget.remove": "Ta bort", + "gui.createrailwaysnavigator.route_widget.share": "Dela...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "FĂśrr i tiden", + "gui.createrailwaysnavigator.saved_routes.today": "Idag", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Imorgon", + "gui.createrailwaysnavigator.saved_routes.in_days": "Om %s dagar", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Visa detaljer", + "gui.createrailwaysnavigator.saved_route_widget.share": "Dela...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Visa Notifikationer", + "gui.createrailwaysnavigator.train_status.unknown_delay": "DriftfĂśrsening", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "FĂśrsening i fĂśrra resan", + "gui.createrailwaysnavigator.train_status.red_signal": "RĂśd signal", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Prioritet fĂśr ett annat tĂĽg", + "gui.createrailwaysnavigator.train_status.delay_other_train": "FĂśrsening av ett annat tĂĽg", + "gui.createrailwaysnavigator.train_status.track_closed": "SpĂĽr avstängt", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Personalbrist", + "gui.createrailwaysnavigator.train_status.operational_disruption": "DriftstĂśrning", + "gui.createrailwaysnavigator.train_status.special_trip": "Specialresa", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Spara rutt", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Radera rutt", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Visa Popup", + "gui.createrailwaysnavigator.navigator.my_profile": "Min Profil", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Vissa rutter kan vara ofullständiga eller inte fĂśreslagna alls eftersom att Create Järnvägsnavigatorn inte har initialiserat alla tĂĽg ännu.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "TĂĽglinjer", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Välj färg", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Skapa tĂĽglinjer fĂśr att gruppera olika tĂĽg tillsammans, skriv Ăśver deras visningsnamn och justera andra inställningar fĂśr varje linje. (Kan endast bli tilldelad till tĂĽgen i schemat)" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json index e1d8537b..4de46c0a 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/sxu.json @@ -3,56 +3,51 @@ "advancement.createrailwaysnavigator.navigator.description": "Schdelle enen Wegweesr her, um Zuhchferbindungn zwischn zwee färschiednen BoahnhĂśfn ze findn.", "advancement.createrailwaysnavigator.advanced_display": "Ne ganz 4goa", "advancement.createrailwaysnavigator.advanced_display.description": "forbessere deene Anzeichedafl, um mehr Informadschionen darzeschdellen und se sogoar uf ZĂźhchn ze ferwen'n.", - - "itemGroup.createrailwaysnavigator.tab": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator": "Create Railways Navigator", - "item.createrailwaysnavigator.navigator.tooltip.summary": "Dor _Wegweesr_ zeichd mĂśchliche _Zuhchforbindungn_ midd zusädslichn Informadschionen, wie _Zwischenhalde_, _Echdzeiddaden_ und mehr", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "Dor _Wegweesr_ zeichd mĂśchliche _Zuhchforbindungn_ midd zusädslichn Informadschionen, wie _Zwischenhalde_, _Echdzeiddaden_ und mehr", "block.createrailwaysnavigator.train_station_clock": "Boahnhofsuhr", "block.createrailwaysnavigator.advanced_display_block": "Forbessorrdor Anzeichn-Blogg", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", "block.createrailwaysnavigator.advanced_display": "Forbessorrde Anzeichedoafl", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", "block.createrailwaysnavigator.advanced_display_small": "Gleene forbessorrde Anzeichedofl", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", "block.createrailwaysnavigator.advanced_display_panel": "Forbessorrdes Anzeichebännl", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", "block.createrailwaysnavigator.advanced_display_half_panel": "Halbes Forbessorrdes Anzeichebännl", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", "block.createrailwaysnavigator.advanced_display_sloped": "Schrääsche forbessorrde Anzeichedofl", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", + "block.createrailwaysnavigator.advanced_display_slab": "Forbessorde Anzeicheschdufe", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Benuddse dä Blogg uf _ZĂźhchn_ als _Zuhchzielanzeiche_ odorr _Foahrgassdinformadschionsanzeichn_ odorr an _BoahnhĂśfn_ als forbesserrde _Bahnschdeichanzeichn_, die och mehr Informadschionen anzeichn als normale Anzeichedofln.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "R-Gligg midd nem SchraubnschlĂźssl", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Öffne ä _MenĂź_, um dä Anzeichn ze _gonfiguriern_.", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Beddriebschfoahrd", - "category.createrailwaysnavigator.crn": "Create Railways Navigator", "key.createrailwaysnavigator.route_overlay_options": "Schdreggnanzeiche Eenschdellungn", - "enum.createrailwaysnavigator.overlay_position": "Anzeichebosiddsion", "enum.createrailwaysnavigator.overlay_position.info.top_left": "Egge ob'n lings", "enum.createrailwaysnavigator.overlay_position.info.top_right": "Egge ob'n reschds", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Egge und'n lings", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Egge und'n reschds", - "gui.createrailwaysnavigator.loading.title": "Dodai'n wer'n herundorgeladn...", - "gui.createrailwaysnavigator.overlay_settings.title": "Schdreggnanzeiche Eenschdellungn", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Deddails anzeichn", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Anzeiche endfernn", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "OrrzählerangĂźndschung'n aggdiwierd.", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "OrrzählerangĂźndschungn deaggdiwierd.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "OrrzählerangĂźndschungn deaggdiwierd.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Benoochrischdschung'n aggdiwierd.", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Benoochrischdschung'n deaggdiwierd.", "gui.createrailwaysnavigator.route_overlay_settings.scale": "Benuddsoroberflächenschgallierung", @@ -60,7 +55,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Dor Orrzähler gĂźnndschd wichdsche Ereichnisse uf dener Schdregge an, z.B. dorr nächsde Hald.", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Benoochrischdschung'n", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Orhallde Benoochrischdschung'n in Form fonn Bob-Abbs ze wichdschn Oreichnissen, z.B. Forschbädung'n", - "gui.createrailwaysnavigator.common.expand": "Deddails anzeichn", "gui.createrailwaysnavigator.common.collapse": "Deddails Ferbärchn", "gui.createrailwaysnavigator.common.go_back": "Zorigge", @@ -72,8 +66,9 @@ "gui.createrailwaysnavigator.common.search": "Suchn", "gui.createrailwaysnavigator.common.auto": "Audom.", "gui.createrailwaysnavigator.common.server_error": "Fehlor beim AusfĂźhrn dor Ufgobe. Schau in de Servergonsole.", - "gui.createrailwaysnavigator.via": "Ăźbor", - + "gui.createrailwaysnavigator.common.delete": "LĂśschn", + "gui.createrailwaysnavigator.common.add": "Hinzufieschn", + "gui.createrailwaysnavigator.common.help": "Hilfe holn", "gui.createrailwaysnavigator.navigator.title": "Create Railways Navigator", "gui.createrailwaysnavigator.navigator.no_connections": "Geene Forbindung'n gefund'n.", "gui.createrailwaysnavigator.navigator.not_searched": "Bisher nĂźschd gesuchd.", @@ -88,14 +83,12 @@ "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Sucheenschdellungn", "gui.createrailwaysnavigator.navigator.search.tooltip": "Suchn", "gui.createrailwaysnavigator.navigator.location.tooltip": "Nächschdgelech'ne Schdadschion fonn aggduellor Bosidschsion", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Aggdualisiere", "gui.createrailwaysnavigator.navigator.switch.tooltip": "Eingabn dauschn", - "gui.createrailwaysnavigator.route_details.title": "Schdreggendeddails", "gui.createrailwaysnavigator.route_details.departure": "Abfohrd in", "gui.createrailwaysnavigator.route_details.next_transfer_time": "Umschdiech in", "gui.createrailwaysnavigator.route_details.transfer": "Umschdiech", - "gui.createrailwaysnavigator.route_details.save_route": "Schdregge sbeichorn", - "gui.createrailwaysnavigator.route_overview.title": "Schdreggendeddails", "gui.createrailwaysnavigator.route_overview.journey_begins": "Ihre Reise beginnd! %s nooch %s, Abfohrd %s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Ihre Reise beginnd! %s nooch %s, Abfohrd %s fonn Gleis %s", @@ -138,7 +131,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Se hamm Ihr Ziel erreischd!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Wir bedangn uns fĂźr Ihre Reise und wĂźnschen nen schĂśnen Dahch.", "gui.createrailwaysnavigator.route_overview.date": "Dahch %s, %s", - "gui.createrailwaysnavigator.global_settings.title": "Globoale Eenschdellungn", "gui.createrailwaysnavigator.global_settings.option.tooltip": "Zum Beoarbeidn gliggn", "gui.createrailwaysnavigator.global_settings.option_alias.title": "Boahnhof Dägs", @@ -149,33 +141,28 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "Zuhchgaddegorien dienen dorr Organisadsion fonn ZĂźhchn (z.B. Regionalforgehr, Fernforgehr, ...). Nudzer gĂśnn'n nooch diesen Gadegorien fildorn.", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Zuhch Blägglisd", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Schließe ZĂźhche aus, z.B. GĂźdorzĂźhche, SonderzĂźhche, edc., damidd diese ne in'dn Forschlächn Ufdauchn.", - - "gui.createrailwaysnavigator.station_tags.title": "Boahnhof Dägs Eenschdellungn", "gui.createrailwaysnavigator.station_tags.summary": "Endhäld %s Schdadsionen", "gui.createrailwaysnavigator.station_tags.editor": "Z'ledsd fonn %s am %s beoarbeided", "gui.createrailwaysnavigator.station_tags.add.tooltip": "Neuen Eindrahch erschdelln", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Dahch lĂśschn", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Schdadschion endfernn", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Gligg zum ändorn", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Schdadschion hinzufĂźchn", "gui.createrailwaysnavigator.station_tags.hint.station_name": "Boahnhofsname", "gui.createrailwaysnavigator.station_tags.hint.platform": "Gleisbezeichnung", "gui.createrailwaysnavigator.station_tags.enter_name": "Name eingeb'n", - "gui.createrailwaysnavigator.train_group_settings.title": "Zuhchgaddegorien Eenschdellungn", "gui.createrailwaysnavigator.train_group_settings.summary": "Endhäld %s ZĂźhche", "gui.createrailwaysnavigator.train_group_settings.editor": "Z'ledsd fonn %s am %s beoarbeided", "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Gaddegorie lĂśschn", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Zuhch endfernn", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Zuhch hinzufĂźchn", - "gui.createrailwaysnavigator.blacklist.title": "Boahnhof Blägglisd", "gui.createrailwaysnavigator.blacklist.add.tooltip": "Zur Blägglisd hinzufĂźchn", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Fonn Blägglisd endfernn", - "gui.createrailwaysnavigator.train_blacklist.title": "Zuhch Blägglisd", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Zur Blägglisd hinzufĂźchn", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Fonn Blägglisd endfernn", - "gui.createrailwaysnavigator.search_settings.title": "Sucheenschdellungn", "gui.createrailwaysnavigator.search_settings.transfer_time": "Minimale Umschdiechszeid", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Die minimale Zeid fĂźr nen Umschdiech. (1h ~ 50 Real Life Seggundn)", @@ -185,28 +172,24 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Alle", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Geene", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Fildor zoriggesedsn", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Hinzufieschn", - "gui.createrailwaysnavigator.time": "Zeid: %s", "gui.createrailwaysnavigator.time.now": "nu", "gui.createrailwaysnavigator.time_format.dhm": "%s Dahche %s Schd. %s min.", "gui.createrailwaysnavigator.time_format.hm": "%s Schd. %s min.", "gui.createrailwaysnavigator.time_format.m": "%s min.", - "gui.createrailwaysnavigator.platform": "Gleis", "gui.createrailwaysnavigator.departure": "Abfohrd", "gui.createrailwaysnavigator.destination": "Ziel", "gui.createrailwaysnavigator.line": "Linie", "gui.createrailwaysnavigator.following_trains": "FolgezĂźhche:", - + "gui.createrailwaysnavigator.via": "Ăźbor", "gui.createrailwaysnavigator.advanced_display_settings.title": "Forbessorrde Anzeiche Eenschdellungn", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "AnzeichedĂźhb", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Die Informadschionen, die angezeichd wer'n solln.", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Informadschionsard", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Gibd an, wie dedallierd dä Informadschionen sind.", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Beidseidsch", - "enum.createrailwaysnavigator.display_info_type": "Informadschionsard", "enum.createrailwaysnavigator.display_info_type.description": "Gibd an, wie fiele Informadschionen dargeschdelld wer'n solln.", "enum.createrailwaysnavigator.display_info_type.simple": "Eefach", @@ -215,7 +198,6 @@ "enum.createrailwaysnavigator.display_info_type.info.detailed": "Die wichdigsdn Informadschionen midd einichen Deddails, wie z.B Zwischenhalde.", "enum.createrailwaysnavigator.display_info_type.informative": "Informaddief", "enum.createrailwaysnavigator.display_info_type.info.informative": "Zeichd alle Informadschionen, die relewand sein gĂśnn'n, und schdelld sie mĂśchlichsd anschaulich dar. Ne fĂźr gleene Anzeichn embfohlen.", - "enum.createrailwaysnavigator.display_type": "AnzeichedĂźhb", "enum.createrailwaysnavigator.display_type.description": "Die Ard dorr Anzeiche, was fonn ihr'm Zwegg abhängd.", "enum.createrailwaysnavigator.display_type.train_destination": "Zuhchzielanzeiche", @@ -224,26 +206,110 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "Rebräsendiern dä Anzeichn, die in ZĂźhchn forgomm'n. Dargeschdelld wer'n dorr nächsde Hald, die Ausschdiechsseide und (je nooch Eenschdellung) noch weidere Informadschionen.", "enum.createrailwaysnavigator.display_type.platform": "Bahnschdeichsanzeiche", "enum.createrailwaysnavigator.display_type.info.platform": "Diese Anzeichn wer'n an Bahnschdeichn ferwend' und zeichen dä nächsden abfahrnden ZĂźhche midd zusädslichn Informadschionen an. Fungdschionierd ne uf ZĂźhchn!", - "enum.createrailwaysnavigator.side": "Seidde", "enum.createrailwaysnavigator.side.description": "Die Seidde des Bloggs, uf dem de Informadschionen angezeichd wer'n solln.", "enum.createrailwaysnavigator.side.front": "Forderseidde", "enum.createrailwaysnavigator.side.info.front": "Die Informadschionen wer'n nur uf dorr Forderseidde dargeschdelld.", "enum.createrailwaysnavigator.side.both": "Beidseidsch", "enum.createrailwaysnavigator.side.info.both": "Die Informadschionen wer'n uf beidn Seiddn dargeschdelld.", - "enum.createrailwaysnavigator.time_display": "Zeidanzeiche", "enum.createrailwaysnavigator.time_display.description": "Gibd an, in welchm Formad dä Zeid angegebn wird.", "enum.createrailwaysnavigator.time_display.abs": "ABS", "enum.createrailwaysnavigator.time_display.info.abs": "ABS (Absolud)", "enum.createrailwaysnavigator.time_display.eta": "ETA", "enum.createrailwaysnavigator.time_display.info.eta": "ETA (Geschädsde Angunfdszeid)", - "create.display_source.advanced_display": "Forbessorrde Anzeiche", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Zuhchname Schbaldnbreide", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "Dä Breide dorr Schbalde fĂźr'n Zuhchname in Biggsl. (Schdandard: 16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Gleis Schbaldnbreide", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "Die Breide dorr Schbalde fĂźr's Gleis in Biggsl. (-1 = Audom., Schdandard: -1)", - - "createrailwaysnavigator.moin": "Dahch fonn Schdeffn" + "gui.createrailwaysnavigator.section_settings.title": "Abbschniddseenschdellungen", + "gui.createrailwaysnavigator.section_settings.train_groups": "Zuchgrubbe zuweisn", + "gui.createrailwaysnavigator.section_settings.train_lines": "Zuchlinie zuweisn", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "Schdoardd des näschsdn Bereischs einbinden", + "gui.createrailwaysnavigator.section_settings.usable": "Weechweesboar", + "gui.createrailwaysnavigator.section_settings.none": "(Geene)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Dinnamische ForzĂśscherung", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Mindesworddezeid", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Wordde: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "odor mindeschdens %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Neuer Foahrblanabschnidd", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Anfang eenes neuen Foahrblanabschnidd.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Gonfigurieren...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Seddse Zuchgaddegorie: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Seddse Zuchgaddegorie: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - Schdoardd des näschsdn Bereischs einbinden: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Weechweesboar: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Zeiddn zoriggeseddsn", + "display.createrailwaysnavigator.train_destination.simple": "Gommbaggd", + "display.createrailwaysnavigator.train_destination.extended": "Erweidord", + "display.createrailwaysnavigator.train_destination.detailed": "Deddallierd", + "display.createrailwaysnavigator.passenger_information.running_text": "Loofdeggsd", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Deddallierd mid Foahrblan", + "display.createrailwaysnavigator.platform.running_text": "Loofdeggsd", + "display.createrailwaysnavigator.platform.table": "Dabelle", + "display.createrailwaysnavigator.platform.focus": "Foggussierd", + "gui.createrailwaysnavigator.saved_routes.title": "Geschbeichorde Reisen", + "gui.createrailwaysnavigator.schedule_board.title": "Abfoahrdenlisdde", + "gui.createrailwaysnavigator.station_tags.title": "Boahnhof Dägs Eenschdellungn", + "gui.createrailwaysnavigator.journey_info.title": "Reiseinformadschonen", + "gui.createrailwaysnavigator.journey_info.date": "Dach %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) noach %s", + "gui.createrailwaysnavigator.color_picker.custom": "Benuddserdefinieord...", + "gui.createrailwaysnavigator.color_picker.no_color": "Geene Farbe", + "gui.createrailwaysnavigator.schedule_board.view_details": "Deddails ansehen", + "gui.createrailwaysnavigator.schedule_board.train_from": "fonn %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Abfohrd in", + "gui.createrailwaysnavigator.search_options.train_groups": "Zuhchgaddegorien", + "gui.createrailwaysnavigator.search_options.transfer_time": "Umschdiech in", + "gui.createrailwaysnavigator.search_options.advanced_options": "Erweidorrde Obschonen", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Alle", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s ausgeschlossn", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Alle", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s ausgeschlossn", + "gui.createrailwaysnavigator.empty_list": "De Lisde is leer", + "gui.createrailwaysnavigator.new_entry.add": "Neuen Eindrach hinzufieschn", + "gui.createrailwaysnavigator.new_entry.new": "Neu:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s geschbeischord", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "Foahrzeidn geändorrd!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "De Foahrzeidn eenor geschbeischorrdn Schdregge hamm sich geändorrd. Bidde informieren se sich im Weechweesor Ăźbor diese Änderungen.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Umschdieche", + "gui.createrailwaysnavigator.route_overview.cancelled": "Fälld aus", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Geschbeichorde Reisen", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Angunnfd", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "Fälld aus", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Forschbädschung za. %s Minudn", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Informadschonen Ăźbor %s: Fälld heude aus", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Informadschonen Ăźbor %s: Forschbädschung za. %s Minudn", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", fälld heude aus. Wir Biddn um Enschuldschung.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", heude za. %s Minudn schbädorr.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Grund: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Deddails ansehen", + "gui.createrailwaysnavigator.route_widget.save": "Schbeischorn", + "gui.createrailwaysnavigator.route_widget.remove": "Nimmer schbeischorn", + "gui.createrailwaysnavigator.route_widget.share": "Deilen...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "In dorr Forrgangenheid", + "gui.createrailwaysnavigator.saved_routes.today": "Heude", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Morschn", + "gui.createrailwaysnavigator.saved_routes.in_days": "In %s Dachn", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Deddails anzeichn", + "gui.createrailwaysnavigator.saved_route_widget.share": "Deilen...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Benoochrischdschung'n anzeischn", + "gui.createrailwaysnavigator.train_status.unknown_delay": "ForrzĂśscherung'n im Beddriebsablauf", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Forschbädung aus forhährscher Foahrd", + "gui.createrailwaysnavigator.train_status.red_signal": "Rodes Signoal", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Forfoahrd eenes andorren Zuches", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Forschbädung eenes forausfoahrenden Zuches", + "gui.createrailwaysnavigator.train_status.track_closed": "Schdregge geschberrd", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Bersonalausfall", + "gui.createrailwaysnavigator.train_status.operational_disruption": "BedriebsschdĂśrung", + "gui.createrailwaysnavigator.train_status.special_trip": "Sondorfoahrd", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Reise schbeischorn", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Reise nimmer schbeischorn", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Bobub-Fenschdorr anzeichn", + "gui.createrailwaysnavigator.navigator.my_profile": "Mein Brofil", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Mansche Schdreggen gĂśnn'n unvollschdänsch sein oder Ăźberhaubt ne vorgeschlagn wer'n, da Create Railways Navigator noch ne alle ZĂźche inidschialisierd had.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Zuchlinien", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Farbe auswähln", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Errschdelle Linien, um mehrere ZĂźche zu LinienzĂźchn zusammezufassen, deren Anzeichename zu Ăźborschreiben oder um weidorre Eenschdellungen anzebassen. (Gann nur im Foahrplan zugewiesn werden)" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/uk_ua.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/uk_ua.json new file mode 100644 index 00000000..cd98f5e8 --- /dev/null +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/uk_ua.json @@ -0,0 +1,315 @@ +{ + "advancement.createrailwaysnavigator.navigator": "Дякуємо Са подорож", + "advancement.createrailwaysnavigator.navigator.description": "Створіть Навігатор для пОшуку залізничних сполучень від однієї станції Đ´Đž іншої.", + "advancement.createrailwaysnavigator.advanced_display": "Майже 4k", + "advancement.createrailwaysnavigator.advanced_display.description": "Оновіть свої інформаційні табло, щоб вОни пОкаСуваНи більше інформації, і навіть розмістіть їх у потягах.", + "item.createrailwaysnavigator.navigator": "Створіть Залізничний Навігатор", + "item.createrailwaysnavigator.navigator.tooltip.summary": "_Навігатор_ показує можливі _залізничні сполучення_ С додатковою інформацією, такою як _Супинки_, _дані в реальному часі_ та інше.", + "block.createrailwaysnavigator.train_station_clock": "Годинник Залізничної станції", + "block.createrailwaysnavigator.advanced_display_block": "Розширений Блок Дисплею", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "Натискаючи ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display": "Розширене ТайНО Дисплею", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "При ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display_small": "Мале Просунуте ТайНО", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "При ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display_panel": "Просунута Панель Дисплею", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "При ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display_half_panel": "Половина Просунутої Панелі Дисплею", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "При ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display_sloped": "Похилене Просунуте ТайНО", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "При ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display_slab": "Розширена Плита Дисплею", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "Використовуйте КОгО у _поїздах_ як _диспНоК призначення поїзда_ айО _інформаційний диспНоК для пасажирів_, айО на _залізничних станціях_ як _покращені платформні дисплеї_, які також показують більше інформації, ніж звичайні табло.", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "При ПКМ Са допомогою Гайкового ключа", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "Відкрийте меню щоб _налаштувати_ _табло_.", + "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "Не працює!", + "category.createrailwaysnavigator.crn": "Залізничний Навігатор", + "key.createrailwaysnavigator.route_overlay_options": "Показувати параметри накладання маршруту", + "enum.createrailwaysnavigator.overlay_position": "Розташування дисплею", + "enum.createrailwaysnavigator.overlay_position.info.top_left": "Верхній лівий кут", + "enum.createrailwaysnavigator.overlay_position.info.top_right": "Верхній Правий Кут", + "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "Нижній Лівий Кут", + "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "Нижній правий кут", + "gui.createrailwaysnavigator.loading.title": "Завантаження даних із сервера...", + "gui.createrailwaysnavigator.overlay_settings.title": "Налаштування накладання маршруту", + "gui.createrailwaysnavigator.route_overlay_settings.show_details": "Показати деталі", + "gui.createrailwaysnavigator.route_overlay_settings.unpin": "Видалити накладання маршруту", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "Оголошення Диктора увімкнено.", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "Оголошення Диктора виПкнонО.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "Сповіщення увімкнено", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "Сповіщення виПкнонО", + "gui.createrailwaysnavigator.route_overlay_settings.scale": "Масштаб інтерфейсу", + "gui.createrailwaysnavigator.route_overlay_settings.narrator": "Оголошення Диктора", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "Диктор оголошує важливі події у вашій подорожі, наприклад, наступна Супинка, зміни тощо.", + "gui.createrailwaysnavigator.route_overlay_settings.notifications": "Сповіщення", + "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "Отримувати сповіщення про важливі події в поїздці, наприклад, про наступну Супинку, зміни тощо.", + "gui.createrailwaysnavigator.common.expand": "Показати подробиці", + "gui.createrailwaysnavigator.common.collapse": "Сховати подробиці", + "gui.createrailwaysnavigator.common.go_back": "Назад", + "gui.createrailwaysnavigator.common.go_to_top": "Прокрутити на початок", + "gui.createrailwaysnavigator.common.reset_defaults": "Відновити стандартні значення", + "gui.createrailwaysnavigator.common.count": "Кількість", + "gui.createrailwaysnavigator.common.true": "Так", + "gui.createrailwaysnavigator.common.false": "Ні", + "gui.createrailwaysnavigator.common.search": "Пошук", + "gui.createrailwaysnavigator.common.auto": "Авто", + "gui.createrailwaysnavigator.common.server_error": "Помилка сервера під час виконання завдання. Дивись консоль для отримання додаткової інформації.", + "gui.createrailwaysnavigator.common.delete": "Видалити", + "gui.createrailwaysnavigator.common.add": "Додати", + "gui.createrailwaysnavigator.common.help": "Допомога", + "gui.createrailwaysnavigator.navigator.title": "Залізничний Навігатор", + "gui.createrailwaysnavigator.navigator.no_connections": "З'єднань но СнаКдонО.", + "gui.createrailwaysnavigator.navigator.not_searched": "Поки що нічого но знайшлося.", + "gui.createrailwaysnavigator.navigator.searching": "Пошук підключень...", + "gui.createrailwaysnavigator.navigator.error_title": "Помилка при навігації!", + "gui.createrailwaysnavigator.navigator.start_end_null": "Старт айО призначення порожні.", + "gui.createrailwaysnavigator.navigator.start_end_equal": "Початок і призначення рівні.", + "gui.createrailwaysnavigator.navigator.route_entry.connection_in_past": "❌ З'єднання в ПинуНОПу", + "gui.createrailwaysnavigator.navigator.route_entry.transfer": "Пересадка", + "gui.createrailwaysnavigator.navigator.route_entry.station_start": "С %s", + "gui.createrailwaysnavigator.navigator.global_settings.tooltip": "Глобальні налаштування", + "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "Пошук налаштувань", + "gui.createrailwaysnavigator.navigator.search.tooltip": "Пошук", + "gui.createrailwaysnavigator.navigator.location.tooltip": "Найближча станція С поточної позиції", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "Оновити", + "gui.createrailwaysnavigator.navigator.switch.tooltip": "Перемкнути поля", + "gui.createrailwaysnavigator.route_details.title": "Подробиці маршруту", + "gui.createrailwaysnavigator.route_details.departure": "Відправлення через", + "gui.createrailwaysnavigator.route_details.next_transfer_time": "Пересадка через", + "gui.createrailwaysnavigator.route_details.transfer": "Пересадка", + "gui.createrailwaysnavigator.route_overview.title": "Супутник подорожі", + "gui.createrailwaysnavigator.route_overview.journey_begins": "Ваша поїздка починається! %s Đ´Đž %s, відправлення %s", + "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "Ваша поїздка починається! %s Đ´Đž %s, відправлення %s С платформи %s", + "gui.createrailwaysnavigator.route_overview.train_details": "%s Đ´Đž %s", + "gui.createrailwaysnavigator.route_overview.next_stop": "Наступна Супинка: %s", + "gui.createrailwaysnavigator.route_overview.transfer": "Змінити %s → %s", + "gui.createrailwaysnavigator.route_overview.transfer_with_platform": "Змінити %s → %s на платформі %s", + "gui.createrailwaysnavigator.route_overview.journey_completed": "Поїздку завершено", + "gui.createrailwaysnavigator.route_overview.after_journey": "Ви досягли %s. Дякуємо Са подорож і приємного дня.", + "gui.createrailwaysnavigator.route_overview.next_connections": "Наступне С'єднання", + "gui.createrailwaysnavigator.route_overview.schedule_transfer": "Планування пересадки", + "gui.createrailwaysnavigator.route_overview.train_cancelled": "Поїзд скасОванО", + "gui.createrailwaysnavigator.route_overview.train_cancellation_info": "Інформація про %s", + "gui.createrailwaysnavigator.route_overview.stop_cancelled": "❌ ХкасОванО", + "gui.createrailwaysnavigator.route_overview.connection_endangered": "З'єднання втрачене", + "gui.createrailwaysnavigator.route_overview.connection_missed": "З'єднання пропущено", + "gui.createrailwaysnavigator.route_overview.connection_cancelled": "Поїзд скасОванО", + "gui.createrailwaysnavigator.route_overview.journey_interrupted": "Ваша поїздка Đ´Đž %s но ПОМо бути продовжена.", + "gui.createrailwaysnavigator.route_overview.connection_missed_info": "Через затримку поїзда ви пропустили через пересадку. Знайжіть альтернативу в навігаторі. Перепрошиваємо Са незручності.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_info": "Інформацію про %s: Сьогодні цей поїзд скасОванО! Ми вибачаємося Са незручності. Знайдіть альтернативу в навігації.", + "gui.createrailwaysnavigator.route_overview.train_cancelled_title": "Поїзд йуНО скасОванО", + "gui.createrailwaysnavigator.route_overview.journey_interrupted_info": "Ваша подорож Đ´Đž %s но ПОМо бути продовжена. Знайдіть альтернативу в навігаторі.", + "gui.createrailwaysnavigator.route_overview.options": "Натисніть %s для опцій.", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins.title": "Ваша поїздка Đ´Đž %s починається!", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins": "%s Đ´Đž %s, відправлення %s", + "gui.createrailwaysnavigator.route_overview.notification.journey_begins_with_platform": "%s Đ´Đž %s, відправлення %s С платформи %s", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "Ваша платформа змінилася!", + "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "Ваш поїзд через %s покидає С платформи %s сьогодні.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s: Прибуття %s затрималося.", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "%s замість %s через %s", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s: Поїзд скасОванО", + "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "%s Đ´Đž %s відмінено сьогодні.", + "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "Наближається пересадка", + "gui.createrailwaysnavigator.route_overview.notification.transfer": "Змінити %s → %s", + "gui.createrailwaysnavigator.route_overview.notification.transfer_with_platform": "Змінити %s → %s на платформі %s", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered.title": "З'єднання втрачене!", + "gui.createrailwaysnavigator.route_overview.notification.connection_endangered": "Можливо, ви но зможете дістатися від %s Đ´Đž %s.", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed.title": "Пропущене С'єднання", + "gui.createrailwaysnavigator.route_overview.notification.connection_missed": "Ви пропустили свій поїзд пересадки від %s Đ´Đž %s.", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "Досягнуто пункту призначення!", + "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "Дякую Са подорож і гарного дня", + "gui.createrailwaysnavigator.route_overview.date": "День %s, %s", + "gui.createrailwaysnavigator.global_settings.title": "Глобальні налаштування", + "gui.createrailwaysnavigator.global_settings.option.tooltip": "Натисніть для редагування", + "gui.createrailwaysnavigator.global_settings.option_alias.title": "Тоги Залізничної станції", + "gui.createrailwaysnavigator.global_settings.option_alias.description": "Об'єднати окремі залізничні станції в Одну багато платформну станцію С власною назвою, щоб навігатор пропонував пересадки та інше.", + "gui.createrailwaysnavigator.global_settings.option_blacklist.title": "Чорний ХписОк Залізничних Станцій", + "gui.createrailwaysnavigator.global_settings.option_blacklist.description": "Сховати залізничні станції, які но повинні С'являтися в результатах навігації. Ці станції будуть проігноровані при створенні маршрутів.", + "gui.createrailwaysnavigator.global_settings.train_group.title": "Поїздні групи", + "gui.createrailwaysnavigator.global_settings.train_group.description": "Створюйте групи поїздів, щоб організувати всі поїзди (наприклад, регіональні пОсНуги, міжміські пОсНуги тощо.) Користувачі можуть вирішити, які залізничні групи вОни хочуть використовувати.", + "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "Чорний списОк поїздів", + "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "Сховати поїзди, наприклад, вантажні поїзди, спеціальні потяги тощо., щоб їх но використовували в пропонованих напрямках.", + "gui.createrailwaysnavigator.station_tags.summary": "Містить %s залізничних станцій", + "gui.createrailwaysnavigator.station_tags.editor": "Востаннє змінено %s Đž %s", + "gui.createrailwaysnavigator.station_tags.add.tooltip": "Створити нОвиК Сапис", + "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "Видалити тег", + "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "Прибрати станцію", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "Натисніть, щоб змінити", + "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "Додати станцію", + "gui.createrailwaysnavigator.station_tags.hint.station_name": "Назва Залізничної станції", + "gui.createrailwaysnavigator.station_tags.hint.platform": "Назва платформи", + "gui.createrailwaysnavigator.station_tags.enter_name": "Введіть тут ім'я", + "gui.createrailwaysnavigator.train_group_settings.title": "Налаштування Групи Поїздів", + "gui.createrailwaysnavigator.train_group_settings.summary": "Містить %s поїздів", + "gui.createrailwaysnavigator.train_group_settings.editor": "Востаннє змінено %s Đž %s", + "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "Видалити групу", + "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "Прибрати поїзд", + "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "Додати поїзд", + "gui.createrailwaysnavigator.blacklist.title": "Чорний ХписОк Залізничних Станцій", + "gui.createrailwaysnavigator.blacklist.add.tooltip": "Додати Đ´Đž чорного списку", + "gui.createrailwaysnavigator.blacklist.delete.tooltip": "Видалити С чорного списку", + "gui.createrailwaysnavigator.train_blacklist.title": "Чорний списОк поїздів", + "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "Додати Đ´Đž чорного списку", + "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "Видалити С чорного списку", + "gui.createrailwaysnavigator.search_settings.title": "Пошук налаштувань", + "gui.createrailwaysnavigator.search_settings.transfer_time": "Мінімальний Час Пересадки", + "gui.createrailwaysnavigator.search_settings.transfer_time.description": "Мінімальний час, який пОвинон бути доступний для пересадки між поїздами. (1 гОд ~ 50 реальних сокунд)", + "gui.createrailwaysnavigator.search_settings.train_groups": "Фільтр категорії поїздів", + "gui.createrailwaysnavigator.search_settings.train_groups.description": "Вирішіть, С яких категорій поїздів, які ви хочете використовувати.", + "gui.createrailwaysnavigator.search_settings.train_groups.overview": "Вибрано категорії %s", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "Усі", + "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "Немає", + "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "Скинути фільтр", + "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "Додати", + "gui.createrailwaysnavigator.time": "Час: %s", + "gui.createrailwaysnavigator.time.now": "зараз", + "gui.createrailwaysnavigator.time_format.dhm": "%s Днів %s гОд %s хв.", + "gui.createrailwaysnavigator.time_format.hm": "%s Год %s хв.", + "gui.createrailwaysnavigator.time_format.m": "%s мв.", + "gui.createrailwaysnavigator.platform": "Платформа", + "gui.createrailwaysnavigator.departure": "Відправлення", + "gui.createrailwaysnavigator.destination": "Пункт призначення", + "gui.createrailwaysnavigator.line": "Лінія", + "gui.createrailwaysnavigator.following_trains": "Наступні поїзди:", + "gui.createrailwaysnavigator.via": "через", + "gui.createrailwaysnavigator.advanced_display_settings.title": "Розширені налаштування дисплея", + "gui.createrailwaysnavigator.advanced_display_settings.display_type": "Тип Дисплею", + "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "Визначає відображувану інформацію.", + "gui.createrailwaysnavigator.advanced_display_settings.info_type": "Тип інформації", + "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "Визначає, наскільки детально Описана інформація.", + "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "Двосторонній", + "enum.createrailwaysnavigator.display_info_type": "Тип Відображуваної інформації", + "enum.createrailwaysnavigator.display_info_type.description": "Визначає, скільки інформації слід відображати на дисплеї.", + "enum.createrailwaysnavigator.display_info_type.simple": "Спрощено", + "enum.createrailwaysnavigator.display_info_type.info.simple": "Дисплей йудо показувати Нишо найважливішу інформацію йоС додаткових деталей.", + "enum.createrailwaysnavigator.display_info_type.detailed": "Докладно", + "enum.createrailwaysnavigator.display_info_type.info.detailed": "Буде пОкаСана найважливіша інформація С деякими деталями, такими як швидкість поїзда, Супинки тощо.", + "enum.createrailwaysnavigator.display_info_type.informative": "Інформаційно", + "enum.createrailwaysnavigator.display_info_type.info.informative": "Показує всю інформацію, яка ПОМо бути цікавою і відображати її в вишуканОПу макеті. Не рекомендується для малих екранів, оскільки текст ПОМо бути дуМо ПаНиП.", + "enum.createrailwaysnavigator.display_type": "Тип Дисплею", + "enum.createrailwaysnavigator.display_type.description": "Тип вашОгО дисплею, залежить від їх призначення.", + "enum.createrailwaysnavigator.display_type.train_destination": "Пункт призначення поїзда", + "enum.createrailwaysnavigator.display_type.info.train_destination": "ЊОй КОгО використовували Са ПоМаПи потягів, він показує інформацію про саП потяг таку, як ім’я, призначення та (якщо обрано) Супинки та інші відомості.", + "enum.createrailwaysnavigator.display_type.passenger_information": "Інформація для пасажирів", + "enum.createrailwaysnavigator.display_type.info.passenger_information": "Представляє дисплеї всередині поїздів. Такі дисплеї показуватимуть наступну Супинку, сторону виходу та (якщо вибрано) швидкість поїзду та маршрут.", + "enum.createrailwaysnavigator.display_type.platform": "Дисплей платформи", + "enum.createrailwaysnavigator.display_type.info.platform": "Ці дисплеї слід використовувати на залізничних станціях, йО показують наступні поїзди С додатковими подробицями якщо вибрано. Не ПОМна використовувати в поїздах!", + "enum.createrailwaysnavigator.side": "Сторона", + "enum.createrailwaysnavigator.side.description": "Сторона йНОку, на якій слід подавати інформацію.", + "enum.createrailwaysnavigator.side.front": "Лицьова сторона", + "enum.createrailwaysnavigator.side.info.front": "Інформація йудо відображена тільки на лицьовій стороні. ĐŚĐľ типова поведінка.", + "enum.createrailwaysnavigator.side.both": "Обидві сторони", + "enum.createrailwaysnavigator.side.info.both": "Інформація йудо відображена С обох сторін.", + "enum.createrailwaysnavigator.time_display": "Відображення часу", + "enum.createrailwaysnavigator.time_display.description": "Визначає, як має відображатися час.", + "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.info.abs": "ABS (абсолютний)", + "enum.createrailwaysnavigator.time_display.eta": "ЕТА", + "enum.createrailwaysnavigator.time_display.info.eta": "ETA (орієнтовний час прибуття)", + "create.display_source.advanced_display": "Просунуті Дисплеї", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "Ширина стовпця наСви поїзда", + "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "в пікселях йНОку (Са замовчуванням: 16)", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "Ширина кОНОни платформи", + "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "в пікселях йНОку (Са замовчуванням: авто)", + "gui.createrailwaysnavigator.section_settings.title": "Налаштування секції", + "gui.createrailwaysnavigator.section_settings.train_groups": "Призначити поїздну групу", + "gui.createrailwaysnavigator.section_settings.train_lines": "Призначити залізничну лінію", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "З початком наступної секції", + "gui.createrailwaysnavigator.section_settings.usable": "Доступний для навігації", + "gui.createrailwaysnavigator.section_settings.none": "(Не вкаСанО)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "Динамічна затримка", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "Мінімальна тривалість", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "Почекайте: %s..%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "айО принаймні %s", + "createrailwaysnavigator.schedule.instruction.travel_section": "Новий розділ розкладу", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "Початок нОвОгО розділу розкладу.", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "Налаштувати...", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " - Задати поїздну групу: ", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " - Призначити залізничну лінію: ", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " - З початком наступної секції: ", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " - Доступний для навігації: ", + "createrailwaysnavigator.schedule.instruction.reset_timings": "Скинути таймінги", + "display.createrailwaysnavigator.train_destination.simple": "Компактно", + "display.createrailwaysnavigator.train_destination.extended": "Розширено", + "display.createrailwaysnavigator.train_destination.detailed": "Докладно", + "display.createrailwaysnavigator.passenger_information.running_text": "Прокручування тексту", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "Докладно С розкладу", + "display.createrailwaysnavigator.platform.running_text": "Прокручування тексту", + "display.createrailwaysnavigator.platform.table": "Таблиця", + "display.createrailwaysnavigator.platform.focus": "ФОкус", + "gui.createrailwaysnavigator.saved_routes.title": "Збережені маршрути", + "gui.createrailwaysnavigator.schedule_board.title": "Дошка розкладу", + "gui.createrailwaysnavigator.station_tags.title": "Мітки станції", + "gui.createrailwaysnavigator.journey_info.title": "Інформація про подорож", + "gui.createrailwaysnavigator.journey_info.date": "День %s", + "gui.createrailwaysnavigator.journey_info.train": "%s (%s) Đ´Đž %s", + "gui.createrailwaysnavigator.color_picker.custom": "Власне...", + "gui.createrailwaysnavigator.color_picker.no_color": "Без кольору", + "gui.createrailwaysnavigator.schedule_board.view_details": "Переглянути подробиці", + "gui.createrailwaysnavigator.schedule_board.train_from": "С %s", + "gui.createrailwaysnavigator.search_options.departure_in": "Відправлення через", + "gui.createrailwaysnavigator.search_options.train_groups": "Поїздні групи", + "gui.createrailwaysnavigator.search_options.transfer_time": "Час пересадки", + "gui.createrailwaysnavigator.search_options.advanced_options": "Просунуті налаштування", + "gui.createrailwaysnavigator.search_options.train_groups.all": "Усі", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s виключно", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "Усі", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s виключно", + "gui.createrailwaysnavigator.empty_list": "ХписОк порожній", + "gui.createrailwaysnavigator.new_entry.add": "Додати нОвиК Сапис", + "gui.createrailwaysnavigator.new_entry.new": "Нове:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s збережено", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "РОСкНад змінився!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "РОСкНад збереженого маршруту змінився. Будь Наска, перевірте навігатор.", + "gui.createrailwaysnavigator.route_overview.transfers": "%s Пересадок", + "gui.createrailwaysnavigator.route_overview.cancelled": "ХкасОванО", + "gui.createrailwaysnavigator.saved_routes.saved_route": "Збережений маршрут", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "Прибуття", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "ХкасОванО", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "Затримка приблизно %s хвилин", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Інформація про %s: ЌоК поїзд йуНО скасОванО", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Інформація про %s: Затримка приблизно %s хвилин", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ", скасОвуванО сьогодні. Перепрошуємо Са незручності.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ", сьогодні приблизно %s хвилин затримки.", + "block.createrailwaysnavigator.advanced_display.ber.reason": "Причина: ", + "gui.createrailwaysnavigator.route_widget.show_details": "Показати деталі", + "gui.createrailwaysnavigator.route_widget.save": "Зберегти", + "gui.createrailwaysnavigator.route_widget.remove": "Вилучити", + "gui.createrailwaysnavigator.route_widget.share": "Поділитися...", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "В ПинуНОПу", + "gui.createrailwaysnavigator.saved_routes.today": "Сьогодні", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "Завтра", + "gui.createrailwaysnavigator.saved_routes.in_days": "За %s дні", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "Показати деталі", + "gui.createrailwaysnavigator.saved_route_widget.share": "Поділитися...", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "Відображати сповіщення", + "gui.createrailwaysnavigator.train_status.unknown_delay": "Затримка в операціях", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "Затримка в попередній поїздці", + "gui.createrailwaysnavigator.train_status.red_signal": "Червоний сигнаН", + "gui.createrailwaysnavigator.train_status.priority_other_train": "Пріоритет іншого поїзду", + "gui.createrailwaysnavigator.train_status.delay_other_train": "Затримка іншого поїзду", + "gui.createrailwaysnavigator.train_status.track_closed": "Маршрут перекрито", + "gui.createrailwaysnavigator.train_status.staff_shortage": "Недостатньо персоналу", + "gui.createrailwaysnavigator.train_status.operational_disruption": "Операційний перерив", + "gui.createrailwaysnavigator.train_status.special_trip": "Спеціальна поїздка", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "Зберегти маршрут", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "Видалити маршрут", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "Показувати спливаюче вікно", + "gui.createrailwaysnavigator.navigator.my_profile": "Мій профіль", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "Деякі маршрути можуть бути нопОвниПи айО но запропоновані взагалі, оскільки навігатор ще но ініціалізував усі потяги.", + "gui.createrailwaysnavigator.global_settings.train_line.title": "Залізничні лінії", + "gui.createrailwaysnavigator.global_settings.train_line.color": "Вибір кольору", + "gui.createrailwaysnavigator.global_settings.train_line.description": "Створюйте залізничні лінії для групування поїздів разом, перезаписувати їх наСви та налаштовувати інші параметри для кожної лінії. (Можна присвоїти потягам тільки в розкладі)" +} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json index ec0cfa8b..f76ebaf8 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/zh_cn.json @@ -1,58 +1,53 @@ { "advancement.createrailwaysnavigator.navigator": "感谢您的乘坐", - "advancement.createrailwaysnavigator.navigator.description": "制作一个导航仪,用于搜索从一个车站到另一个车站的列车路线。", + "advancement.createrailwaysnavigator.navigator.description": "制作一个导航仪,用于搜索从一个车站到另一个车站的列车路线。", "advancement.createrailwaysnavigator.advanced_display": "还不够清晰", "advancement.createrailwaysnavigator.advanced_display.description": "升级您的显示器以显示更多信息,甚至可以将它们放置在您的列车上。", - - "itemGroup.createrailwaysnavigator.tab": "机械动力:铁路导航", - "item.createrailwaysnavigator.navigator": "铁路导航仪", - "item.createrailwaysnavigator.navigator.tooltip.summary": "_察航䝪_可以显示可能的_列车路线_,以及_经停站_、_实时信息_等。", - + "item.createrailwaysnavigator.navigator.tooltip.summary": "_察航䝪_可以显示可能的_列车路线_,以及_经停站_、_实时信息_等。", "block.createrailwaysnavigator.train_station_clock": "车站时钟", "block.createrailwaysnavigator.advanced_display_block": "高级显示器方块", - "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", + "block.createrailwaysnavigator.advanced_display_block.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", "block.createrailwaysnavigator.advanced_display_block.tooltip.condition1": "用扳手右击时", - "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "打开菜单以_配置_显示器。", + "block.createrailwaysnavigator.advanced_display_block.tooltip.behaviour1": "打开菜单以_配置_显示器。", "block.createrailwaysnavigator.advanced_display": "高级显示器", - "block.createrailwaysnavigator.advanced_display.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", + "block.createrailwaysnavigator.advanced_display.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", "block.createrailwaysnavigator.advanced_display.tooltip.condition1": "用扳手右击时", - "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "打开菜单以_配置_显示器。", + "block.createrailwaysnavigator.advanced_display.tooltip.behaviour1": "打开菜单以_配置_显示器。", "block.createrailwaysnavigator.advanced_display_small": "小型高级显示器", - "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", + "block.createrailwaysnavigator.advanced_display_small.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", "block.createrailwaysnavigator.advanced_display_small.tooltip.condition1": "用扳手右击时", - "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "打开菜单以_配置_显示器。", + "block.createrailwaysnavigator.advanced_display_small.tooltip.behaviour1": "打开菜单以_配置_显示器。", "block.createrailwaysnavigator.advanced_display_panel": "高级显示面板", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", "block.createrailwaysnavigator.advanced_display_panel.tooltip.condition1": "用扳手右击时", - "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "打开菜单以_配置_显示器。", + "block.createrailwaysnavigator.advanced_display_panel.tooltip.behaviour1": "打开菜单以_配置_显示器。", "block.createrailwaysnavigator.advanced_display_half_panel": "高级半显示面板", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.condition1": "用扳手右击时", - "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "打开菜单以_配置_显示器。", + "block.createrailwaysnavigator.advanced_display_half_panel.tooltip.behaviour1": "打开菜单以_配置_显示器。", "block.createrailwaysnavigator.advanced_display_sloped": "斜面高级显示器", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.summary": "使用在_列车上_,可以展示_列车目的地_或_䚘厢俥息_, 也可以放置在_车站_当作_站台显示器_, 比普通显示器显示更多信息。", "block.createrailwaysnavigator.advanced_display_sloped.tooltip.condition1": "用扳手右击时", - "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "打开菜单以_配置_显示器。", - + "block.createrailwaysnavigator.advanced_display_sloped.tooltip.behaviour1": "打开菜单以_配置_显示器。", + "block.createrailwaysnavigator.advanced_display_slab": "Advanced Display Slab", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.summary": "将其放置在_列车_结构中,用于显示_列车目的地_或_䚘厢俥息_,也可以在_列车站_用作改进版本的_站台信息_显示器,能够显示比普通显示器更多的信息。", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.condition1": "用扳手右击时", + "block.createrailwaysnavigator.advanced_display_slab.tooltip.behaviour1": "打开一个用于_配置显示器_的菜单", "block.createrailwaysnavigator.advanced_display.ber.not_in_service": "未启用", - "category.createrailwaysnavigator.crn": "机械动力:铁路导航", "key.createrailwaysnavigator.route_overlay_options": "显示路线覆盖选项", - "enum.createrailwaysnavigator.overlay_position": "显示位置", "enum.createrailwaysnavigator.overlay_position.info.top_left": "左上角", "enum.createrailwaysnavigator.overlay_position.info.top_right": "右上角", "enum.createrailwaysnavigator.overlay_position.info.bottom_left": "左下角", "enum.createrailwaysnavigator.overlay_position.info.bottom_right": "右下角", - "gui.createrailwaysnavigator.loading.title": "正在从服务器下载数据...", - "gui.createrailwaysnavigator.overlay_settings.title": "路线覆盖设置", "gui.createrailwaysnavigator.route_overlay_settings.show_details": "显示详细信息", "gui.createrailwaysnavigator.route_overlay_settings.unpin": "移除路线覆盖", "gui.createrailwaysnavigator.route_overlay_settings.narrator.on": "讲述人通知开启", - "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "讲述人通知关闭", + "gui.createrailwaysnavigator.route_overlay_settings.narrator.off": "讲述人通知关闭", "gui.createrailwaysnavigator.route_overlay_settings.notifications.on": "开启通知", "gui.createrailwaysnavigator.route_overlay_settings.notifications.off": "关闭通知", "gui.createrailwaysnavigator.route_overlay_settings.scale": "GUI 大小", @@ -60,7 +55,6 @@ "gui.createrailwaysnavigator.route_overlay_settings.narrator.description": "讲述人会播报您旅途中的重要事件,例如下一站、换乘等。", "gui.createrailwaysnavigator.route_overlay_settings.notifications": "通知", "gui.createrailwaysnavigator.route_overlay_settings.notifications.description": "在旅途中接收关于重要事件的弹窗通知,例如下一站、换乘等。", - "gui.createrailwaysnavigator.common.expand": "显示详情", "gui.createrailwaysnavigator.common.collapse": "隐藏详情", "gui.createrailwaysnavigator.common.go_back": "返回", @@ -72,7 +66,9 @@ "gui.createrailwaysnavigator.common.search": "搜索", "gui.createrailwaysnavigator.common.auto": "自动", "gui.createrailwaysnavigator.common.server_error": "执行任务时出现问题,请查看控制台以获取更多信息。", - + "gui.createrailwaysnavigator.common.delete": "删除", + "gui.createrailwaysnavigator.common.add": "添加", + "gui.createrailwaysnavigator.common.help": "寻求帮助", "gui.createrailwaysnavigator.navigator.title": "机械动力:铁路导航", "gui.createrailwaysnavigator.navigator.no_connections": "未找到路线。", "gui.createrailwaysnavigator.navigator.not_searched": "尚未搜索。", @@ -87,14 +83,12 @@ "gui.createrailwaysnavigator.navigator.search_settings.tooltip": "搜索设置", "gui.createrailwaysnavigator.navigator.search.tooltip": "搜索", "gui.createrailwaysnavigator.navigator.location.tooltip": "定位最近的车站", + "gui.createrailwaysnavigator.navigator.refresh.tooltip": "刷新", "gui.createrailwaysnavigator.navigator.switch.tooltip": "交换地点", - "gui.createrailwaysnavigator.route_details.title": "路线详情", "gui.createrailwaysnavigator.route_details.departure": "出发时间", "gui.createrailwaysnavigator.route_details.next_transfer_time": "换乘时间", "gui.createrailwaysnavigator.route_details.transfer": "换乘", - "gui.createrailwaysnavigator.route_details.save_route": "保存路线", - "gui.createrailwaysnavigator.route_overview.title": "路线详情", "gui.createrailwaysnavigator.route_overview.journey_begins": "您的旅程开始了!%s到%s,出发时间%s", "gui.createrailwaysnavigator.route_overview.journey_begins_with_platform": "您的旅程开始了!%s到%s,出发时间%s,在%s站台乘车", @@ -124,7 +118,7 @@ "gui.createrailwaysnavigator.route_overview.notification.platform_changed.title": "站台已变更!", "gui.createrailwaysnavigator.route_overview.notification.platform_changed": "您在%s的列车将从%s号站台出发。", "gui.createrailwaysnavigator.route_overview.notification.train_delayed.title": "%s:延误%s", - "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "原定%2$s,现将于%1$s抵达%3$s", + "gui.createrailwaysnavigator.route_overview.notification.train_delayed": "原定%2$s,现将于%1$s抵达%3$s", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled.title": "%s:列车取消", "gui.createrailwaysnavigator.route_overview.notification.train_cancelled": "前往%2$s的%1$s已取消。", "gui.createrailwaysnavigator.route_overview.notification.transfer.title": "即将换乘", @@ -137,7 +131,6 @@ "gui.createrailwaysnavigator.route_overview.notification.journey_completed.title": "您已抵达目的地!", "gui.createrailwaysnavigator.route_overview.notification.journey_completed": "感谢您的乘坐,祝您旅途愉快", "gui.createrailwaysnavigator.route_overview.date": "珏%s天,%s", - "gui.createrailwaysnavigator.global_settings.title": "全局设定", "gui.createrailwaysnavigator.global_settings.option.tooltip": "点击编辑", "gui.createrailwaysnavigator.global_settings.option_alias.title": "车站标签", @@ -148,13 +141,12 @@ "gui.createrailwaysnavigator.global_settings.train_group.description": "将列车加入列车分类(例如,区域服务、长途服务等)以方便管理,乘客可以决定要使用哪些列车分类。", "gui.createrailwaysnavigator.global_settings.train_blacklist.title": "列车黑名单", "gui.createrailwaysnavigator.global_settings.train_blacklist.description": "排除列车,例如货运列车、专列等,以便在路线建议中不显示它们。", - - "gui.createrailwaysnavigator.station_tags.title": "车站标签设定", "gui.createrailwaysnavigator.station_tags.summary": "包含%s个车站", "gui.createrailwaysnavigator.station_tags.editor": "最后编辑者:%s,于%s", "gui.createrailwaysnavigator.station_tags.add.tooltip": "创建新条目", "gui.createrailwaysnavigator.station_tags.delete_alias.tooltip": "删除标签", "gui.createrailwaysnavigator.station_tags.delete_station.tooltip": "移除车站", + "gui.createrailwaysnavigator.station_tags.modify_platform.tooltip": "点击更改", "gui.createrailwaysnavigator.station_tags.add_station.tooltip": "添加车站", "gui.createrailwaysnavigator.station_tags.hint.station_name": "车站名称", "gui.createrailwaysnavigator.station_tags.hint.platform": "站台", @@ -165,15 +157,12 @@ "gui.createrailwaysnavigator.train_group_settings.delete_alias.tooltip": "删除分类", "gui.createrailwaysnavigator.train_group_settings.delete_station.tooltip": "移除列车", "gui.createrailwaysnavigator.train_group_settings.add_station.tooltip": "添加列车", - "gui.createrailwaysnavigator.blacklist.title": "车站黑名单", "gui.createrailwaysnavigator.blacklist.add.tooltip": "加入黑名单", "gui.createrailwaysnavigator.blacklist.delete.tooltip": "移除黑名单", - "gui.createrailwaysnavigator.train_blacklist.title": "列车黑名单", "gui.createrailwaysnavigator.train_blacklist.add.tooltip": "加入黑名单", "gui.createrailwaysnavigator.train_blacklist.delete.tooltip": "移除黑名单", - "gui.createrailwaysnavigator.search_settings.title": "搜索设定", "gui.createrailwaysnavigator.search_settings.transfer_time": "最短换乘时间", "gui.createrailwaysnavigator.search_settings.transfer_time.description": "换乘需要的最短可用时间。(1 小时 ≈ 50 秒现实时间)", @@ -183,29 +172,24 @@ "gui.createrailwaysnavigator.search_settings.train_groups.overview.all": "全部", "gui.createrailwaysnavigator.search_settings.train_groups.overview.none": "无", "gui.createrailwaysnavigator.search_settings.train_groups.tooltip.reset": "重置筛选项", - "gui.createrailwaysnavigator.new_text_entry.add.tooltip": "添加", - "gui.createrailwaysnavigator.time": "时间:%s", "gui.createrailwaysnavigator.time.now": "现在", "gui.createrailwaysnavigator.time_format.dhm": "%s夊%s小时%s分钟", "gui.createrailwaysnavigator.time_format.hm": "%s小时%s分钟", "gui.createrailwaysnavigator.time_format.m": "%s分钟", - "gui.createrailwaysnavigator.platform": "站台", "gui.createrailwaysnavigator.departure": "出发", "gui.createrailwaysnavigator.destination": "目的地", "gui.createrailwaysnavigator.line": "衯线", "gui.createrailwaysnavigator.following_trains": "后续车次:", "gui.createrailwaysnavigator.via": "经停", - "gui.createrailwaysnavigator.advanced_display_settings.title": "鍘级昞示莞罎", "gui.createrailwaysnavigator.advanced_display_settings.display_type": "显示器类型", "gui.createrailwaysnavigator.advanced_display_settings.display_type.description": "决定显示的信息。", "gui.createrailwaysnavigator.advanced_display_settings.info_type": "信息类型", "gui.createrailwaysnavigator.advanced_display_settings.info_type.description": "决定信息的详细程度。", "gui.createrailwaysnavigator.advanced_display_settings.double_sided": "双面显示", - "enum.createrailwaysnavigator.display_info_type": "信息类型", "enum.createrailwaysnavigator.display_info_type.description": "决定显示器上应显示多少信息。", "enum.createrailwaysnavigator.display_info_type.simple": "简洁", @@ -222,26 +206,110 @@ "enum.createrailwaysnavigator.display_type.info.passenger_information": "代表适用于列车内部的显示器。这种显示器将显示下一站、开门方向以及(如果选择)列车速度和路线概览。", "enum.createrailwaysnavigator.display_type.platform": "站台显示", "enum.createrailwaysnavigator.display_type.info.platform": "这种显示应在车站站台上使用,显示后续车次以及(如果选择)其他详细信息。不能在列车上使用!", - "enum.createrailwaysnavigator.side": "显示方向", "enum.createrailwaysnavigator.side.description": "决定信息应呈现在方块的哪一面。", "enum.createrailwaysnavigator.side.front": "正面", "enum.createrailwaysnavigator.side.info.front": "信息将仅在正面显示(默认行为)。", "enum.createrailwaysnavigator.side.both": "双面", "enum.createrailwaysnavigator.side.info.both": "信息将在双面显示。", - "enum.createrailwaysnavigator.time_display": "时间显示", "enum.createrailwaysnavigator.time_display.description": "决定时间的显示方式。", - "enum.createrailwaysnavigator.time_display.abs": "ABS", + "enum.createrailwaysnavigator.time_display.abs": "自动区间信号系统", "enum.createrailwaysnavigator.time_display.info.abs": "ABS(绝对时间、absolute)", - "enum.createrailwaysnavigator.time_display.eta": "ETA", + "enum.createrailwaysnavigator.time_display.eta": "预计到达时间", "enum.createrailwaysnavigator.time_display.info.eta": "ETA(预计到达时间、estimated time of arrival)", - "create.display_source.advanced_display": "鍘级昞示", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width": "列车名称列宽", "gui.createrailwaysnavigator.display_source.advanced_display.train_name_width.description": "以像素为单位。(默认:16)", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width": "站台列宽", "gui.createrailwaysnavigator.display_source.advanced_display.platform_width.description": "以方块像素为单位。(默认:自动)", - - "createrailwaysnavigator.moin": "moin" + "gui.createrailwaysnavigator.section_settings.title": "区段设置", + "gui.createrailwaysnavigator.section_settings.train_groups": "分配列车编组", + "gui.createrailwaysnavigator.section_settings.train_lines": "分配列车线路", + "gui.createrailwaysnavigator.section_settings.include_previous_station": "包含下一个区段的起始", + "gui.createrailwaysnavigator.section_settings.usable": "可通行", + "gui.createrailwaysnavigator.section_settings.none": "(无)", + "createrailwaysnavigator.schedule.condition.dynamic_delay": "动态延迟", + "createrailwaysnavigator.schedule.condition.dynamic_delay.min_duration": "最低持续时间", + "createrailwaysnavigator.schedule.condition.dynamic_delay.title": "等待:%s至%s", + "createrailwaysnavigator.schedule.condition.dynamic_delay.at_least": "或至少%s", + "createrailwaysnavigator.schedule.instruction.travel_section": "新的时刻表区段", + "createrailwaysnavigator.schedule.instruction.travel_section.description": "从一个新的时刻表区段开始", + "createrailwaysnavigator.schedule.instruction.travel_section.configure": "配置…", + "createrailwaysnavigator.schedule.instruction.travel_section.train_group": " -设定列车组:", + "createrailwaysnavigator.schedule.instruction.travel_section.train_line": " -设定列车路线:", + "createrailwaysnavigator.schedule.instruction.travel_section.include_previous_station": " -包含下一个区段的开始:", + "createrailwaysnavigator.schedule.instruction.travel_section.usable": " -可通行:", + "createrailwaysnavigator.schedule.instruction.reset_timings": "重置时间", + "display.createrailwaysnavigator.train_destination.simple": "紧凑", + "display.createrailwaysnavigator.train_destination.extended": "扩展", + "display.createrailwaysnavigator.train_destination.detailed": "详细", + "display.createrailwaysnavigator.passenger_information.running_text": "滚动文本", + "display.createrailwaysnavigator.passenger_information.detailed_with_schedule": "详细时刻表", + "display.createrailwaysnavigator.platform.running_text": "滚动文本", + "display.createrailwaysnavigator.platform.table": "襨栟", + "display.createrailwaysnavigator.platform.focus": "焦点", + "gui.createrailwaysnavigator.saved_routes.title": "保存的路线", + "gui.createrailwaysnavigator.schedule_board.title": "时刻信息板", + "gui.createrailwaysnavigator.station_tags.title": "车站标签设定", + "gui.createrailwaysnavigator.journey_info.title": "旅程信息", + "gui.createrailwaysnavigator.journey_info.date": "珏%s日", + "gui.createrailwaysnavigator.journey_info.train": "%s%s)到%s", + "gui.createrailwaysnavigator.color_picker.custom": "自定义…", + "gui.createrailwaysnavigator.color_picker.no_color": "无色", + "gui.createrailwaysnavigator.schedule_board.view_details": "查看详情", + "gui.createrailwaysnavigator.schedule_board.train_from": "从%s", + "gui.createrailwaysnavigator.search_options.departure_in": "距发车还有", + "gui.createrailwaysnavigator.search_options.train_groups": "列车编组", + "gui.createrailwaysnavigator.search_options.transfer_time": "换乘时间", + "gui.createrailwaysnavigator.search_options.advanced_options": "高级选项", + "gui.createrailwaysnavigator.search_options.train_groups.all": "全部", + "gui.createrailwaysnavigator.search_options.train_groups.excluded": "%s 不包括在内", + "gui.createrailwaysnavigator.search_options.saved_routes.all": "全部", + "gui.createrailwaysnavigator.search_options.saved_routes.excluded": "%s 不包括在内", + "gui.createrailwaysnavigator.empty_list": "列表为空", + "gui.createrailwaysnavigator.new_entry.add": "添加新条目", + "gui.createrailwaysnavigator.new_entry.new": "新条目:", + "gui.createrailwaysnavigator.saved_routes.saved": "%s已保存", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed.title": "时刻表已更改!", + "gui.createrailwaysnavigator.route_overview.notification.schedule_changed": "您保存的路线时间表已被更改,请检视导航器以了解相关更动。", + "gui.createrailwaysnavigator.route_overview.transfers": "%s换乘", + "gui.createrailwaysnavigator.route_overview.cancelled": "已取消", + "gui.createrailwaysnavigator.saved_routes.saved_route": "保存的路线", + "block.createrailwaysnavigator.advanced_display.ber.arrival": "到螞", + "block.createrailwaysnavigator.advanced_display.ber.cancelled": "已取消", + "block.createrailwaysnavigator.advanced_display.ber.delayed": "坜误續%s分钟", + "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": " 关于列车的信息:%s 列车已停运。", + "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": " 关于列车的信息:%s 预计延误约%s分钟。", + "block.createrailwaysnavigator.advanced_display.ber.cancelled2": ",今日服务已取消,给您带来的不便,敬请谅解。", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": ",今日延误约%s分钟", + "block.createrailwaysnavigator.advanced_display.ber.reason": "原因:", + "gui.createrailwaysnavigator.route_widget.show_details": "显示详情", + "gui.createrailwaysnavigator.route_widget.save": "保存", + "gui.createrailwaysnavigator.route_widget.remove": "移除", + "gui.createrailwaysnavigator.route_widget.share": "分享…", + "gui.createrailwaysnavigator.saved_routes.in_the_past": "旧前", + "gui.createrailwaysnavigator.saved_routes.today": "今日", + "gui.createrailwaysnavigator.saved_routes.tomorrow": "明日", + "gui.createrailwaysnavigator.saved_routes.in_days": "在%s日内", + "gui.createrailwaysnavigator.saved_route_widget.show_details": "显示详情", + "gui.createrailwaysnavigator.saved_route_widget.share": "分享…", + "gui.createrailwaysnavigator.saved_route_widget.notifications": "显示通知", + "gui.createrailwaysnavigator.train_status.unknown_delay": "操作延误", + "gui.createrailwaysnavigator.train_status.delay_previous_journey": "前一班次延误", + "gui.createrailwaysnavigator.train_status.red_signal": "红灯信号", + "gui.createrailwaysnavigator.train_status.priority_other_train": "另一列车优先权", + "gui.createrailwaysnavigator.train_status.delay_other_train": "另一列车延误", + "gui.createrailwaysnavigator.train_status.track_closed": "轨道封闭", + "gui.createrailwaysnavigator.train_status.staff_shortage": "人员短缺", + "gui.createrailwaysnavigator.train_status.operational_disruption": "运营中断", + "gui.createrailwaysnavigator.train_status.special_trip": "特别行程", + "gui.createrailwaysnavigator.route_details.save_route.tooltip": "保存路线", + "gui.createrailwaysnavigator.route_details.remove_route.tooltip": "移除路线", + "gui.createrailwaysnavigator.route_details.show_popup.tooltip": "显示弹窗", + "gui.createrailwaysnavigator.navigator.my_profile": "个人资料", + "gui.createrailwaysnavigator.navigator.train_initialization_warning": "部分路线可能尚未完成,或未被推荐,原因是 机械动力:铁路导航 的导航系统尚未初始化所有列车。", + "gui.createrailwaysnavigator.global_settings.train_line.title": "列车路线", + "gui.createrailwaysnavigator.global_settings.train_line.color": "选择颜色", + "gui.createrailwaysnavigator.global_settings.train_line.description": "创建列车路线以便将不同列车分组,覆盖其显示名称并调整各路线的其他设置。(仅适用于已在调度中的列车)" } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block.json index 0751c70d..cd4d7d74 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 0], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "south"}, "west": {"uv": [8, 0, 0, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [0, 0, 8, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3", "cullface": "north"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block_double.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block_double.json index c5e5382b..e1a37839 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block_double.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_block_double.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 0], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "south", "tintindex": 0}, "west": {"uv": [8, 0, 0, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [0, 0, 8, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3", "cullface": "north"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_cen.json index d3406276..ce9c2524 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_cen.json @@ -2,8 +2,9 @@ "credit": "Made with Blockbench", "texture_size": [64, 64], "textures": { - "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", + "4": "createrailwaysnavigator:block/advanced_display", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 3], "to": [16, 16, 13], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#4", "tintindex": 0}, "east": {"uv": [8, 0, 13, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2"}, "west": {"uv": [13, 0, 8, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 3], + "to": [16, 16, 13], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_cen.json index 639116d2..014e2570 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_cen.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 3], "to": [16, 16, 13], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [8, 0, 13, 8], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "west": {"uv": [13, 0, 8, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 3], + "to": [16, 16, 13], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_neg.json index 4740e249..492c994d 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_neg.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 0], "to": [16, 16, 8], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [8, 0, 13, 8], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "west": {"uv": [13, 0, 8, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 16, 8], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_pos.json index a28324df..53973f43 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_double_pos.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 8], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [8, 0, 13, 8], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "west": {"uv": [13, 0, 8, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 8], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_cen.json index 6297c662..0de1f964 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_cen.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 6.5], "to": [16, 12, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 6.5], + "to": [16, 12, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_neg.json index fcaa1bda..94ff9fef 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_neg.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 0], "to": [16, 12, 3], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 0], + "to": [16, 12, 3], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_pos.json index 0236a56c..6ac5c0d3 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_cen_pos.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 13], "to": [16, 12, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 13], + "to": [16, 12, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_cen.json index 29a07ba8..dfc11fe5 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_cen.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 6.5], "to": [16, 12, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 6.5], + "to": [16, 12, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_neg.json index b3b784d7..c7ee74be 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_neg.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 0], "to": [16, 12, 3], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 0], + "to": [16, 12, 3], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_pos.json index 047f02c3..83238922 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_cen_pos.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 13], "to": [16, 12, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 13], + "to": [16, 12, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_cen.json index 067a2ca3..818c6e64 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_cen.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 6.5], "to": [16, 8, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 6.5], + "to": [16, 8, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_neg.json index 7f1fba1a..5d629482 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_neg.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 0], "to": [16, 8, 3], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 8, 3], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_pos.json index 0fd706c7..dd44ee88 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_neg_pos.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 13], "to": [16, 8, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 13], + "to": [16, 8, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_cen.json index 55b4de20..bc204008 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_cen.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 6.5], "to": [16, 16, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 6.5], + "to": [16, 16, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_neg.json index 705a87a2..06c91448 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_neg.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 0], "to": [16, 16, 3], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 0], + "to": [16, 16, 3], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_pos.json index 665b098c..075caf35 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_double_pos_pos.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 13], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 13], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_cen.json index cd5d0169..3a04e959 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_cen.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 6.5], "to": [16, 8, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 6.5], + "to": [16, 8, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_neg.json index 949021b4..9ad74f5e 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_neg.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 0], "to": [16, 8, 3], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 8, 3], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_pos.json index bdfe2032..8df6284d 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_neg_pos.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 13], "to": [16, 8, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 13], + "to": [16, 8, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_cen.json index 903787e0..4a63dba9 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_cen.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 6.5], "to": [16, 16, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 6.5], + "to": [16, 16, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_neg.json index 74561ab5..7122ec18 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_neg.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 0], "to": [16, 16, 3], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 0], + "to": [16, 16, 3], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_pos.json index ffbb04ce..a19e3dd9 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_half_panel_pos_pos.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 13], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [13, 8, 14.5, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 8, 13, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 13], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_neg.json index ad91264e..4003926d 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_neg.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 0], "to": [16, 16, 8], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [8, 0, 13, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2"}, "west": {"uv": [13, 0, 8, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 16, 8], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_cen.json index 3dd79f50..f5bcf658 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_cen.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 6.5], "to": [16, 16, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [13, 0, 14.5, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 0, 13, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 6.5], + "to": [16, 16, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_cen.json index 4186fbd3..51c9d9ae 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_cen.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 6.5], "to": [16, 16, 9.5], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [13, 0, 14.5, 8], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "west": {"uv": [14.5, 0, 13, 8], "texture": "#2"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 0, 6.5], + "to": [16, 16, 9.5], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_neg.json index da9bfa45..3c753702 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_neg.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 0], "to": [16, 16, 3], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [13, 0, 14.5, 8], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "west": {"uv": [14.5, 0, 13, 8], "texture": "#2"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 16, 3], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_pos.json index c4f23b7e..a9a25b16 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_double_pos.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,22 @@ "from": [0, 0, 13], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [13, 0, 14.5, 8], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "west": {"uv": [14.5, 0, 13, 8], "texture": "#2"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 0, 13], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_neg.json index 25188414..acd3b57f 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_neg.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 0], "to": [16, 16, 3], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [13, 0, 14.5, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 0, 13, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 16, 3], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_pos.json index 66b652e9..4b8da61e 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_panel_pos.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 13], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [13, 0, 14.5, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "south"}, "west": {"uv": [14.5, 0, 13, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [13, 0, 14.5, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 13], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_pos.json index e2f69e76..5923fd09 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_pos.json @@ -4,6 +4,7 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display", "2": "createrailwaysnavigator:block/advanced_display_back", + "3": "createrailwaysnavigator:block/advanced_display_border", "particle": "createrailwaysnavigator:block/advanced_display" }, "elements": [ @@ -11,13 +12,21 @@ "from": [0, 0, 8], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 16], "texture": "#1"}, + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "tintindex": 0}, "east": {"uv": [8, 0, 13, 8], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 0, 8, 8], "texture": "#2"}, "west": {"uv": [13, 0, 8, 8], "texture": "#2", "cullface": "west"}, "up": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "up"}, "down": {"uv": [8, 0, 13, 8], "rotation": 90, "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 8], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json index 9c33aad6..94464635 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_cen.json @@ -4,7 +4,8 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -12,13 +13,22 @@ "to": [16, 12, 16], "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, -8]}, "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, "down": {"uv": [0, 0, 8, 8], "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 4, 0], + "to": [16, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4", "cullface": "north"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json index 67a2fbe4..a2ea8ef6 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_cen.json @@ -4,7 +4,8 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -12,13 +13,23 @@ "to": [16, 12, 16], "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, -8]}, "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, "down": {"uv": [0, 0, 8, 8], "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 4, 0], + "to": [16, 12, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 0, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4", "cullface": "north"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json index e566e226..937cd018 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_neg.json @@ -4,7 +4,8 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -12,13 +13,23 @@ "to": [16, 8, 16], "rotation": {"angle": 0, "axis": "y", "origin": [0, -4, -8]}, "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, "down": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 8, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, -4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4", "cullface": "north"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json index 146c2ec8..0838d72f 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_double_pos.json @@ -4,7 +4,8 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -12,13 +13,23 @@ "to": [16, 16, 16], "rotation": {"angle": 0, "axis": "y", "origin": [0, 4, -8]}, "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "up"}, "down": {"uv": [0, 0, 8, 8], "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 8, 0], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4", "cullface": "north"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json index 37d2a568..536f8323 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_neg.json @@ -4,7 +4,8 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -12,13 +13,22 @@ "to": [16, 8, 16], "rotation": {"angle": 0, "axis": "y", "origin": [0, -4, -8]}, "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "texture": "#2"}, "down": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 8, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, -4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4", "cullface": "north"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json index 6bf910ae..6c7d7620 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_slab_pos.json @@ -4,7 +4,8 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -12,13 +13,22 @@ "to": [16, 16, 16], "rotation": {"angle": 0, "axis": "y", "origin": [0, 4, -8]}, "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "north", "tintindex": 0}, "east": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [8, 8, 0, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 0, 8, 8], "texture": "#2", "cullface": "up"}, "down": {"uv": [0, 0, 8, 8], "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 8, 0], + "to": [16, 16, 16], + "rotation": {"angle": 0, "axis": "y", "origin": [0, 4, -8]}, + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4", "cullface": "north"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_sloped.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_sloped.json index f0bf0054..fc429cda 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_sloped.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_sloped.json @@ -4,7 +4,8 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display_small", "2": "createrailwaysnavigator:block/advanced_display_back", - "particle": "createrailwaysnavigator:block/advanced_display" + "3": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -48,24 +49,42 @@ "faces": { "north": {"uv": [0, 8, 8, 12], "texture": "#2"}, "east": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#1", "tintindex": 0}, "west": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, "up": {"uv": [0, 0, 8, 0.5], "texture": "#2"}, "down": {"uv": [0, 0, 8, 0.5], "texture": "#2"} } }, + { + "name": "overlay", + "from": [0, 7, 10], + "to": [16, 16, 11], + "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 8, 12]}, + "faces": { + "south": {"uv": [0, 0, 16, 8], "texture": "#3"} + } + }, { "from": [0, 7, 5], "to": [16, 16, 6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 8, 4]}, "faces": { - "north": {"uv": [0, 0, 15.8, 8], "texture": "#1"}, + "north": {"uv": [0, 0, 15.8, 8], "texture": "#1", "tintindex": 0}, "east": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2"}, "west": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, "up": {"uv": [0, 0, 8, 0.5], "texture": "#2"}, "down": {"uv": [0, 0, 8, 0.5], "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 7, 5], + "to": [16, 16, 6], + "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 8, 4]}, + "faces": { + "north": {"uv": [0, 0, 15.8, 8], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_cen.json index dc8e01b8..aaa52c9a 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_cen.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 4], "to": [16, 12, 12], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 4], + "to": [16, 12, 12], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_neg.json index 2145cfef..629f67f7 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_neg.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 0], "to": [16, 12, 8], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 0], + "to": [16, 12, 8], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_pos.json index 02e5cb55..4d825f53 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_cen_pos.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 8], "to": [16, 12, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 8], + "to": [16, 12, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_cen.json index e8a11ee4..8f831bfb 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_cen.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 4], "to": [16, 12, 12], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 4], + "to": [16, 12, 12], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_neg.json index 220b57cc..3c227049 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_neg.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 0], "to": [16, 12, 8], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 0], + "to": [16, 12, 8], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_pos.json index ff3f3ddd..81a59914 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_cen_pos.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 4, 8], "to": [16, 12, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 4, 8], + "to": [16, 12, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_cen.json index 3b37befa..221c945e 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_cen.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 4], "to": [16, 8, 12], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 4], + "to": [16, 8, 12], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_neg.json index 1ca936e5..a733eb68 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_neg.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 0], "to": [16, 8, 8], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 8, 8], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_pos.json index ab2d6e1f..e1296df0 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_neg_pos.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 8], "to": [16, 8, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 8], + "to": [16, 8, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_cen.json index 51ec6752..bbceeb36 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_cen.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 4], "to": [16, 16, 12], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 4], + "to": [16, 16, 12], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_neg.json index e2a71ee3..bc423a06 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_neg.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 0], "to": [16, 16, 8], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 0], + "to": [16, 16, 8], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_pos.json index 1d21826b..f999adc9 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_double_pos_pos.json @@ -4,20 +4,30 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 8], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#3", "cullface": "south", "tintindex": 0}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 8], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_cen.json index f69ec022..16533a3f 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_cen.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 4], "to": [16, 8, 12], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 4], + "to": [16, 8, 12], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_neg.json index 68cb9b74..16ff955b 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_neg.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 0], "to": [16, 8, 8], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 0], + "to": [16, 8, 8], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_pos.json index 8a095f5c..0eb13425 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_neg_pos.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 0, 8], "to": [16, 8, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 0, 8], + "to": [16, 8, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_cen.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_cen.json index f4d2d8e6..13d86ee7 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_cen.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_cen.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 4], "to": [16, 16, 12], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 4], + "to": [16, 16, 12], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_neg.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_neg.json index 19c6f579..5f526d32 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_neg.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_neg.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 0], "to": [16, 16, 8], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 0], + "to": [16, 16, 8], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_pos.json b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_pos.json index ab7ed260..c83a68f9 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_pos.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/block/advanced_display_small_pos_pos.json @@ -4,20 +4,29 @@ "textures": { "2": "createrailwaysnavigator:block/advanced_display_back", "3": "createrailwaysnavigator:block/advanced_display_small", - "particle": "createrailwaysnavigator:block/advanced_display" + "4": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { "from": [0, 8, 8], "to": [16, 16, 16], "faces": { - "north": {"uv": [0, 0, 16, 8], "texture": "#3"}, + "north": {"uv": [0, 0, 16, 8], "texture": "#3", "tintindex": 0}, "east": {"uv": [8, 8, 12, 12], "texture": "#2", "cullface": "east"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "south"}, "west": {"uv": [12, 8, 8, 12], "texture": "#2", "cullface": "west"}, "up": {"uv": [0, 8, 8, 12], "texture": "#2"}, "down": {"uv": [0, 8, 8, 12], "texture": "#2", "cullface": "down"} } + }, + { + "name": "overlay", + "from": [0, 8, 8], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 8], "texture": "#4"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_sloped.json b/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_sloped.json index 07b19dce..fc429cda 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_sloped.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/models/item/advanced_display_sloped.json @@ -4,7 +4,8 @@ "textures": { "1": "createrailwaysnavigator:block/advanced_display_small", "2": "createrailwaysnavigator:block/advanced_display_back", - "particle": "createrailwaysnavigator:block/advanced_display_small_connected" + "3": "createrailwaysnavigator:block/advanced_display_small_border", + "particle": "createrailwaysnavigator:block/advanced_display_small_border" }, "elements": [ { @@ -38,7 +39,7 @@ "faces": { "east": {"uv": [1.375, 0, 6.625, 1], "texture": "#2"}, "west": {"uv": [1.375, 0, 6.625, 1], "texture": "#2"}, - "up": {"uv": [0, 1.375, 8, 6.625], "texture": "#2"} + "up": {"uv": [0, 1.375, 8, 6.625], "texture": "#2", "cullface": "up"} } }, { @@ -48,24 +49,42 @@ "faces": { "north": {"uv": [0, 8, 8, 12], "texture": "#2"}, "east": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, - "south": {"uv": [0, 0, 16, 8], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 8], "texture": "#1", "tintindex": 0}, "west": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, "up": {"uv": [0, 0, 8, 0.5], "texture": "#2"}, "down": {"uv": [0, 0, 8, 0.5], "texture": "#2"} } }, + { + "name": "overlay", + "from": [0, 7, 10], + "to": [16, 16, 11], + "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 8, 12]}, + "faces": { + "south": {"uv": [0, 0, 16, 8], "texture": "#3"} + } + }, { "from": [0, 7, 5], "to": [16, 16, 6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 8, 4]}, "faces": { - "north": {"uv": [0, 0, 15.8, 8], "texture": "#1"}, + "north": {"uv": [0, 0, 15.8, 8], "texture": "#1", "tintindex": 0}, "east": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, "south": {"uv": [0, 8, 8, 12], "texture": "#2"}, "west": {"uv": [0, 0, 0.5, 4], "texture": "#2"}, "up": {"uv": [0, 0, 8, 0.5], "texture": "#2"}, "down": {"uv": [0, 0, 8, 0.5], "texture": "#2"} } + }, + { + "name": "overlay", + "from": [0, 7, 5], + "to": [16, 16, 6], + "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 8, 4]}, + "faces": { + "north": {"uv": [0, 0, 15.8, 8], "texture": "#3"} + } } ], "display": { diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display.png index ae15e2bd780a00d38d35067da2ff02f6878dad1a..33891c50f33c359f915acd8314c97ecb94d31218 100644 GIT binary patch literal 4433 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 Ar~m)} diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display.xcf b/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display.xcf new file mode 100644 index 0000000000000000000000000000000000000000..78c754d887a499cc047cbe8c5d5993dabcb53d1a GIT binary patch literal 293721 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�naplnv~-nv9u^W*0%z@R1ai!y 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 E1BLp6jsO4v literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_border.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_border.png new file mode 100644 index 0000000000000000000000000000000000000000..da6d3996383539cc35d27f28833f8c19bfae1573 GIT binary patch literal 824 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 HmcjoA!CbP@ literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_connected.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_connected.png index 256bd1f07d4b79250a0968de5c19e6d92236ff1a..f4cd4803083af16b51b0fe8cbe7576b0ff11ff1a 100644 GIT binary patch literal 10508 zcmeHtcT^MG7Wafmk#0jkfruy;0x8rWBvOD!mEPrAd`gz5(yO@80#jx4!SL_1=GXSTmV9d!OIg`?vSmIcGwMw&q1PW?p6h z0N7Mjlym?90^ULZCI;}s%@enInvRX;m2tF-qejb>Y?@NoJ#I- zlSg`Wc4^Cj0+T5t{950|w*yxaH1ap88@Z#~y&k^uquvL-4=x@F6%%MI3LSUwnTb;E z8rd4hH69pSxKutc?_bP6kP}40Ibj)13}9X779*lKM4)>sGF4h~M)Cz`GJTH2Yf09{ zX8AeT)2y9p=;I3(KN`4a=KFrA@(w+jQc(Yv&6Z+~OE}D*Bk;y8GsNcYyTEOQE}zI} zTdH4O<3_fH4t_bZ(9UwJck0AIKth$82;{6sqmsYxjp91%=7RnN{``}dbll2)Y73-f zCc|U2@_gKyr$*TpS~^s*0$w@jW$uZE+ukiTo#@`R*q<{c`R8(y5+zF2RSU(Roxk;% z*J$F_3&ztkJhLo^YFAF*w?LC`d^S0kUddkR`9nAG((}^_pXlOc>)z6;h18o~RlE7! z6R`?;XyY{h-bZM@!u+-6!;yX8ZNfP=@~iK=2<~NB2-dirGW4+^S2D}~ld^T1lCD)z zB%0OW*7OgtSEdt{eYVpZynAj zCeSr2?x{7eKEazz@1*A)TM+-`H7gK|D5$oI1{hB^2U_u-Ma3Se15)U3o{M64{02E{ zskRWP>~=bk!99^L`bYvYv;V`P1hJY7W>swiIIH(aAmghPy>Nl!aN_&3NhvyNXOlDC zKG-L{93wkYBt8x>9lww2;U+Xlk7VVxpLv;;*MXVl^P(Fwm6=F?)gm*8Os{{>pRiK& zmR5C>3H2iJ=3Yr@W_g9H&+eU>vVC@6bp72U747+yN2-#=-_VVm&qZE+z{EJbYOKJ0 zzH=)7#k0FfR(H>CH5A;+iW!7t=iRa|bIlmAjW?^&C-5ug+G*;I@<`X6fYz`TF@&Da zJ^S%wW6HNk_eh)hi>=JHc`Eu(9vO{tOP@lIWaJ9ZakcDQt^9g~r+2Mx&MoxJG)Csl zXfJ=K&hq{|R|kU{<|(1XYqxBI-{b*>xu=dvvO*N!$|g!rd=qKR4|h-Ej#G!U364L6 zGIT5#qH6bFQadkm*`j6(FK_tp6q3aWHIUsYQ&WAc`@|DF+48g9L!s$HCvj|C_dEGl zGwvnrlMQz0*<%oM&`ndm#Ll!wNJgi@W6v1sdm@s@{N*JbhAbnB(e)Kfk=`>`L8h#{ ziiZd8b_WSpt#TE)FpW-!JF^;mY>|srVElNFgzg5jnvKsWQC2z zI$VWaYDTr|9Sx_hRzw>Kjm z^(B?G--9R|VbGJ!P%=yuW=!M*Y|f3oh|b6~kf9HJjKkQoMk-W!H*uZwZHnZjC3DIg zLSe|p{b%iS?Vy+8bk$dGRAsg)^i%w_AO$sWOsOU>N zomUo@gl}c>^P8)_7IXh@)P^C8%kO+ROGPc0;bBeqN^@;j3KK zaCzNdqiLUOl7i(R0bezBz#;bzxJqV&;Fd32UMk_k7ga$<11kw=W<^u)`)r#1UwGUd%Ets9|t7TtS_MU zj7X3l@AG?wcYH39p&zN2u6)J1G&)mtnG5=u>~Q<5)Vv*!*|_G*4<7uMscM<(VbSbS z&kklZoRGd}tQX=UYPnVVPQvHqg4%&1EmuG0BU!HtdV{kZyA<^&b#F@z>)lGf^D;)F zqHJ=ik)gHX3GW-7C5=Yktb;Q#>8@v$Y1VdqYHhpCOs_jGtKBU?7+&3TdN{_<_Z0oL z3@dTQ7c-c0^3&u)OyAN6d)<9aA9q@(T(03L&u2JIr|=1r_U6omdVPrka|TAo1C9pV zCWKr{*8$dtOM%k^-<9PAB=1zfKEbspvn%VPA@QHJ>i13Oi8vG&Iv-E&mf-Vyh$~>*%wmy7Jb8^28@(g`gT)Z=?D&e zA!hfAVql`@$G1gS%?O%OBJ-hd6LP-$*-B)5Gg#l}i-|wK5+D|sy2Kx2&Y?j*6eD=F z&#+0+Gc!2#T86R=%emEI&654zv$b)Q6Z0GQWZh#;G^a6})UWBr26ZnVOAMu7zJzh~ z(0RIFevCV1!?-YgQsZpj(5GiFPLpyf4s+KbFnOY%pI_$lyv~2~f=ICPf#zZz| zw7MeBaj|GGHrE^~aLiEaJY#3-nuN)`>2YxaFF^s?`Eq&F_BAli+-xza>nSje@$bHb z?bx&aMD)%$uD`7!4w{&yR|QiPov=r)Xq++W^p_-Qh}uP3MX{Eccx^qkTzjnxUIIr=+i{lMg4>IiNzZpkI&ke4eVk3$5oz|? z8~y|VwvC>UYv03$bWVFp& zhz>KM$trugXH{q91i3`#Oy6A~UDzymNIPq9u*rewl$9nXcD^=@Z2XCV)1h|95NQzz zvZHCC#ObqE*Bb{%`yt~myu21H(Z6`M8LK1$rzxsB9mR;mTC^ISQJsTo8jI&qG{HRiZ8 zITI!J7}ECXYfhXgO6bzDpnap+P3B%h)roue5K`GVSI*=O2xWR6yWm#q@3rS{b+&}$ zgkO4oj*hB@T4et*^h|A&g@Se78`aw_YKyW<{uhpajBGnEjy-y!O%h+yaU$uu;F9qH z!M4=B$gUd`)2nj^E^*m$*R340N-s&O#+T)O`JDPrKM>Pfcr(4D{?dIbJ_=8=W~yM$ zyTkg=Al#a!$j^g!W!@a1X#4lv{Ce=#SiTg_jFTQikH;OHh`MZw$MwUm?xQd1i0XZY z>+Eqez0fL^&~}!2iM__-GU#Om40lbxZtfZG@zU25%)_YYU^XE4o)FXXW1qX{CnFs9ibDalhW0_Dp@w&okS(JLyvmbFiTw zDPnzp;M_ai`rFU5gJVj542KGZ%$}Mu(VBEZE#d-Z>o-2N#$RqLEgnYXWGUvS=d()k zs$~&588SJ05PM>bVIL10A8(A$M`Z_|D&xwEBc*=GzdSr~cyBZ9bV5?DU8xDi$sq2z zDpg3PqF5uyAHRkT)$8v~RUFu~j$yyj+g0qbpW;XuHyD`gscXr4lxTlM78|=KT5Zfa zZ0yBXKa&mly9aEGAo)|&fYxH@L!$e|!()R#BI|f%>uuXeCFXhs^sU$Yo4-G!Z9MH( zUj6yy%HrH7#dQrvowuLN#|#VT#pZC$azg{MHm@_TB)+I9dN)3eHvXN!L^?V0P(q#| zU4LF>Re2$dyO8CYhuf~z*1gO}V^P$e&$!|Z^2Axs#s^})j zo!P9O!?q}FyzAQf*p<9Me3I>#f0JveU(6>-Hm#=9^NDARuGxCNaF49I>u6*A#Dvh- zi4c=1+K%bc zo}8D1wd1}&6AXQRM>$u!hdB!qZMIIY!Ta#w;frqzhKA~`zPzn9He@tB&$$7(db;RP z9_-(Ke9+{bw@Y7`*DAl)9FSy8%IH1-)oVtzhZIFO=G=K}7|4m^PUe0Toi2>NYwR7G z^E0dm_tztlJgOpZDDx z@Yii_-*@nhm%bztRbn)2Wgw|H%rhMP%q~Lgo0);Mv#Y^NkB$mqD*i=c7O_avea~Kz zV<88X-oHq`aYjUm{6z)%Aa%1Sh}yM~_Ws19mvk3G=|3kbxNQtk=SxjrNz09%yfus) zsx8FUT^|x{wd-10&2xOC32dWXo_lN*oJ6du`X(nUhX!1C92zc3Zt$xbWk)QfsNepk z;{Q}3kEtx7O=|S|Thm16=9#SWuuqII^(*i}fH9~_DI~vBZlJ%nzqit+XLA!MIO6u! zypC24z7NnRgYN^3G%jHX&W@sZqO%oA)Z5Vod|vLoWJ^}@btCEeYU&Yu?FlkOL99IUId2RI;7Fq2Vcw1oPVN|QIl*0A z40ylOEG7usRiW6+2^wi=!xWv}NHB3xaZvbV%;b9&QAZvM0%ja{SK_M8Y5aT|C?zcHJQo#7GV#M^M!r92NPOAupBD zk?<5}H$7)(2RXqVqhLEd{}5|{6N-qZ;Fa(c5(tVwiDQr`3_?N=g~UjpF$mOI1QLVz zlfE;NZ0+;^(%zXquyenHu0nPP$M@N7`ZcF?Nv^-1emy#ncPA4JwmU5_c)~9i-0_~I zUl9kfeu)UScqbbYSU-MC*x%d9|0Wp_1W9QWfq;ff6D1|!;v{JqINll|0Y^$(NfME0 zl9Uy}YL|sS(A}M_DPDLt(s>(@N02KpL3g==o%$_Qr~j(&WlP$L14tPhiGu$|83Kcp z5fuC1yC*@iMo1B)rQkABXe3-*k|Yh6K_F$|k_2(IG?@A%q_pH;&Hb;qx04aSyLV1( zry%|w%yVM@SGa#E{HcP1wESuVD>GQ=#r~-Dzwrfz;(vMkmS+FUAz-k72Kih3{zKP4 zbp0&`{+9AT+4T=ye~W>?rTkBJ{nzMX{?}fSZ(xnSs47D#q>rAbNb~gm^#4dV-CN6jcpn#tByXgA#O)HgBteO*|B3BZ{K4HX)m;P{jPo^9RE!3eAp4bnWJXV_9-;e``ayfJcSGmp^NTV=>v>s2Qs~ zTd5cg+Qvp-p(Cx{O!Tp%zW&TrdFPs{N3^vQi%sK;g&7&1#bzr%#;E@OPM~h7kw{Oz zi0Q~-B$T%@n5T8^#N547>%>%YH3h?#HZ(FaVmrw4R5L9pd)kFc6)-n9uWoK`7H}^x zx%KQ+wQsq+EkldX#zVtw`EA8d;$LTHMawEHS6e12>!U1oPfs3H-e+!M;pekhSymR< z?Z7ovB430ZUr-+t4zRFE3=0bzFgmDg`*f`F@avg`X6y3ifSjD1i>u=p{{_BtlU~}J z*$|aF!=&X>78olI3*}@;On@*!TR#GUkO>>08dyRDcwn6;00C^W;vMs)i}ygfHolTp}xMpZJcLral3c6rML5u%gZlkK)O1n zJ;v|FEUwXfyC%%SsI$dWgh85O_p*>l=l0nQ!1DMVNY>yc&D+mU_Qgno>8~_yTd}V!EoHf~ndZBHwsotoe$Z=Mo_%J`hDsN` zI~8ncg>zGLG~>0PprGU(h=fg=^y`s1S?}Swfc52423NdcQdsf-lJk!;arSghoq@8; z6$_6VgjkC!b4WQY1=~8I+erKU)>OK#>;^9T!L_87)#8%_y}iBN%NFemSZfUhhF0;m zmAjP{73_-Nv9+vboy*-z7Cj3#z{4fp;Qs3$$9W@)SU`^N%_;1KT7r23@@NS>>}PGl z4YyPwi<~#oEfTcKc;<)Mgd|q)W0$8FL%?(j04FA>4fDT{w@SbUUYpaZ+@+a)df8Wd zO!MEYT)s+MfzM=A<*i)D3a^x7H^EHzxH8?jZJN;azLsb9-5ir=Yw)nGbGm;f%PW?VI z_{D2#>ljZ}&d8E!0B1E=&R=HOVbPCyq&=ujp!7g8^%TRrPpm&1w?c3MRt`Eb8|Ja` z=G<5i@-Y#(GGYT*4~5gbv|sb%I?yh&hDi?~(bstZ6o5uRaSWCW*1&t9sfrcg3E+u_ zz@P{y8XyAJmJ9)`Rjf@w`2UA8Xf$lcc7Ugfy5F9<(#Iw=ns2f0wQA!GlyarMK8dgF z^N3weoB22ZltiT#ioKm&*A=;+MfIYtFtiqE%0pWd)OT)4dsuPQY*|Gluv62)xeQ)~ zRGdmbjkP*^S(ZWtV*5PPnv(`+t2`O4cew%>c8OE51rh{qzwZZ1_}N2zHV8``4S;;u zV~A4h^%DM|rhdSa@3C%EKq>!4Igf2^R?Fa#raGW-W8-FU9~-QWLw|*D`!hBogank< zeFUyDR<6xe*~$f^Foe_ey_LWrIC%gljtF}A0JIAH4EWt)0BQ-+2Zj#Euww`c06)Ta zEDzX;pZx3`XLqaUn|yidbka`oY#c8fXysjRy`4UIm3ZvZ7+W(nvT@M*d7l%0QgNs_=rF z6s#1C<*r@O-OMs8gM2H(ZpznjQ)la`gKIpv;mvOMt^|Jp!aeNjNTu@zE3BQzyOTrA ztt(+`drP}v0lT@z6Ocgs53}VzPNQFfqXCekrI!w_s0+|Bgrkx(GI~m~6)zuzj4Xcr zdaMlm@{fr*(iHyY&70&D;eel42Qq|I9cP-DPoY~a6(r)WTp@*m*kRMyCNjH6VFtXeu_|0VyNU!B_fv@ zk!wnUuS+Sry+Upwk`|7-$^t#AU*y(F&@3s+5y>b?XK*G-Zv}O+}~*R z(fpvm)80pRv@r-jJQV%4UM%iFysr~=V-BP0{(oAZ_@8%_tXEIimyqDK|LnaKOTDvn zqlOt-`jq<}?`0A{m{S;;h(O$kNZRn!x^ACwg|E`X#T({Msg5JTBSC4pmV&m>x1$E{ z&C}7@e?GYX+4#BTU-CxB7+y}>ca8XtBos03=$+o{v?nmAPkUG2YfbQkVR`wJ8k=BL z&|hkoH8ivea&JodoB8%yJ-_`(n2`*ASq!g zSWFzfbQ78Dp5qe!kaeonHydd5rXLLC7fX3@(>PJCi5n5ZLCV`u*P~owr?S^X`)%v~ zyH|F`??*)79|v|I#9k3t52&twxO^<(+Uw&D2x;ld&(+VA^XqE!D_()!FL``~X#66L zHPxpCZ_BEahb+v1cPk%Sqo*R=BXPmfB^Us7H?2H~h&+54+$kPnWo<4#Att|eo2~-D z8il(ygqR--F%1j|ICDM(Fui!@M97)bs^PvNKB^X0)^^U9r4;}`*44^vpF_j|GmGY@ zx|E*LBP$gjMrVxKWGtS&?5H95BSbZxlXuXE!vr zcuF?q?OU@Jb7Q=ox_W)OUMmC+&uy7cD#i;P&CJ%NT`r%a4G*L|e!R)W#U;g~Tr)8- z@zc4=;e$%Iu9+Rt=sJ7!+!RTkcm1B|Y8UR{ev}exnS1T^P?>|ayj4$#HYg}{I+UuB zVw1n=$HjiWr{%=5-1*VeLSt}wie_f^oo9hdNU*e5zO;AmVR10S*TT%K!|KfU1sb?) zZDuxbotc93k4;)OksGUa(ortF)B;BPS^S-qR8mrM_(w9$fpRcUz3Xi7MA|s+ID|qG z&>En+1G3GjB(Q;Z95&-l=NX;XL4l{Hm9{~!0q4-hh7m_5aOYrI{Sv~VLx(PggzOB> zdV=(IrhH&)BC~+hbLv*RHWU(svEaGmg$_>!nH}@s4M(wE?87x(*604YGnzp#+9fD|r)-A{^TS zR0Ib^WDQKhn8Y*f*ML(@>CSmRZ?>s^)n!0*JCp^mWb^=FOjVPIbm*t`8gP{?E zesKZ;STY6T5{rScvfYE@IsFt*Dr`dmaY@b%eW6w|013K-NMsIj#5V44L^GScsrz|w zrKED}M>_tu=@f~@^%G6Ow0Aa!f>6P)MH*4`v5lO%UlU%F*Gf9&yHhc+H1Ig0Qm6)~ zXGrA4e7VOx5q<3PBdt`SP*_-4NDvp-Yw<|rcF~;B++Ns@Yw|wl&)Yoc8v7jjNwR>6 zMaVR|q4h=jj^EgNWG%s}U^i~ki@I`ePdsb<9N&-mmde#s1&~NpWTHKDhL>Y>Vf%pc zD;myCGwIRD(8)BF^jqHDB&_!EU7e9M(eS9IQ!P7oo}pYWc!e{0WTcOR{b6?759o(D z{rddRT}hSGqH*Owbyl_0gCqx&QF49dsY+yDA6|!JP)-^P)=3kU9`-Oe@VO3px;*G1 zrfR?i+B43%$K1OX^37t755 zG?Gky3S$)g28X0;6Z=BFR&=ie1q2v$GQ{EyhDuALX0L>HGe@S>s9S4$JdubRoU%}WlOb+AQg3c0sb#OyEC^N7-;H-d|f^L znB18^&zoRfge~}yMSSW!I5MA_&{h+E@P7Y}Xt!Z%MY(sAfvAh0=|t*e)sm;?!t3R3 zx5)se`iddqVM;Q@^LNgLQSmZYaH$TeYRQqQ@oF6*Q&6=Y6R@;&=P3U`% zkq6%hC+bX*L$-B-{BN6Hi^<6VD47XGq7xX_Lo%8A1-A#m!^R++=WFt6`!cQLd*~^% zuBpHWWacojrS->>0%Ni(+1%{(!q}}hb1!#6C9pHwX)nN(HSkV~}F zNkNVx)jJ1o0r5K)5qAja-@wi+ZwUL8KUA=1d^c3peVHuc*6tU5V88n2`DE;?yPrUf zxJ{noNap6{ZO)g!U=I&bH2~%uHVkQ5bVH-}^b=(D#9;O~o>@Puuz=kw^5rSW5pknf z#LYsWieKef8`foojyk%crw(+%dQy~zf&FtJ=g7tps5V3MZ?j9J06|3kHh%Ylr zxYp-O)A-8xvhdHKQwt86;f3_URprHeQF%`(?IWYE@uQO=Sa=Pc**ru1fCMw8_oSqthsz{yWpui?Sk;_I9Lr0=y zqCO}`aN$+jl2`cadBRCBf(dap{DYbI-2BBZa9u0eGCVx|kPLu;588<`#A$w$chhu_ z3A5XTCyH#%G%Uy0CPExFl8jyzc@nKa~DP)QuNm`R1Vm(cfaa;BI&3+wffTWj`{0L4pz{ zT6p!*SL#Li@^ZGQM22uAyHSo(B1k>Lad%jdAbKMIvmX#_w_A%O9Qkja&qheCp@tZ~ zq176z9)}M{vijssjTpAzM9)oJU0sW?=&eIF!Q<6E49h%wS&l=1@m&Q_P+<}s9sSxC z9o8!jx!b$i{&^B z!QJ#TnR^)7}=M4a63vm?P3XBK+cXx7`C z&_f!6Bgc?SK$vfR0=X38JT)ra;5|xG7G13IuQYmg4J__W#o`5a$4l?M#6ofJ{Qf`vk)182BUrFtEO@ zA@~LaFu$AudDEdEruU@4@SzD7Rq(RuqhOz=Z9bDFhWP#?@cnY-Fl}b0Y{n5t7c$g{ zRH1v)%J{FH6$}x}csqC4vz<5f{|%WX%5m@VyhA1KEV-;>XTyNpUBgFeI@?RTBreB zn4|Us`(z1^Ki)P>YSl*RD|ac;43p-Zx!5E?TH52L+aK`Z;w~3c*pY_XLaBM zAI?!1u-Skc*BcunbZjf zSvO{}H9J1PlOx_=J`8eBqz|Nd1XR_Io)uQITk-xPI(5ZTHNYHstbX}#d`x7n1qaPp1xhc5ro z2fUfF4b-On#pfGCWfH2Hl|FM`_mH^-bHVZW&+aXMn7vhp5f7<3IXlBX#P{XE{L%ZWz#~7Cs(ce z(XL3bdUCyRa!*77q}45!5sAda$g7ywZze{>#?D?Rg*j+@8G|}L;97XgRDrd~4{F5W z&h)Dt=??J71f=y7>{_UtXnEDbEk(t%xx9ZtGUG^GDH=lMO&tDJr)$Gu#qJ+iQF>7_ z-Q9^U;)bpW_aUiuzoD=U;lqr_{)VUw;AM83T+vUQ-V^GD4Lzw2RST4P$o;q>FKJ1(3DxoObsZnoZx zwBY%IAZEQNSSR-rjg&|R%VGE!`Arzj?blR&Ev6$!3PEJl|i}sz#Zirgwh=OvH!6VSvL(eH8b7tn= z6*ZzAzek}kQDBM_JrclZv8%;LV}y*v&*r~G8);GnL!`ZN5XXE{UG$-qtYwQK@?ZQ{ z;o$cW@>%Kk&|tTy*pZg}VU9;{6dSsJWo*I1Vn_>z#1KM}>!~>9(ELl7V`6s9)pwZ5 z4Rx0)XO&jY|6X0zR6o@^L%06v4(kG_-y>cNMZW{Y*-63dLp<-DQ#;kFp6`Crn!}Hg zmA&z(U{?Hal-us?y`ZkTI-D?S_=wVGnlG(?YW~Nan-H@#!t?g*QT>ai9 zt)p#hlGG?khyXM(AzaYIn4+jRIydicZGOUQ3{I13C&uuD&E6jtDfqvz&B;y6DVjUYBe z=_<75_;kl7Z^-q#ku#9kC)45=ypgao(w@)GC|fPJr!6zxtzJu|a85ZEg;%ChvQHtd zOj@AlvJ;ikWea1{07gEJGC1)nQ3zh7c9ya)l?I&QNixL7W=E3XMPmONd?hHLvGvAV zqf^XJZ7z*|*gww-Zzm>oedj@@T>t<9gOeQr8GjtbUsG#~R2UL09CV$bRsV0@~cwt6=C!vgaAaDnR+s_n1$!~x`&UicTt|@eeTaOSSXqd z@PA3fF{T?9@dokirloVNujv;|1B$0vy1r1bDgM}!q8Yw2y zv>*5Ik2?MoxfF6$!N{?II#kGxAN&t~cYkY^rl;Jba2yD{*!IUb5ZndYP22uHw(a%_ z;C}|Lw64G20_HwRZ+5lF5iqn3TwHfGWe>RA0Y;v5*^nK{Pg5wBfcG={raUlw3k<3+`T%69tK}Qu;1HN7Q}(*YyFhzK6L;94>W01SaeuTOtZBD zg90QsWH>M~GdVUbIAt_9Ei^PaW-U24V`MF1H#j$9GdX2pGGbzrTm`5oGczV>lA~G&9FfKDVlPm^P4ly!1Fg8>$IXW;kIx{ho zjRqhNF)}(ZHdHV}XRF*r0gIAvouEoNdkH7ztVWnnEjWHmW0 zGC4A4FgRmmF=R6}lW7M@5HT_>Fg8>$IXW;iIx#UKFO%a2Vk9*%F=90^FflDKGh;a| zG-fzqEjc(bWG!PbF*q<|G-WwuIX9DO2T2YwGA%GRR4_R@Ff}?eF|)u2Xb2X6BVWS+ z000JJOGiWiKmb4hK&SREW|KY;9|;Tw2n8P~G%1>sW)LWUsYygZR9M69kUz_xA8O+-h5V*nAM){1c)v94<~8)LxCp97c~#u$`R;JwGPEVtWQi(2b>0c$OC z&T!7%?o!G>wE``k=bWX$4;9c_W16PheW^g-_n7ConNJHSrI1o;?)L(MAP9mW2*L}h W^86>|RiOz00000)pL&~wlNH8s0xFX2ZZ8^OsJ^g8HCEzLVt^y+^*rmfV3On+Mz2Rbq_|ZRzM&K)= za2IN|{4(bkBjo%|WD3OhlWDUbj7ZoNX~(N6iY2G%YTHV)<$I|ljv@M_@XC}@EKi6t zlP1h_YeFUUvAJ>5NDMq$8Jc((F9a_VyGm(GrD7L&R9$Pt*Ff_@a(@~;B_x2jbmF7Z zHO$Z1+%x)N|6I$x0VC>lcm|xVRsaA2g_9it8Gjgt-=<2XR2=LeB9fsx*+oTkD^)Cl zh0<1N)xqS_KWNgBq_{W=t_25w7OM^}&bm6d3WDGdh_i!}qKlOHT~cTf;~mF6y!X8? zci#a*v&vMnCkCjRWuy~vF_&Kz1F!HSNOKf(5;OH=W-$xT@pTU$-|wP4%e(H+(XSLt z27mZO;uzBni+F>0X4BF+?-PevSyG74i6;!YAn_yDWtZPLmmC&&X4uH4=ZV9_VyTPe zE@ovzC7vdZD5^&JLe6D{^A=~dT4(Kh@)w2*+DeA&v__D?5|T(kgp4{WsKP>&c8wGh zX*!R4_=g>Tid-_eDq!STKn*G+#}EDozkhpc7N;iNq);5_eX;G2F(9xDv>LYkeQevU z6TtrrTxlJDtqIJ2lHTZOkt1MW8@RacX!0I#xdRM6>5?HilAo4PECTOm^i6qS@D>QJ zxqWM&Y8fP+I|yhPdS9`Ek!?%TgL?f(4$&?<7wDI;m~000ekX;fHrShKML zg#sjHF=b|CHZ(9TWHV+mEi^M?FfC#=Wo0cgI5RObH8nXlIWstuS_P;MF)%SOH8D3h zH!?XeIX9CK1{nc1lPU&N4lpx1F)&mzGCDCbIyE?xiv}PLFf%$aFjO)!Ix#XjH8``m z1`PqTgb82>7Kjas#{d8T24YJ`L;(K){{a7>y{D6t`Vk)q0}2%kI&O+y-jgB{D1YKf zL_t(Y$L*9s3c@fDMW-T7%Y=|p=s6TGqIhXR4OX$k<%?e=#5yyNmYJ)X`0FoTIRcYojS4n%~ABXY4?z6^*c%d)m@ z$08!SmmdX)XpG78+*<2T&N&hF3V4W!bFQ^6ilS*69||FiF~bHz2&I&i(uZ2>Zw8*P zE2V;guR)R|Wm)?0n*n32sw#gD8^m#3*Y#L@8qD3!b9=+ZS_=S-vBBl_`}A_X{$ra) pUDr)oFu6e}2nC@a6oi5?atGge8WlHMD&+tG002ovPDHLkV1jBuz_T^bZ8kHvyx7S-^}>k3fBk<0crf_Jxd%_Rv8#(0Munj}puQ--nWMY|?7wT zRBRK)aMVBNtL|};300?PoA)N>t%Bhcor*B;Rbr)}Q$g|`au3*g8#WQV2iZr}eQMa{ zfsz*im9GHK|LnA8eVJTv|RV z6@>#cw+Jp z`LMLkM%QJxtE{qGKiEC)-1E|Y3=NJw$gM+w#M%Z(Qz%!YuH{rp(abZuY&wV4cK&Hf zW4z{F2y$4sLL{Qfl9Ei4q$anuqpA0E{fOamI&K0An?~#rEQZ4FdZ}k&zg+0FAlJ66 zSoZ+JVUGY_)H{_e$0-H^qSN50vEio+JgC7#y45(O-%BJdb#$*c1f&w{_~HX>Y{b?a z<0Yo`^|fdQb6MJS4BJe5q+)6*GohNIL@ZU5WkN)%P+d$V45~_UB4t`ih=nlvk9O5{ zD#tI#uM1W8;enf`M*T2W=>aqQ)HU{Kxxmb5W}#0-yIgf?*XyWsiLS1Oz(4=s-F-Z6u_Sl1b81~KnnAI?BDBiz{5QT`4rQ)C?!NG zzC}oy$Z67})8hj;y{X%;)0>LzoNk(iLad}J8iq_nS<-8<~dp#h4O0v;j$a@5!RPoSU>$*dQ#rm1|74V_wc2rR~2Ts`zzlwAu&=-MzzBj2I z*t1R8v&*ViBb)r=aUc99uK^w-uy1OU@4|oI-g7(O@j;8d9i?_)&o=qN-_3PT`?gG? zZEbteh7<0zHp}s~+knfdPqw>NnozPn@WQ+D6#i0Iq{l#Vja%YV zy7~ND4b8(cqt$EgorO2TmzmiHx>2$~AfU{Y>b2)}?$yZKtkn4dtq?BqI4(>eoQs;_ zGlX-8^AG(W9%12Jj6eL2e~p23(I>*azOXLL3F;S}8TE#E$y_|Nh2>C)jZAF#Qgme#1Q+&ixSn lAEy6d`oE9|KI*Hlk^X0E@ZU7R<<#DP$Z8R8v1j1A{sZLaOzQvu literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/settings.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_small_border.png similarity index 53% rename from common/src/main/resources/assets/createrailwaysnavigator/textures/gui/settings.png rename to common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_small_border.png index 2c89e9761324329622f7ff73be7545d98d3ab6e2..f40a715ce0874d03b520b15273415d85e5350ccc 100644 GIT binary patch delta 1160 zcmcbh`A1o`Gr-TCmrII^fq{Y7)59eQNGpIa2OE$quB!S1q+U-{m9LNHV^-w&^IgQ2 ziD`9_%=f8Z*lg?4May^l&YI%V;+QCN_*>PxhlLX2d{$2<>8LiXTv_NEu;TWe8kO9A zlGU6)dHK^D89B>duIxR;CR6z3PK}4mbN9u9N4he0ac#A-yZ`Z~bkWbeCp;?OPHy?d zny>PvXUp?$W3RAJZ!=#zm1|!Q4LUhznaJit+QH{e3W%$ny-;l1;q`7)t6?UCvb?+G zoQlaBEc!uK-j>t6SkHY>;Y>>2Jk3Kp;=fdls>XtiUyoHU4L;+atJhn1!~Q&1zQ(K4 z#;dYlcTARKRH|1JWw-9INLk@m7I?95X_xDUfLmN^t!BjhV)d1pBqHv9aO;By$CIue zJQ^9!9_}dchoP{nAgE-b(EsToQAa8Y?(~;`mn(nIurM-p)lFgMkh4aYI(N)Cy}qk4 zoaJIW!=f3JXRK5=?vAYJTVMU-N0s=)$a~NK$uxT%Olj~}=`*}rzoEmtBi&qg?q|Eg zj;X4xKju_iTEH!2Uld#Rv$yzUVqlMPHw4NY~;P1BNfO)M3=Iv8OcD(Y)66DIu*$0#rKDJ-S|pq5 zrlgn}=$e=(CFv%b8Kvr`rI;ETCnYDPB`2p$j%HO?u}Cs9HBL@U)HO3Rvd}d#Ff-M) zOfpZ>H8M&~PBBh1Gfgoun7oWNhTq)S*u>n($iTqdz}(2pV6q^aB%{S-ZMHxG6Dva_ zD+99-14}CdV=F_;$vtdxn1Z|5x>O8|OjD9ff$p_1H8j^XNd~ekjgw7u4U8jO0W&IFlDE4Hg98XI|E8ZdSzADk z>je)R2eZh_J>gR(2MegzGt_vxIEF+VetXrBtJOfn<)XCUqTU@Uj&DS-tG_tsB66p3 z*(dS&B27^dD~`_5`{1E=*(UhfRkjJsINcXnuqDJ!JN)_Po^rG6{K^cB`rLod-(F_y zZ}(<){a!}a00s~;h@AL;d*Y`1ZRac*)@+?FerN9NXFsKRJAmL1!x!Nw`3bkO7Oew1 Omci52&t;ucLK6Vn2Fnrv delta 2050 zcmeyPd_hySGr-TCmrII^fq{Y7)59f*fq@aoWMtrA15z&mdh*tUw`}#q^zu3p~_Zm@Garcue#ia8)v6*nk|*_?qS)S}ZPJm~F4|=z7?P4>un_@~wCx-+JdESH2a0R%zY7&zr@k{E0mws`35Y zmLIJ8OCEKtIqn}W;_bJ6xvu}NXuIiQnUgGB{n$(vPDyH<->Q>1@3dY>aJGM9iPVt} z_f-y)*PQZl2z{+p8ox@^hFOqRWs@Jr62;8CX7w6Xl?4SaTeo{1H(0-U=f7M2zn@1w zWR_c2TGGk2e8pr5My2{iqU_ck7AY(AZn?V3c#C!?EZia*Rcf&F3->G&6-~XqrtE_U z`;V?_?%uqPZ(WnZ55`BY9xZvHr20=^b4$m^2Rr27zq5UJkI`xIp^(kmTq|a!E_q~V zI6GRDdHsv_28AjEMZ;kKCE~^(Wa7Vn`1n=t(B?ho|5~z5*>jR%zK2ZeuKGm5e1Y?M zQC81u9|>rCa(%S?xWrLhrS8eDH=m@RFFMS0o=0-#6`R722%WMGcMh&eS#iYpNTTai zjrbG0))d@(v?eTTO{xFCLkV%wp2c&YX*`N>V)E>m5a=u^8qslK)x;zv*WZTx|2rQ0 zPx4j^m+X<@Ug6aGHz~OAAY5Q$qs_OG67Y6U)g0Y?6#7leO3a z1&pl>4Xq4}LJUoz>UOPpvb3Na*K=+rK1q&N&Ju;m&VuUo zEDoM7jv*Dd-rhdAagl>a+r`^rj5}0VoD6iPr!^|>P-Ix7!0J?x$(GBrez$zby!zs> zO%py{GY>Y_JT>j`nQw8cY8}FV9h`HQVe{R(pPHtXe&N3xQX}{9ZtgYfOTPSkZq3rP z{nqRU&M+Dn14$VM<6?$0NSu0xGjF)V{`fa8&+O{xsGL{ZoyH#f^1sFh`^GbUzkla` zYN*+N|MAb7D&92dweNrLh+FSl_b0TTrK3ZlO;N7jea`vk6?@}U{@=X)_S%;PA^%ku zOn1sle;q2%px=7uT)#C#9M?()4=sjCTn$T@6fl|fXu_fmObXP+)17eJ-ms(|-BGLy z7(A$oA1s}K!|x!k*P{jrRc(g(mdKI;Vst07-G=Gynhq diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/route_details.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_small_border_connected.png similarity index 51% rename from common/src/main/resources/assets/createrailwaysnavigator/textures/gui/route_details.png rename to common/src/main/resources/assets/createrailwaysnavigator/textures/block/advanced_display_small_border_connected.png index b7cdc286bf66089e29fc111d1f92ad69a9e429f0..25ea754e8b87e1145c9c7f1509b83e206209b9a4 100644 GIT binary patch delta 1395 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 DMV$|> delta 1252 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 J|Eq|V{{*<&;9dX# diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/gui.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/gui.png index e77d23eed7e946ed79cdfebbdbe1d021bbe63899..0c7b81a101aa1bbd489c3adf05c99bbe819a55f6 100644 GIT binary patch delta 2474 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 delta 2386 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 Ef}Zb2+5i9m diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/icons.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/icons.png index 7f9006bfdda170bf5d99338622c9f52c08324e4f..fc48ec582c92f322c02bc40505f06a6e3ea44c92 100644 GIT binary patch delta 3783 zcmZWrc{J3G*Pj`Cg;FFG$|TERHpW;cN@6TwB3rT+!z4@gzUVD_DzcYt=8>@^OR~+# zQYwV(MhvE&2$L-##?1UY?>X;#-gDk_&$)lxb3UKX`P_TYJ@>2id&eiXfSqBAfBLg& zO6ST2c&ZKV)1xLOEsgsd+=+MIdb$!=Upy3==s);Qy1i_Y8LJX>=SmpRlO%w+{Z@-h zsqt`46G=I7^rn zTY6pr9vYf!-@C8cO1~6h8EBOY@Ao~bPOC4~!9ISUY_4|haJAGumf6dS%(!yG_hW@B z_gV%7{6)ruZU0za1l?MA?@IN4%J||2ER}NCU#^0cSHh#LyFF_&WWmbE>rYCHAd@jZ zWj43%(o(2@nblmH*jI^t>b4_feIUaT3F=}K#- z7}BXTm>hSR$dVg#h|u>ZUlEqQs1Kb#j3i%AKJ}u+q4mkumDx8RoGV71Ifc9Tz819t z`LozQxXP>KBSTLploa)VnnTpfsvk`6Zyq-0*)hncb4;t$W%cJ?fyHatBK*5EQ!dm6 zHEp$gP_&1_4{rt0B}nD{e_|C4Y}kUL#sW^!oo$1HUgTmutjl*%*$H8-Dudiw5w~T~ zDck#jpQ?@&t7TfYbB@J1fMr{SgdJPdrzbd=I8eyQ=#Y4Du&7n0T1?-MDehDYH>PB2 zMv`t9(%}%O@fZYBFne7A`3s_g@$~W4$LRaP5S|z>7)lq3fa!Z;{9vAXI=+4gKa`J- zo_>b2P?-c0iH2kJ^)WiSx=0KnLsFP@5P{XvB_Q@P5(~#@=)f}`3!nIZ#n;WkhT<9e zQf?|3w6CrX8tDtuN22v$C_g_q%u`1f0rSD=d+VV5e358{5W8et54$@~&+WY@t|HshlmBTkgWxBdwJqLk6 z!d`gu^Psc;M(L)uUq+CEqSygpX#uGt%I^|0vJ_N-{i7D=O$d<_OH)QE-#f*M;j+mVra+Rj zx5=q@$UC}Ii=~MGmZf*%@OTEr9FqK}${=j!j_9xc!*TpiEMV%XAaA#^0mz~D_fuW@ zAYgD~%HuGXcTez%{1O0{lVuV$l}3Nzg)f587Ocq~7QdZ7o88 z)L&?n$2}8-<>u3TVKCsjzrkF#yz@;`E_K@#R*Ay`B(nG?6XkBpWs_dEX z+L>zzRbX|XpzGQ#pQ%v}Y;E_g321PY`grto=p`30*;VgUEiaF1MM#gEbLnbH1k~7C zL7nzRd4TKkQ1p*YyMSr82ud-eC+nMfe5qa3$eZh-mm_t-#S$+G`(?!_3n81NZHLL@ zQz^8!ueyk-b<6l}RmSlvi>Ae`%gf939BmICPAqfNZ0jXx`c2d@&e(W!cm)Q|a`E%` zwx1NO9E{65;eekcM*~0TQhKgJEH}00@YS3;fSHD93IDIw`q+wNb#js{>x_2h?{gJK z+-ZAorY7;C+#ZWqS(r0z*_ADOc45uIB|$p~2#_ab3s>4-HetuVKXEq!3XAZEUK;i~ zUy(|)`+kGrt0mTJ&0id?=y-j-Ut*8JLbJZNg?jo#e6U-dOjFgus`)F2)xIW>S5u?jeb7Pysmwov${zeODiSZKOHx>SE# z)7zg*LvpiQD?q%a{^vi4fd!%;y>+4PrOeLG@~zl#SmB>05IL7M-^9L2@2VW*Oc=gmwR7Nbt~dwKQLaMpC!UmqpwM#zFC4 z>37vbE`1h?4B&;vy6tpyba;7bewiCmskyMK?XGn-de_!gGuP(z;_-xree6)1acF#8(tV=IKpp*Fq#Om9)3q5_FxrF_# zb&sf8XNy1NIqrjj4SPLYH%5AA(z`-9tqplO2KORj4M&;De#fq%kBB{ zit>j_mbY0nmFG!GNy|K5YpSD2xxgVYtD`VTP z&DVbrtV?VvBqP89dYA~w_S|fgy_|ayT4H1ZL^2vkvVYT{Q25!y<#O>D@OW-nr`{RT zmDNs7aE*o!$b12A_rsGX#(ey1Nd88HEV-Pqj(%EpLwIm7o#}7(DpP@>ATUnfe^8uR z`69{NU+rp!pk;O9Wc0Nqre!_NggP?gAaYe@zUZzKz8iXUml&r$AV+~pUkq#O>&uak zI5VXfI=b!%4|ZGJYMefFL0QJ2DJ0=3M^sDdeL1MmCJ!LOl9aFt5=n04JAJ(0xmo8H z(W4E!gY8vDHykRT(oyo@=m*8Ql>7a(d`u-L~MO3Bj0^u5nC8vfL zjqn5>iM^E;I9EooD5$&!p9lGwM?chAvQkqjQWm z)aX34208dBMfYq->Ts@cQDlY60G-LaX9b;P4n?mVpU3As%x<92vW-K4M1s^6fZg)= zbXwQBD|q#U-0V2)3^OCn-a@yL?7+v7;tq}(&Jk1B;^$4qf5}!(aFp|lAXnHIv|KN{ zQnwPvU+gez*K%Vh*Hy$cp^QD%*;`#d3{BnE16zr7k+(zRXT#jCrONwe21ZzfXEYzs ziw-0&UU}B66u8XAjMeE3{uluoQ~V?)BLbsL1Bs>vW#z>E#gEik@&r$tLQx|{}I6CJDEHa1> z^vM?m)^i8bmTs&z?lsoQz0NSVW!8oOU9ku=Lign6sit4)$jLXi?4N&(d_GL&ku|_X zl2-JhgOe?dH_!H%m_b*2F8_V9ezJL#L)&02M&d~IReP7_&c!$XkaKt*OkC<6K}1B% zw|NUS^yQB(us?FZI&Ku)wm@wAoiLC~3G+O1^q43upnaNVRV(`tRx~CNKg`C}s8XhG zRgs-yw*5a{bs7w!i8D}Vy0W%7mbludcSZ(x;6oE)lyLbH2E)Lsschf1%CQi3bHM@J zN>p`t6J4-o&RUshqi8L(J#uO9*pZqd~lWx{R!Jka3>TQ?1-5;U0_yk}^Ny#Y<^!Y1=M0;pu zkwcl=(chhr_iq322%NdujK5pFsfBPA!nuV*&MXL+7pfx}zZlbICE=5n*?;eEr|MS- sL`8SJOIPjw`RqW(q0=?8OKjU1AD31cGVS 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 Z@IQ;`NeN(xm_z^o002ovPDHLkV1f%OD(?UQ diff --git a/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/navigator.png b/common/src/main/resources/assets/createrailwaysnavigator/textures/gui/navigator.png deleted file mode 100644 index 0dc23cc8c1b296938ccb5364d532f4dcc01b7544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6859 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.19.2", - "architectury": ">=6.5.85", - "dragonlib": ">=1.19.2-2.2.16", - "create": "*" + "fabricloader": ">=${fabric_loader_version}", + "minecraft": "~${minecraft_version}", + "java": ">=${java_version}", + "dragonlib": ">=${dragonlib_version}", + "create": "*", + "fabric-api": "*" }, - "accessWidener": "createrailwaysnavigator.accesswidener" -} \ No newline at end of file + "custom": { + "modmenu": { + "links": { + "Discord": "${discord}" + }, + "update_checker": true + } + }, + "accessWidener": "${modid}.accesswidener" +} diff --git a/forge/build.gradle b/forge/build.gradle index 7fdb673d..31926da9 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -38,9 +38,7 @@ dependencies { modCompileOnly("dev.architectury:architectury-forge:${rootProject.architectury_api_version}") modImplementation(include("de.mrjulsen.mcdragonlib:dragonlib-forge:${rootProject.minecraft_version}-${rootProject.dragonlib_version}")) - modImplementation("net.minecraftforge:forgeconfigapiport-fabric:${rootProject.forge_config_api_port_version}") - modImplementation("com.electronwill.night-config:toml:3.6.0") - + modImplementation("com.simibubi.create:create-${rootProject.minecraft_version}:${rootProject.create_forge_version}:slim") { transitive = false } modImplementation("com.tterrag.registrate:Registrate:${rootProject.registrate_forge_version}") modImplementation("com.jozufozu.flywheel:flywheel-forge-${rootProject.minecraft_version}:${rootProject.flywheel_forge_version}") diff --git a/forge/src/main/java/de/mrjulsen/crn/forge/CRNPlatformSpecificClientImpl.java b/forge/src/main/java/de/mrjulsen/crn/forge/CRNPlatformSpecificClientImpl.java index 620bf805..fb3ba350 100644 --- a/forge/src/main/java/de/mrjulsen/crn/forge/CRNPlatformSpecificClientImpl.java +++ b/forge/src/main/java/de/mrjulsen/crn/forge/CRNPlatformSpecificClientImpl.java @@ -7,11 +7,8 @@ import com.simibubi.create.foundation.block.connected.ConnectedTextureBehaviour; import com.simibubi.create.foundation.utility.RegisteredObjects; -import de.mrjulsen.crn.CreateRailwaysNavigator; import net.minecraft.world.level.block.Block; -import net.minecraftforge.fml.common.Mod; -@Mod(CreateRailwaysNavigator.MOD_ID) public class CRNPlatformSpecificClientImpl { public static void registerCTBehviour(Block entry, Supplier behaviorSupplier) { diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index abfbcae1..781ccc9e 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -1,52 +1,42 @@ modLoader = "javafml" -loaderVersion = "[43,)" -#issueTrackerURL = "" -license = "GNU General Public License v3.0" +loaderVersion = "[${forge_version_int},)" +issueTrackerURL = "${issues}" +license = "${license}" [[mods]] -modId = "createrailwaysnavigator" +modId = "${modid}" version = "${version}" -displayName = "Create Railways Navigator" -authors = "MrJulsen" -updateJSONURL="https://raw.githubusercontent.com/MisterJulsen/Create-Train-Navigator/1.19.2/update.json" -displayURL="https://www.curseforge.com/minecraft/mc-mods/create-railways-navigator" -issueTrackerURL="https://github.com/MisterJulsen/Create-Train-Navigator/issues" -description = ''' -Get possible train connections in your world from one station to another using the Create Railways Navigator. An addon for the Create Mod. -''' -logoFile="icon.png" +displayName = "${display_name}" +displayUrl="${homepage}" +authors = "${authors}" +description = '''${description}''' +logoFile = "${icon}" -[[dependencies.createrailwaysnavigator]] +[[dependencies.${modid}]] modId = "forge" mandatory = true -versionRange = "[43,)" +versionRange = "[${forge_version_int},)" ordering = "NONE" side = "BOTH" -[[dependencies.createrailwaysnavigator]] +[[dependencies.${modid}]] modId = "minecraft" mandatory = true -versionRange = "[1.19.2,)" +versionRange = "[${minecraft_version},${next_unsupported_minecraft_version})" ordering = "NONE" side = "BOTH" -[[dependencies.createrailwaysnavigator]] -modId = "architectury" -mandatory = true -versionRange = "[6.5.85,)" -ordering = "AFTER" -side = "BOTH" - -[[dependencies.createrailwaysnavigator]] +[[dependencies.${modid}]] modId = "dragonlib" mandatory = true -versionRange = "[1.19.2-2.2.16,)" -ordering = "AFTER" +versionRange = "[${dragonlib_version},)" +ordering = "BEFORE" side = "BOTH" -[[dependencies.createrailwaysnavigator]] + +[[dependencies.${modid}]] modId = "create" mandatory = true -versionRange = "[0.5.1.f,)" -ordering = "AFTER" +versionRange = "[${create_forge_version},)" +ordering = "BEFORE" side = "BOTH" \ No newline at end of file diff --git a/forge/src/main/resources/pack.mcmeta b/forge/src/main/resources/pack.mcmeta index 9252d3db..2eb93b44 100644 --- a/forge/src/main/resources/pack.mcmeta +++ b/forge/src/main/resources/pack.mcmeta @@ -1,6 +1,7 @@ { "pack": { - "description": "Example Mod", - "pack_format": 8 + "description": "${display_name}", + "forge:data_pack_format": 9, + "pack_format": 9 } } diff --git a/gradle.properties b/gradle.properties index 38fadcf8..56ad0c6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,27 +1,43 @@ # Done to increase the memory available to Gradle. -org.gradle.jvmargs=-Xmx6G +org.gradle.jvmargs=-Xmx8G org.gradle.parallel=true # Mod properties -mod_version = 0.6.0 +mod_version = 0.7.0 release_channel = beta maven_group = de.mrjulsen.crn archives_name = createrailwaysnavigator +main_class_name = CreateRailwaysNavigator enabled_platforms = fabric,forge +java_version = 17 +datapack_version = 10 + +# Mod Info +display_name = Create Railways Navigator +authors = MrJulsen +description = Get possible train connections in your world from one station to another using the Create Railways Navigator. An addon for the Create Mod. +homepage = https://www.curseforge.com/minecraft/mc-mods/create-railways-navigator +issues = https://github.com/MisterJulsen/Create-Train-Navigator/issues +source = https://github.com/MisterJulsen/Create-Train-Navigator +license = GNU General Public License v3.0 +icon = icon.png +discord = https://discord.gg/AeSbNgvc7f # Minecraft properties minecraft_version = 1.19.2 parchment_version = 2022.11.27 +next_unsupported_minecraft_version=1.20 # Dependencies architectury_api_version = 6.5.85 fabric_loader_version = 0.15.11 fabric_api_version = 0.77.0+1.19.2 forge_version = 1.19.2-43.3.13 +forge_version_int = 43 -dragonlib_version = 2.2.16 -modmenu_version=4.1.2 -forge_config_api_port_version=4.2.11 +dragonlib_version = 2.2.21 +modmenu_version = 4.1.2 +forge_config_api_port_version = 4.2.11 create_fabric_version = 0.5.1-f-build.1416+mc1.19.2 create_forge_version = 0.5.1.f-46 registrate_forge_version = MC1.19-1.1.5 diff --git a/settings.gradle b/settings.gradle index ccd48de9..5ff73e23 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,7 @@ pluginManagement { repositories { maven { url "https://maven.fabricmc.net/" } maven { url "https://maven.architectury.dev/" } - maven { url "https://files.minecraftforge.net/maven/" } + maven { url "https://maven.minecraftforge.net/" } gradlePluginPortal() } } @@ -11,4 +11,4 @@ include("common") include("fabric") include("forge") -rootProject.name = "CRN" +rootProject.name = "Create Railways Navigator" diff --git a/update.json b/update.json index 7ae6eccb..8e6347d0 100644 --- a/update.json +++ b/update.json @@ -1,7 +1,7 @@ { "promos": { - "1.19.2-latest": "0.5.3-1.19.2", - "1.19.2-recommended": "0.5.3-1.19.2" + "1.18.2-latest": "0.5.3-1.18.2", + "1.18.2-recommended": "0.5.3-1.18.2" }, "homepage": "https://www.curseforge.com/minecraft/mc-mods/create-railways-navigator" } \ No newline at end of file From ad30bf137876f14431d84eecc8135b9b053dd632 Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Tue, 17 Dec 2024 16:09:18 +0100 Subject: [PATCH 08/10] Updated project setup --- build.gradle | 1 + forge/src/main/resources/META-INF/mods.toml | 2 +- gradle.properties | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 699af0f6..daa9077e 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,7 @@ subprojects { "maven_group": rootProject.maven_group, "create_forge_version": rootProject.create_forge_version, "main_class_name": rootProject.main_class_name, + "create_forge_version_dependency": rootProject.create_forge_version_dependency, ] filesMatching(['pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'createrailwaysnavigator.mixins.json']) { expand expandProps diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index 781ccc9e..48d95da2 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -37,6 +37,6 @@ side = "BOTH" [[dependencies.${modid}]] modId = "create" mandatory = true -versionRange = "[${create_forge_version},)" +versionRange = "[${create_forge_version_dependency},)" ordering = "BEFORE" side = "BOTH" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 56ad0c6c..dac97b84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -40,5 +40,6 @@ modmenu_version = 4.1.2 forge_config_api_port_version = 4.2.11 create_fabric_version = 0.5.1-f-build.1416+mc1.19.2 create_forge_version = 0.5.1.f-46 +create_forge_version_dependency = 0.5.1.f registrate_forge_version = MC1.19-1.1.5 flywheel_forge_version = 0.6.10-20 \ No newline at end of file From 4ffb9cb4d56b8a63548fe5a2ad91af3d08442bd7 Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Tue, 17 Dec 2024 16:32:53 +0100 Subject: [PATCH 09/10] 1.19.2 Port --- .../block/display/AdvancedDisplaySource.java | 1 - .../block/display/AdvancedDisplayTarget.java | 2 +- .../AbstractAdvancedDisplayRenderer.java | 1 - .../ber/variants/BERPlatformDetailed.java | 2 +- .../ber/variants/BERPlatformSimple.java | 12 +-- .../screen/TrainSeparationSettingsScreen.java | 71 ++++++++++++++-- .../mrjulsen/crn/config/ModClientConfig.java | 8 +- .../de/mrjulsen/crn/data/ETimeSource.java | 39 +++++++++ .../condition/TrainSeparationCondition.java | 34 +++++++- .../de/mrjulsen/crn/data/train/TrainData.java | 8 +- .../crn/data/train/TrainListener.java | 81 ++++++++++++------- .../events/TrainArrivalAndDepartureEvent.java | 6 +- .../de/mrjulsen/crn/mixin/TrainMixin.java | 6 +- .../createrailwaysnavigator/lang/de_de.json | 2 +- .../createrailwaysnavigator/lang/en_us.json | 3 +- gradle.properties | 4 +- 16 files changed, 218 insertions(+), 62 deletions(-) create mode 100644 common/src/main/java/de/mrjulsen/crn/data/ETimeSource.java diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java index d7d7fcd4..8e3d81cc 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java +++ b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplaySource.java @@ -7,7 +7,6 @@ import com.simibubi.create.content.redstone.displayLink.source.DisplaySource; import com.simibubi.create.content.redstone.displayLink.target.DisplayTargetStats; import com.simibubi.create.content.trains.station.GlobalStation; -import com.simibubi.create.content.trains.station.StationBlockEntity; import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; import com.simibubi.create.foundation.utility.Lang; diff --git a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java index e71ec518..c494913b 100644 --- a/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java +++ b/common/src/main/java/de/mrjulsen/crn/block/display/AdvancedDisplayTarget.java @@ -51,7 +51,7 @@ public static void start() { try { workerTasks.poll().run(); } catch (Exception e) { - CreateRailwaysNavigator.LOGGER.info("Error while process Advanced Display Data.", e); + CreateRailwaysNavigator.LOGGER.info("Error while process Advanced Display Data. " + e.getMessage(), e); } } try { diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java index 62cab4a9..07e7b7d8 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/AbstractAdvancedDisplayRenderer.java @@ -18,5 +18,4 @@ default T getDisplaySettings(AdvancedDisplayBlockEntity blockEntity) { throw new IllegalArgumentException("Could not get display data of display at " + blockEntity.getBlockPos(), e); } } - } diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java index 61b3d84f..2855f9f9 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformDetailed.java @@ -98,7 +98,7 @@ public void render(BERGraphics graphics, float pPart @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getScheduledArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); + List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getRealTimeArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); showInfoLine = !preds.isEmpty() && preds.get(0).getStationData().isDepartureDelayed() && preds.get(0).getTrainData().hasStatusInfo(); if (showInfoLine) { diff --git a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java index c607cbeb..0a6d6efc 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java +++ b/common/src/main/java/de/mrjulsen/crn/client/ber/variants/BERPlatformSimple.java @@ -65,7 +65,7 @@ public void render(BERGraphics graphics, float pPart @Override public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayBlockEntity blockEntity, AdvancedDisplayRenderInstance parent, EUpdateReason reason) { - List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getScheduledArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); + List preds = blockEntity.getStops().stream().filter(x -> x.getStationData().getRealTimeArrivalTime() < DragonLib.getCurrentWorldTime() + ModClientConfig.DISPLAY_LEAD_TIME.get() && (!x.getTrainData().isCancelled() || DragonLib.getCurrentWorldTime() < x.getStationData().getScheduledDepartureTime() + ModClientConfig.DISPLAY_LEAD_TIME.get())).toList(); label .setColor((0xFF << 24) | (getDisplaySettings(blockEntity).getFontColor() & 0x00FFFFFF)) @@ -88,10 +88,12 @@ public void update(Level level, BlockPos pos, BlockState state, AdvancedDisplayB text.append(", ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.cancelled2").getString()); } else if (x.getStationData().isDepartureDelayed()) { String delay = getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ETA ? ModUtils.timeRemainingString(x.getStationData().getDepartureTimeDeviation()) : String.valueOf(TimeUtils.formatToMinutes(x.getStationData().getDepartureTimeDeviation())); - text.append(", ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed2", delay).getString()); - if (getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ABS) { - text.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delay_abs_suffix")); - } + String timeUnitSuffix = getDisplaySettings(blockEntity).getTimeDisplay() == ETimeDisplay.ABS ? + " " + CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delay_abs_suffix").getString() : + ""; + + text.append(", ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.delayed2", delay, timeUnitSuffix).getString()); + if (x.getTrainData().hasStatusInfo()) { text.append(" ").append(CustomLanguage.translate("block." + CreateRailwaysNavigator.MOD_ID + ".advanced_display.ber.reason").getString()).append(x.getTrainData().getStatus().get(0).text()); } diff --git a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java index ddebd7d5..52e59bcd 100644 --- a/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java +++ b/common/src/main/java/de/mrjulsen/crn/client/gui/screen/TrainSeparationSettingsScreen.java @@ -56,24 +56,30 @@ public class TrainSeparationSettingsScreen extends DLScreen { private final CompoundTag nbt; //private String stationFilter; - private int time = 5; - private TimeUnit timeUnit = TimeUnit.SECONDS; + private int minutes = 0; + private int seconds = 5; + private int ticks = 0; private ETrainFilter filter = ETrainFilter.ANY; public TrainSeparationSettingsScreen(Screen lastScreen, CompoundTag nbt) { super(title); this.lastScreen = lastScreen; this.nbt = nbt; - this.time = nbt.getInt(TrainSeparationCondition.NBT_TIME); - this.timeUnit = TimeUnit.values()[nbt.getInt(TrainSeparationCondition.NBT_TIME_UNIT)]; + @SuppressWarnings("deprecation") + int t = nbt.contains(TrainSeparationCondition.NBT_TICKS) ? nbt.getInt(TrainSeparationCondition.NBT_TICKS) : nbt.getInt(TrainSeparationCondition.NBT_TIME) * TimeUnit.values()[nbt.getInt(TrainSeparationCondition.NBT_TIME_UNIT)].ticksPer; + ticks = t; + minutes = ticks / 1200; + ticks %= 1200; + seconds = ticks / 20; + ticks %= 20; + this.filter = ETrainFilter.getByIndex(nbt.getByte(TrainSeparationCondition.NBT_TRAIN_FILTER)); } @Override public void onClose() { super.onClose(); - nbt.putInt(TrainSeparationCondition.NBT_TIME, time); - nbt.putInt(TrainSeparationCondition.NBT_TIME_UNIT, timeUnit.ordinal()); + nbt.putInt(TrainSeparationCondition.NBT_TICKS, minutes * TimeUnit.MINUTES.ticksPer + seconds * TimeUnit.SECONDS.ticksPer + ticks); nbt.putByte(TrainSeparationCondition.NBT_TRAIN_FILTER, filter.getIndex()); Minecraft.getInstance().setScreen(lastScreen); } @@ -99,7 +105,55 @@ protected void init() { }); }); */ + + /* + builder.addLine("time_src", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TIME.getAsSprite(16, 16))); + line.add(new DLCreateSelectionScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, line.getRemainingWidth() - 6, 18) + .setRenderArrow(true) + .forOptions(Arrays.stream(ETimeSource.values()).map(x -> TextUtils.translate(x.getValueTranslationKey(CreateRailwaysNavigator.MOD_ID))).toList()) + .titled(TextUtils.translate(timeSource.getEnumTranslationKey(CreateRailwaysNavigator.MOD_ID))) + .setState(timeSource.getIndex()) + .calling((i) -> { + timeSource = ETimeSource.getByIndex(i); + }) + ); + }); + */ + builder.addLine("times", (line) -> { + line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TIME.getAsSprite(16, 16))); + line.add(new DLCreateScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, 30, 18) + .setRenderArrow(true) + .titled(Lang.translateDirect("generic.unit.minutes")) + .withShiftStep(10) + .withRange(0, 901) + .setState(minutes) + .calling(x -> { + minutes = x; + }) + ); + line.add(new DLCreateScrollInput(this, line.getCurrentX() + 2, line.getY() + 2, 30, 18) + .titled(Lang.translateDirect("generic.unit.seconds")) + .withShiftStep(10) + .withRange(0, 60) + .setState(seconds) + .calling(x -> { + seconds = x; + }) + ); + line.add(new DLCreateScrollInput(this, line.getCurrentX() + 2, line.getY() + 2, 30, 18) + .titled(Lang.translateDirect("generic.unit.ticks")) + .withShiftStep(5) + .withRange(0, 20) + .setState(ticks) + .calling(x -> { + ticks = x; + }) + ); + }); + + /* builder.addLine("time", (line) -> { line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TIME.getAsSprite(16, 16))); line.add(new DLCreateScrollInput(this, line.getCurrentX() + 6, line.getY() + 2, 30, 18) @@ -107,9 +161,9 @@ protected void init() { .titled(Lang.translateDirect("generic.duration")) .withShiftStep(15) .withRange(0, 121) - .setState(time) + .setState(ticks) .calling(x -> { - time = x; + ticks = x; }) ); @@ -123,6 +177,7 @@ protected void init() { }) ); }); + */ builder.addLine("train_filter", (line) -> { line.add(new IconSlotWidget(line.getCurrentX(), line.y() + 2, ModGuiIcons.TRAIN.getAsSprite(16, 16))); diff --git a/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java b/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java index a6ca273b..7aced8a4 100644 --- a/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java +++ b/common/src/main/java/de/mrjulsen/crn/config/ModClientConfig.java @@ -28,12 +28,12 @@ public class ModClientConfig { BUILDER.push("Create Railways Navigator Config"); /* CONFIGS */ - NEXT_STOP_ANNOUNCEMENT = BUILDER.comment(new String[] {"[in Ticks]", "The next stop or information about the start of the journey is announced in the specified number of ticks before the scheduled arrival at the next station. (Default: 500, 30 real life seconds)"}) - .defineInRange("general.next_stop_announcement", 500, 100, 1000); + NEXT_STOP_ANNOUNCEMENT = BUILDER.comment(new String[] {"[in Ticks]", "The next stop or information about the start of the journey is announced in the specified number of ticks before the scheduled arrival at the next station. (Default: 600, 30 real life seconds)"}) + .defineInRange("general.next_stop_announcement", 600, 100, 1000); REALTIME_PRECISION_THRESHOLD = BUILDER.comment(new String[] {"[in Ticks]", "This value indicates how accurately the real-time data should be displayed. By default, only deviations above 10 in-game minutes (167 ticks, approx. 8 real life seconds) are displayed. The lower the value, the more accurate the real-time data but also the more often deviations from the schedule occur. (Default: 167, 10 in-game minutes)"}) .defineInRange("general.realtime_precision_threshold", 167, 1, 1000); - DISPLAY_LEAD_TIME = BUILDER.comment(new String[] {"[in Ticks]", "How early a train should be shown on the display. (Default: 1000, 1 in-game hour)"}) - .defineInRange("general.display_lead_time", 1000, 100, 24000); + DISPLAY_LEAD_TIME = BUILDER.comment(new String[] {"[in Ticks]", "How early a train should be shown on the display. (Default: 1200, 1 real life minute)"}) + .defineInRange("general.display_lead_time", 1200, 100, 24000); OVERLAY_SCALE = BUILDER.comment("Scale of the route overlay UI. (Default: 0.75)") .defineInRange("route_overlay.scale", 0.75f, MIN_SCALE, MAX_SCALE); ROUTE_NOTIFICATIONS = BUILDER.comment("If active, you will receive short toasts about important events on your trip, e.g. delays, changes, ... (Default: ON)") diff --git a/common/src/main/java/de/mrjulsen/crn/data/ETimeSource.java b/common/src/main/java/de/mrjulsen/crn/data/ETimeSource.java new file mode 100644 index 00000000..ad938aca --- /dev/null +++ b/common/src/main/java/de/mrjulsen/crn/data/ETimeSource.java @@ -0,0 +1,39 @@ +package de.mrjulsen.crn.data; + +import java.util.Arrays; +import de.mrjulsen.mcdragonlib.core.ITranslatableEnum; + +public enum ETimeSource implements ITranslatableEnum { + REAL_LIFE((byte)0, "real_life"), + IN_GAME((byte)1, "in_game"); + + final byte index; + final String name; + + ETimeSource(byte index, String name) { + this.index = index; + this.name = name; + } + + public byte getIndex() { + return index; + } + + public String getName() { + return name; + } + + public static ETimeSource getByIndex(int index) { + return Arrays.stream(values()).filter(x -> x.getIndex() == index).findFirst().orElse(REAL_LIFE); + } + + @Override + public String getEnumName() { + return "time_source"; + } + + @Override + public String getEnumValueName() { + return name; + } +} diff --git a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java index 502b8757..a62ef544 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java +++ b/common/src/main/java/de/mrjulsen/crn/data/schedule/condition/TrainSeparationCondition.java @@ -11,6 +11,7 @@ import de.mrjulsen.crn.CreateRailwaysNavigator; import de.mrjulsen.crn.client.ClientWrapper; +import de.mrjulsen.crn.data.ETimeSource; import de.mrjulsen.crn.data.schedule.IConditionsRequiresInstruction; import de.mrjulsen.crn.data.schedule.INavigationExtension; import de.mrjulsen.crn.data.train.StationDepartureHistory; @@ -29,13 +30,16 @@ public class TrainSeparationCondition extends ScheduledDelay implements IDelayedWaitCondition, IConditionsRequiresInstruction { - public static final String NBT_TIME = "Value"; + @Deprecated public static final String NBT_TIME = "Value"; + @Deprecated public static final String NBT_TIME_UNIT = "TimeUnit"; + public static final String NBT_TICKS = "Ticks"; public static final String NBT_TRAIN_FILTER = "TrainFilter"; - public static final String NBT_TIME_UNIT = "TimeUnit"; + public static final String NBT_TIME_SOURCE = "TimeSource"; public TrainSeparationCondition() { super(); data.putByte(NBT_TRAIN_FILTER, ETrainFilter.ANY.getIndex()); + data.putInt(NBT_TICKS, 100); } @Override @@ -48,6 +52,28 @@ public ItemStack getSecondLineIcon() { return new ItemStack(Items.OBSERVER); } + @Override + public int totalWaitTicks() { + if (data.contains(NBT_TICKS)) { + return data.getInt(NBT_TICKS); + } + return super.totalWaitTicks(); + } + + @Override + protected Component formatTime(boolean compact) { + int remainingTicks = totalWaitTicks(); + int minutes = remainingTicks / 1200; + remainingTicks %= 1200; + int seconds = remainingTicks / 20; + remainingTicks %= 20; + + if (compact) { + return TextUtils.text(String.format("%d:%02d,%02d", minutes, seconds, remainingTicks)); + } + return TextUtils.text(String.format("%dm %ds %dt", minutes, seconds, remainingTicks)); + } + @Override public List getTitleAs(String type) { return ImmutableList.of( @@ -94,6 +120,10 @@ public ETrainFilter getTrainFilter() { return ETrainFilter.getByIndex(data.getByte(NBT_TRAIN_FILTER)); } + public ETimeSource getTimeSource() { + return ETimeSource.getByIndex(data.getByte(NBT_TIME_SOURCE)); + } + @Override @Environment(EnvType.CLIENT) public void initConfigurationWidgets(ModularGuiLineBuilder builder) { diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java index 3cd094b2..c73d57d1 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainData.java @@ -67,7 +67,12 @@ public class TrainData implements IListenable { private transient final Cache defaultSection = new Cache<>(() -> TrainTravelSection.def(this), ECachingPriority.LOW); private transient final List predictionsChronologically = new LockedList<>(); private transient final Set validPredictionEntries = new HashSet<>(); - private transient final Cache isDynamic = new Cache<>(() -> getTrain().runtime.getSchedule().entries.stream().anyMatch(x -> x.conditions.stream().flatMap(y -> y.stream()).anyMatch(y -> y instanceof DynamicDelayCondition c && c.minWaitTicks() < c.totalWaitTicks()))); + private transient final Cache isDynamic = new Cache<>(() -> + getTrain() != null && + getTrain().runtime != null && + getTrain().runtime.getSchedule() != null && + getTrain().runtime.getSchedule().entries.stream().anyMatch(x -> x.conditions.stream().flatMap(y -> y.stream()).anyMatch(y -> y instanceof DynamicDelayCondition c && c.minWaitTicks() < c.totalWaitTicks())) + ); private int currentScheduleIndex = INVALID; private transient int currentTravelSectionIndex = INVALID; @@ -635,6 +640,7 @@ public void leaveDestination() { public void onInitialize() { updateTotalDuration(); + isDynamic.clear(); } /** Checks and calculates a new total duration time if necessary. */ diff --git a/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java b/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java index efdf7881..46e26bdd 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java +++ b/common/src/main/java/de/mrjulsen/crn/data/train/TrainListener.java @@ -14,6 +14,7 @@ import com.simibubi.create.content.trains.entity.Train; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.schedule.INavigationExtension; import de.mrjulsen.crn.data.storage.GlobalSettings; import de.mrjulsen.crn.event.CRNEventsManager; @@ -48,49 +49,67 @@ public static void init() { // Register Event Listeners CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPre.class).register(CreateRailwaysNavigator.MOD_ID, () -> { queueTrainListenerTask(() -> { - StationDepartureHistory.cleanUpDepartureHistory(); - TrainListener.refreshPre(); + try { + StationDepartureHistory.cleanUpDepartureHistory(); + TrainListener.refreshPre(); + } catch (Exception e) { + DragonLib.LOGGER.error("Cannot run train listener task 'TrainListener#GlobalTrainDisplayDataRefreshEventPre': " + e.getMessage(), e); + } }); }); CRNEventsManager.getEvent(GlobalTrainDisplayDataRefreshEventPost.class).register(CreateRailwaysNavigator.MOD_ID, () -> { - TrainUtils.refreshCache(); - queueTrainListenerTask(TrainListener::refreshPost); + queueTrainListenerTask(() -> { + try { + TrainUtils.refreshCache(); + TrainListener.refreshPost(); + } catch (Exception e) { + DragonLib.LOGGER.error("Cannot run train listener task 'TrainListener#GlobalTrainDisplayDataRefreshEventPost': " + e.getMessage(), e); + } + }); }); CRNEventsManager.getEvent(TrainDestinationChangedEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, current, next, nextIndex) -> { }); CRNEventsManager.getEvent(TotalDurationTimeChangedEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, old, newDuration) -> { - CreateRailwaysNavigator.LOGGER.warn("The total duration of the train " + train.name.getString() + " (" + train.id + ") has changed from " + old + " Ticks to " + newDuration + " Ticks. This will result in changes to the scheduled departure times!"); + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("The total duration of the train " + train.name.getString() + " (" + train.id + ") has changed from " + old + " Ticks to " + newDuration + " Ticks. This will result in changes to the scheduled departure times!"); }); CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, station, isArrival) -> { queueTrainListenerTask(() -> { - if (data.containsKey(train.id)) { - if (isArrival) { - data.get(train.id).reachDestination(DragonLib.getCurrentWorldTime(), ((ScheduleRuntimeAccessor)train.runtime).crn$getTicksInTransit()); - } else { - data.get(train.id).leaveDestination(); + try { + if (data.containsKey(train.id) && train.runtime != null) { + if (isArrival) { + data.get(train.id).reachDestination(DragonLib.getCurrentWorldTime(), ((ScheduleRuntimeAccessor)train.runtime).crn$getTicksInTransit()); + } else { + data.get(train.id).leaveDestination(); + } } - } - - if (!isArrival && !((INavigationExtension)(Object)train.navigation).isDelayedWaitConditionPending()) { - // If not checking whether a delayed condition is pending, the train would block itself. - StationDepartureHistory.updateDepartureHistory(train, station.name); - } + + if (!isArrival && train.navigation != null && station.isPresent() && !((INavigationExtension)(Object)train.navigation).isDelayedWaitConditionPending()) { + // If not checking whether a delayed condition is pending, the train would block itself. + StationDepartureHistory.updateDepartureHistory(train, station.get().name); + } + } catch (Exception e) { + DragonLib.LOGGER.error("Cannot run train listener task 'TrainListener#TrainArrivalAndDepartureEvent': " + e.getMessage(), e); + } }); }); CRNEventsManager.getEvent(ScheduleResetEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, soft) -> { queueTrainListenerTask(() -> { - if (data.containsKey(train.id)) { - TrainData trainData = data.get(train.id); - if (soft) { - trainData.resetPredictions(); - } else { - trainData.hardResetPredictions(); + try { + if (data.containsKey(train.id)) { + TrainData trainData = data.get(train.id); + if (soft) { + trainData.resetPredictions(); + } else { + trainData.hardResetPredictions(); + } } + } catch (Exception e) { + DragonLib.LOGGER.error("Cannot run train listener task 'TrainListener#ScheduleResetEvent': " + e.getMessage(), e); } }); }); @@ -100,13 +119,17 @@ public static void init() { }); CRNEventsManager.getEvent(CreateTrainPredictionEvent.class).register(CreateRailwaysNavigator.MOD_ID, (train, schedule, predictables, index, stayDuration, minStayDuration, prediction) -> { - queueTrainListenerTask(() -> { - ScheduleRuntimeAccessor accessor = (ScheduleRuntimeAccessor)(Object)schedule; - UUID trainId = accessor.crn$getTrain().id; - if (data.containsKey(trainId) && prediction != null) { - TrainData trainData = data.get(trainId); - TrainPrediction pred = trainData.setPredictionData(index, schedule.currentEntry, schedule.getSchedule().entries.size(), stayDuration, minStayDuration, accessor.crn$predictionTicks().get(index), prediction); - predictables.values().forEach(x -> x.predictForStation(trainData, pred, schedule, index, accessor.crn$getTrain())); + queueTrainListenerTask(() -> { + try { + ScheduleRuntimeAccessor accessor = (ScheduleRuntimeAccessor)(Object)schedule; + UUID trainId = accessor.crn$getTrain().id; + if (data.containsKey(trainId) && prediction != null) { + TrainData trainData = data.get(trainId); + TrainPrediction pred = trainData.setPredictionData(index, schedule.currentEntry, schedule.getSchedule().entries.size(), stayDuration, minStayDuration, accessor.crn$predictionTicks().get(index), prediction); + predictables.values().forEach(x -> x.predictForStation(trainData, pred, schedule, index, accessor.crn$getTrain())); + } + } catch (Exception e) { + DragonLib.LOGGER.error("Cannot run train listener task 'TrainListener#CreateTrainPredictionEvent': " + e.getMessage(), e); } }); }); diff --git a/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java b/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java index 40bcde92..7797a5b7 100644 --- a/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java +++ b/common/src/main/java/de/mrjulsen/crn/event/events/TrainArrivalAndDepartureEvent.java @@ -1,12 +1,14 @@ package de.mrjulsen.crn.event.events; +import java.util.Optional; + import com.simibubi.create.content.trains.entity.Train; import com.simibubi.create.content.trains.station.GlobalStation; import de.mrjulsen.crn.event.CRNEventsManager.AbstractCRNEvent; public final class TrainArrivalAndDepartureEvent extends AbstractCRNEvent { - public void run(Train train, GlobalStation current, boolean arrival) { + public void run(Train train, Optional current, boolean arrival) { listeners.values().forEach(x -> x.run(train, current, arrival)); tickPost(); } @@ -18,6 +20,6 @@ public static interface ITrainApprochEventData { * @param current The current station. * @param departure {@code true} if the train is arriving at the current station, {@code false} when leaving. */ - void run(Train train, GlobalStation current, boolean arrival); + void run(Train train, Optional current, boolean arrival); } } diff --git a/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java b/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java index d7852c88..eb7ed34e 100644 --- a/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java +++ b/common/src/main/java/de/mrjulsen/crn/mixin/TrainMixin.java @@ -1,5 +1,7 @@ package de.mrjulsen.crn.mixin; +import java.util.Optional; + import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -22,14 +24,14 @@ public Train self() { @Inject(method = "arriveAt", remap = false, at = @At(value = "TAIL")) public void onArriveAt(GlobalStation station, CallbackInfo ci) { if (CRNEventsManager.isRegistered(TrainArrivalAndDepartureEvent.class)) { - CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).run(self(), station, true); + CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).run(self(), Optional.ofNullable(station), true); } } @Inject(method = "leaveStation", remap = false, at = @At(value = "TAIL"), locals = LocalCapture.CAPTURE_FAILHARD) public void onLeaveStation(CallbackInfo ci, GlobalStation currentStation) { if (CRNEventsManager.isRegistered(TrainArrivalAndDepartureEvent.class)) { - CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).run(self(), currentStation, false); + CRNEventsManager.getEvent(TrainArrivalAndDepartureEvent.class).run(self(), Optional.ofNullable(currentStation), false); } } } diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json index 5765e4d8..75a47c43 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/de_de.json @@ -377,7 +377,7 @@ "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information zu %s: Fällt heute aus!", "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information zu %s: Verspätung ca. %s", "block.createrailwaysnavigator.advanced_display.ber.cancelled2": "fällt heute aus. Wir bitten um Entschuldigung.", - "block.createrailwaysnavigator.advanced_display.ber.delayed2": "heute circa %s später.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": "heute circa %s%s später.", "block.createrailwaysnavigator.advanced_display.ber.reason": "Grund dafĂźr ist eine ", "gui.createrailwaysnavigator.route_widget.show_details": "Details ansehen", "gui.createrailwaysnavigator.route_widget.save": "Speichern", diff --git a/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json b/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json index f51299f8..a7f7869a 100644 --- a/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json +++ b/common/src/main/resources/assets/createrailwaysnavigator/lang/en_us.json @@ -376,9 +376,8 @@ "block.createrailwaysnavigator.advanced_display.ber.delayed": "Delay approx. %s", "block.createrailwaysnavigator.advanced_display.ber.information_about_cancelled": "Information about %s: This train got cancelled", "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed": "Information about %s: Delay approx. %s", - "block.createrailwaysnavigator.advanced_display.ber.information_about_delayed_with_unit": "Information about %s: Delay approx. %s", "block.createrailwaysnavigator.advanced_display.ber.cancelled2": "is cancelled today. We apologize for the inconvenience.", - "block.createrailwaysnavigator.advanced_display.ber.delayed2": "today approx. %s delayed.", + "block.createrailwaysnavigator.advanced_display.ber.delayed2": "today approx. %s%s delayed.", "block.createrailwaysnavigator.advanced_display.ber.reason": "Reason: ", "gui.createrailwaysnavigator.route_widget.show_details": "Show details", "gui.createrailwaysnavigator.route_widget.save": "Save", diff --git a/gradle.properties b/gradle.properties index dac97b84..912fc6c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx8G org.gradle.parallel=true # Mod properties -mod_version = 0.7.0 +mod_version = 0.7.1 release_channel = beta maven_group = de.mrjulsen.crn archives_name = createrailwaysnavigator @@ -35,7 +35,7 @@ fabric_api_version = 0.77.0+1.19.2 forge_version = 1.19.2-43.3.13 forge_version_int = 43 -dragonlib_version = 2.2.21 +dragonlib_version = 2.2.22 modmenu_version = 4.1.2 forge_config_api_port_version = 4.2.11 create_fabric_version = 0.5.1-f-build.1416+mc1.19.2 From 2717346fcb9240f51caaa7a33dc4ca5a8d4ddf4e Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Tue, 17 Dec 2024 20:19:57 +0100 Subject: [PATCH 10/10] Updated logging --- .../crn/data/navigation/ClientRoute.java | 4 ++-- .../crn/data/navigation/ClientRoutePart.java | 4 ++-- .../crn/data/navigation/NavigatableGraph.java | 24 +++++++++++-------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java index 7918695a..4b3af1f3 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoute.java @@ -652,8 +652,8 @@ public void closeAll() { CRNEventsManager.getEvent(DefaultTrainDataRefreshEvent.class).unregister(CreateRailwaysNavigator.MOD_ID + "_" + id); clearEvents(); isClosed = true; - CreateRailwaysNavigator.LOGGER.info("Route listener closed."); - if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Closed " + this); + + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Route listener closed."); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java index 5b928a50..92f8e272 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/ClientRoutePart.java @@ -14,6 +14,7 @@ import com.google.common.collect.ImmutableList; import de.mrjulsen.crn.CreateRailwaysNavigator; +import de.mrjulsen.crn.config.ModCommonConfig; import de.mrjulsen.crn.data.train.ClientTrainStop; import de.mrjulsen.crn.data.train.RoutePartProgressState; import de.mrjulsen.crn.data.train.TrainStop; @@ -272,7 +273,7 @@ public void update(TrainRealTimeData data) { } if (isCancelled()) { - CreateRailwaysNavigator.LOGGER.info("Train got cancelled. Closing route..."); + if (ModCommonConfig.ADVANCED_LOGGING.get()) CreateRailwaysNavigator.LOGGER.info("Train got cancelled. Closing route..."); notifyListeners(EVENT_TRAIN_CANCELLED, new ListenerNotificationData(this, nextStop)); close(); } @@ -304,7 +305,6 @@ public void close() { } } stopListeningAll(this); - CreateRailwaysNavigator.LOGGER.info("CLOSED " + this); } diff --git a/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java b/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java index 8497f60d..f618e607 100644 --- a/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java +++ b/common/src/main/java/de/mrjulsen/crn/data/navigation/NavigatableGraph.java @@ -65,12 +65,14 @@ public NavigatableGraph(UserSettings userSettings) throws RuntimeSideException { addTrain(train, TrainListener.data.get(train.id)); } - CreateRailwaysNavigator.LOGGER.info(String.format("Graph generated. Took %sms. Contains %s nodes and %s edges. %s train processed.", - System.currentTimeMillis() - startTime, - nodesByTag.size(), - edgesByTag.values().stream().flatMap(x -> x.values().stream().flatMap(y -> y.stream())).count(), - trains.size() - )); + if (ModCommonConfig.ADVANCED_LOGGING.get()) { + CreateRailwaysNavigator.LOGGER.info(String.format("Graph generated. Took %sms. Contains %s nodes and %s edges. %s train processed.", + System.currentTimeMillis() - startTime, + nodesByTag.size(), + edgesByTag.values().stream().flatMap(x -> x.values().stream().flatMap(y -> y.stream())).count(), + trains.size() + )); + } } protected GlobalSettings globalSettings() { @@ -419,10 +421,12 @@ public static List searchRoutes(StationTag start, StationTag destination, int minNumber = routes.stream().mapToInt(x -> x.getTransferCount()).min().orElse(0); routes = routes.stream().filter(x -> x.getTransferCount() == minNumber).toList(); - CreateRailwaysNavigator.LOGGER.info(String.format("%s route(s) calculated. Took %sms.", - routes.size(), - System.currentTimeMillis() - startTime - )); + if (ModCommonConfig.ADVANCED_LOGGING.get()) { + CreateRailwaysNavigator.LOGGER.info(String.format("%s route(s) calculated. Took %sms.", + routes.size(), + System.currentTimeMillis() - startTime + )); + } return routes.stream().sorted((a, b) -> Long.compare(a.getStart().getScheduledDepartureTime(), b.getStart().getScheduledDepartureTime())).toList(); }