From 78ce18135b28b61d2d77383ed97d8254183ea387 Mon Sep 17 00:00:00 2001 From: MrJulsen Date: Sat, 12 Oct 2024 12:48:23 +0200 Subject: [PATCH] 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 zcmeHKYfuzd7H$-I3W9*_DipngMv(MOchkc#BLfkJ#{>sQUZRqOp6=U2$9Xx^FayMh zNK!EdT-cZgJ|c-pT-n7WLVTcNd<0D@(G|-FMwhkJm_#wI35oB$Jv_wPRBchU`NvfC zbl-E&`OZ1tJ?GZFtMoaU0e(~c5CjR(W@&Qady@G0c)|bKr`A3VUwxJNMM5s^L~V8} z$D05uRM-FtTs(&$uAaL2U3G`>iT7Gw#a3unTt4EvGqCmEw8d?EpDkGtjlLYLtN)z8 zfB)=>B?`Xr$~RsG|4tL72kn`M4M!zxjAoBGcQ<$x>V4?4`?E!D>j_R(z`s2Iu=7#o&!+{|3 zK*)XnY!%bhe>hwJ#OipGw-`T2}Tf)T0SjJuT4uE${gmqep!P$ z>)=xnho8%Pe^!C_?5wgQ`q^jG(<8FOR+lulg)J(>cm3qwL`{#C*4Hfg?6Wn8x2{~h zy*0442L09J;klsnX^IRVY|N5z$ID(gfAHYx4xeM+zu0$-*m6nd<`$#NtP2f^?O9WH(>ro`V==kwi;_!4 zPle5z-ZklgcV~y(zdO&;nBw)*zBBz@p1tQLG?kruko@WOE55zUyjt5n?d(CWBx|?r zXg?l|?+GX_-XX7;wDTK}jCX{ie~vhsofi{Y{Y(6v4QWqJhp1@mmPOiRd;`x+tL};v&Vk?VngBQutLjuScyftxhlqLmeQ=%0HiLn4VEZ^ zB&E1)G_wQ(NtXi@_uETYB$o;T zn1Mi}F0;wvP`T6+H?9hPi(;7sb*l(V)RH2d9!;~_0ZK_JDUPMP_;ON`?1v`VSx%L! z$ryxyBekSd5Ns-$%;|JWo$*qu-6$iJN~H`ZWh99~4a`wt5oi}?aYTy{0~i|MVC=k2 z;H?%^#H0<@GC?hozGCgkYIrds~^^P}Kpw5|6pe)avxZ79s^k-fVMQ zL9!py6nO3tS&zjg&bZSV83;5V#(hkCD0jCq)Y9ox8Y@#KhNso2C8B>7Yh`#=<^Cjy zL^@HWhKvU|!x3_71jIZ$537mBtEG^I)S^y9fCn*)KRFR5&B0)teRdPHI zCsp_eeJjgz75__HY#%gfV9~R92lQXz77g@N9#}ds8W@^*cQc`=yDd~SGmwIVE(feT zPKY%y#gx((BY^wIV8IT_`QIo83J0Xzpy03sS_xn=P&Agvl1hxA8I~dy@dN+^Sq-B* zteoJa?O=`(@(8(t1?uJsjTv03*vF%BmI84dAY~Y?#70mikqss*6L*ZEY?EaF#Yd7` zVZ;^#?FM9U>w-I>Y9*=GcbDuj&ur6Kgx$jy~V)ztuCY-y_I);o-ua+iDXxr3Qz#pZ(JFZN&Rj?&~RQ z)Xcf1)||VC)<+&6pLw3K_j?ujc;ep4uQ4AkuO5$|nL9y0KKp!jiD}#pU&K_c#$Wft zClro5X~4Vhh6hzQGziFUNq4dKxuEN9H5cN3xE98Hyzsqa)~f|mc6v<(&mjMpRyW5Z zBWV%v3%|6yrR0)&wePiG-%-kz?Ty(`u&LP5qA}k7d4F#KQXO@@?z_K+9L@+S92Z{V zA91Vj{_~x)(iSc7IA`3xzNs^zB>V34tCEo_WU! zbyR79Xq;ZkDs5`PitITr%HR1w1Z>_NgltjqX8YQ;ox9r$MjFO_@s+`4>2!mzJ;cz~ zSj&{$id@}CkKfa88TBMTZE;zo6wY)gbhU=H@42FmILqz zGk)Fc?ipAe5%1rRO{9r-t8GuWv-M*<%SLq%OnC<5Rg=c@vilX}J)Oz{RZ#lQ*OxAd z~ZU(srGN+7)hsYEI96y znZt0RGH*x2Wb~GffT9mWh>;L^2wbnM>bX3#y)5#1DUOwpV9eGt{KhHRihV69>BKg8 z3kn^Lu=h6kVphbu;hl$QT}wGNov>txE<93RxR=0u>9oB_lH0HBrX;IhxJ+-&4h=mdQRUd{H`>f@ z?-5*nL%(c^NfGw8l~3e6dCI$;JGNUW_={aJ?xKVIuPdHif~w4?(_It9>ek0W6$q`FBor)?aNPH&bAVrXA>J7Iin#=1IHb$sC9(mAthHW zIvjt@NqZR*NJ!Aw%}_G*z8l})2Ign zuye(U?NZBj|HBTL^d-`#a#wtc-a=JFQ~grHC&0g5Ic705gf?{!Tx@K8m^s+AmRdge z1~rH~+5LT4of`l~wXs1Vi6IAl8Bt%?Jg6w*0zArWlq{bXgY&L2qNGacoKs^-GwL0q}td& ziFM7|6`Ha(1bYWTK#t-Ggj|S6~WWr`^nG(U@L7)(+Q8Zo@gy&HN5ShOT(rFYrB#7dYNW3UMK}f~_fhGqLTo@EG zgF^fdO%}LB(S!mX#1&G75SdD$LRz-)TDZ2U~UFx-m%1%iG(k z0RY;(13q40<8xeZs*uV0jjVN$)<{h>l?jE&G`Bj4f>~Z3K`DKcgNk_daZ}|m&1aiY zMptKmbbjRvrBtb zMtov`#m4GNXGyZHt#+ozR%OQUiC)%nYn-huBeUhYS$^w9>T3C$X?u(G^4LO&q-x$E zH8mA;k-i$uDB8I;Z-vGF^zhX5h;_F4oSjT+YHCw;xBi3k!@=skUYK`{81b`mb?jV; zBt!DZUFV^oLTt8KWG9+wGen^-i|lQrx9RR=p9H0U7_rzw4$uxZ7 zsu<^9B$2dmp7(C6jiFm6|AvOJrqYyc(V7!~V3nR2d;yfL|?=Z<&y)if#z9;=sr;r3Y5s+{P z4G@?PAP%kp=YNny)R=6!GZS*;5)JP04)(8)XA%+<69uWE1uTT}g*8kfk;{K@dKPzo z&q|A~vK(Oz7v?;>~?4`8Wp(1IV|`gw-s*ydmz9_=b98q8*qRvqUix5 zQaL@X1&CoVFgaESgfLgMwf0F4ZVp;dZZ z9r-NFr$LKutua2dyxhBg(}&aL;S%H!X2=|YKmxdQ5rWTvono@x4_;mcoQ{ zp>m_E>!MXlRaaLRb)AO;aqP7!fY$`aRJs5gsy-dBwK+o@AO-++ybL##$Y{DeF|+hx za?-_bz#n{CTW@mBRz$mEBXg9AjHGMS52g)TLmmsitz}g<$x7;vg1a=4#>7!Gdv^_K zEL;hF?iD7kpPZWN)A{?C)T=t;%=5WffL<$op?_U2&+@U!G-C!DcfbE=Ym32F%Fnm+ zc{QH@{L=~D2UEX!fNQJLUm@m^BY_2D_5Fv*S3A7`j841a%OhTSs;=;NzL&PKzE||C zm7NAsX;%}eX#VjlvUv^|MT@&*jJ*TB!h5r7|>ugPOTC-;i8^J+fj3I{QWp*4Zw^u)yObYe$t2G-U3sNP2uYQF2FWF gxc~n_{~hUj8(Gwr<;o30t3DU7y#2hYJvkZw22OjJNB{r; 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