From d955025183f3ac467b7b04b35a0baab948d29294 Mon Sep 17 00:00:00 2001 From: SirEdvin Date: Sat, 25 Jul 2026 16:08:58 +0000 Subject: [PATCH 1/5] feat: add configurator target history --- .../.openspec.yaml | 2 + .../add-configurator-target-history/design.md | 70 +++++++++ .../proposal.md | 28 ++++ .../specs/configurator-target-history/spec.md | 103 +++++++++++++ .../add-configurator-target-history/tasks.md | 25 +++ .../ConfiguratorTargetHistoryScreen.kt | 142 ++++++++++++++++++ .../ConfiguratorTargetHistoryScreenEntry.kt | 16 ++ .../common/item/UltimateConfigurator.kt | 77 +++++++++- .../data/ModEnLanguageProvider.kt | 13 ++ .../siredvin/peripheralworks/data/ModText.kt | 13 ++ .../data/ModUaLanguageProvider.kt | 13 ++ .../ConfiguratorTargetActionMessage.kt | 65 ++++++++ .../networking/NetworkMessages.kt | 7 + .../configurator/ConfiguratorTarget.kt | 41 +++++ .../xplat/ModClientInternalPlatform.kt | 1 + .../xplat/ModClientPlatform.kt | 2 + .../testmod/NetworkManagerClientGameTests.kt | 85 +++++++++++ .../testmod/PeripheralWorksGameTests.kt | 119 +++++++++++++++ .../02059454eca80d9e0c9e9e11fd54b2d59e706782 | 4 +- .../0bc303fc01858bc5bb03e4fc1dd179f5647fa6e9 | 4 +- .../assets/peripheralworks/lang/en_us.json | 13 ++ .../assets/peripheralworks/lang/uk_ua.json | 13 ++ .../fabric/FabricModClientPlatform.kt | 2 + .../332c3938fc847411e7f47874e3456ed3599bef6a | 4 +- .../442eea4c4e28c24d3ee14e17147972c3af1159eb | 4 +- .../assets/peripheralworks/lang/en_us.json | 13 ++ .../assets/peripheralworks/lang/uk_ua.json | 13 ++ .../forge/ForgeModClientPlatform.kt | 2 + 28 files changed, 881 insertions(+), 13 deletions(-) create mode 100644 openspec/changes/add-configurator-target-history/.openspec.yaml create mode 100644 openspec/changes/add-configurator-target-history/design.md create mode 100644 openspec/changes/add-configurator-target-history/proposal.md create mode 100644 openspec/changes/add-configurator-target-history/specs/configurator-target-history/spec.md create mode 100644 openspec/changes/add-configurator-target-history/tasks.md create mode 100644 projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreen.kt create mode 100644 projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreenEntry.kt create mode 100644 projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/ConfiguratorTargetActionMessage.kt create mode 100644 projects/core/src/main/kotlin/site/siredvin/peripheralworks/subsystem/configurator/ConfiguratorTarget.kt diff --git a/openspec/changes/add-configurator-target-history/.openspec.yaml b/openspec/changes/add-configurator-target-history/.openspec.yaml new file mode 100644 index 00000000..3a038210 --- /dev/null +++ b/openspec/changes/add-configurator-target-history/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-25 diff --git a/openspec/changes/add-configurator-target-history/design.md b/openspec/changes/add-configurator-target-history/design.md new file mode 100644 index 00000000..b474eea7 --- /dev/null +++ b/openspec/changes/add-configurator-target-history/design.md @@ -0,0 +1,70 @@ +## Context + +The Ultimate Configurator currently persists one active configurator mode, block position, and dimension. Crouch-right-clicking air clears that binding and mode-specific item data; a detached configurator has no air-use behavior. Four configuration modes are registered from block-state predicates, and existing client screens use loader-neutral screen-opening hooks plus validated serverbound messages. + +This change adds item-local target history and favorites. Records can outlive loaded chunks, removed blocks, and dimension changes, so stored identity and every mutation must be validated without loading chunks or trusting client screen state. + +## Goals / Non-Goals + +**Goals:** + +- Preserve three distinct recently attached targets and up to 16 favorites on each configurator. +- Make detached configurators a fast, native UI entry point for reattachment and favorite management. +- Keep the item stack's server-side NBT authoritative while providing responsive client controls. +- Safely retain cross-dimension and temporarily unavailable records. + +**Non-Goals:** + +- Loading chunks, teleporting players, or remotely configuring a stored target. +- Sharing target lists between configurators or players. +- Preserving mode-specific screen selections and rendering preferences per history entry. +- Adding a container menu, external dependency, or Lua API. + +## Decisions + +### Store compact target records on the item + +Store recent and favorite lists as separate NBT lists. Each target record contains the configurator mode ID, dimension ID, and block position; favorite records may additionally contain a custom name. A target's identity is its dimension and position. Attaching at an existing identity replaces its stored mode and moves it to the front of the recent list, then trims the list to three entries. + +Favorites are independent of recent-list eviction and are ordered by most recent favorite action. Favoriting inserts a target at the front, duplicate favorite requests do not create another record, and the server rejects additions after 16 entries. Selecting or renaming a favorite does not reorder it. + +Alternative considered: keep one list with recent/favorite flags. Rejected because trimming recent history would either delete favorites or require extra retention rules, while two small lists directly match the UI sections. + +### Open a non-container screen only while detached + +A normal right-click on air with no active mode opens a client `Screen` through the existing loader-neutral client platform. The screen reads the held stack's recent and favorite NBT and displays two sections. Default rows show the translated configuration type, dimension, and coordinates. A named favorite displays only its custom name, as requested. + +Each row provides immediate selection and a favorite toggle. Favorite rows additionally provide inline rename editing. Submitting an empty name removes the custom name and restores the default label; non-empty names are limited to 64 characters. The screen remains non-pausing and closes after a successful selection. + +Alternative considered: add a `Menu` and synchronized container. Rejected because no inventory slots or continuously authoritative world data are involved; normal item synchronization and explicit mutations are smaller. + +### Route all changes through validated serverbound actions + +Selection, favorite toggling, and rename submission use a loader-neutral serverbound message carrying the requested action and target identity rather than accepting replacement NBT from the client. The handler requires the player's main-hand item to be an Ultimate Configurator and detached, then resolves the referenced target from its current NBT. + +Favorite and rename actions mutate only records already present in the server's recent or favorite lists. Rename additionally requires the target to be a favorite and enforces the 64-character limit. The item is synchronized after successful mutation. + +Selection requires the player to be in the target dimension, the target position to be loaded, and the current block state to resolve to the stored configurator mode. A successful selection uses the same active-mode save path as direct attachment, updates recent history, synchronizes the item, and closes the screen. A rejected selection leaves the item detached and provides feedback. + +Alternative considered: have the client directly edit item NBT or attach from historical data without checking the world. Rejected because either path permits stale or forged targets and bypasses current block compatibility. + +### Record history at the shared attachment path + +The shared active-mode save operation records recent history, covering both direct crouch-click attachment and menu selection once. Detaching clears only active mode and mode-specific transient data; it does not clear recent targets or favorites. + +Alternative considered: update history in each interaction caller. Rejected because it duplicates ordering and trimming behavior and can miss future attachment paths. + +## Risks / Trade-offs + +- [A stored block is removed, changed, unloaded, or in another dimension] -> Keep the record visible and editable, but reject selection until the player is in the dimension and the loaded block matches the stored mode. +- [A client acts on a stale row after item state changes] -> Resolve identity against current server NBT and require a detached main-hand configurator for every action. +- [Mode IDs or malformed records become invalid after upgrades] -> Ignore invalid records when reading and bound both lists whenever writing. +- [Long dimensions or coordinates overflow a row] -> Use clipped or scrolling row presentation while retaining the full value in hover text where needed. + +## Migration Plan + +Existing configurators have no history or favorite tags and therefore open an empty detached menu. The first successful attachment initializes recent history; no data fixer is required. Rolling back leaves unknown item tags that older versions ignore. + +## Open Questions + +None. diff --git a/openspec/changes/add-configurator-target-history/proposal.md b/openspec/changes/add-configurator-target-history/proposal.md new file mode 100644 index 00000000..67d3a954 --- /dev/null +++ b/openspec/changes/add-configurator-target-history/proposal.md @@ -0,0 +1,28 @@ +## Why + +Detaching an Ultimate Configurator currently discards convenient access to its previous target, forcing players to revisit blocks whenever they want to switch configurations. A detached-item menu with recent and favorite targets makes repeated configuration work faster while keeping each configurator self-contained. + +## What Changes + +- Record the three most recently attached distinct configurable blocks on the Ultimate Configurator. +- Allow up to 16 recorded targets to be favorited and unfavorited from a detached configurator menu. +- Open a native target-selection menu when the player right-clicks air with a detached Ultimate Configurator. +- Reattach immediately when the player selects a valid recent or favorite target. +- Display each stored target's configurator type and coordinates, including its dimension, and allow favorites to be renamed. +- Hide a renamed favorite's coordinates behind its custom name; clearing the name restores the type-and-coordinate label. +- Persist recent targets, favorites, and favorite names in the Ultimate Configurator's NBT. + +## Capabilities + +### New Capabilities + +- `configurator-target-history`: Detached Ultimate Configurator target history, favorites, naming, validation, and selection UI. + +### Modified Capabilities + +None. + +## Impact + +- Affects Ultimate Configurator attachment and air-use behavior, item NBT, client screen opening, loader-neutral networking, localization, and client/server GameTests. +- Adds no external dependencies and does not change configured block data or Lua APIs. diff --git a/openspec/changes/add-configurator-target-history/specs/configurator-target-history/spec.md b/openspec/changes/add-configurator-target-history/specs/configurator-target-history/spec.md new file mode 100644 index 00000000..1560a013 --- /dev/null +++ b/openspec/changes/add-configurator-target-history/specs/configurator-target-history/spec.md @@ -0,0 +1,103 @@ +## ADDED Requirements + +### Requirement: Recent target history +The Ultimate Configurator SHALL persist the three most recently attached distinct targets in its own NBT. Each target SHALL include its configuration type, dimension, and block coordinates, and target identity SHALL be determined by dimension and coordinates. + +#### Scenario: Attach a new target +- **WHEN** a player attaches the configurator to a configurable block not already in its recent history +- **THEN** the system stores that target first and retains at most the three newest distinct targets + +#### Scenario: Reattach a recent target +- **WHEN** a player attaches the configurator to a dimension and coordinates already in its recent history +- **THEN** the system updates the stored configuration type, moves that target to the first position, and does not create a duplicate + +#### Scenario: Detach the configurator +- **WHEN** a player detaches an Ultimate Configurator +- **THEN** the system clears its active binding without clearing recent targets or favorites + +### Requirement: Detached target menu +The system SHALL open a non-pausing native target menu when a player right-clicks air with a detached main-hand Ultimate Configurator. The menu SHALL display separate recent and favorite sections from that configurator's NBT. + +#### Scenario: Open detached configurator menu +- **WHEN** a player normally right-clicks air with a detached Ultimate Configurator in the main hand +- **THEN** the system opens the target menu and displays the configurator's stored recent targets and favorites + +#### Scenario: Use an attached configurator on air +- **WHEN** a player normally right-clicks air with an attached Ultimate Configurator +- **THEN** the system preserves the active configuration mode's existing air-use behavior instead of opening the target menu + +#### Scenario: Empty history +- **WHEN** the target menu opens for a configurator with no recent targets or favorites +- **THEN** the system displays both sections as empty without creating records + +### Requirement: Favorite target management +The target menu SHALL allow targets shown in either section to be favorited or unfavorited. Each Ultimate Configurator SHALL persist no more than 16 distinct favorites, ordered by most recent favorite action. + +#### Scenario: Favorite a recent target +- **WHEN** a player favorites a recent target and fewer than 16 favorites exist +- **THEN** the system stores it first in the favorite list without removing it from recent history + +#### Scenario: Favorite an existing favorite +- **WHEN** a duplicate favorite action references a target already in the favorite list +- **THEN** the system retains one favorite record and does not exceed the limit + +#### Scenario: Reach the favorite limit +- **WHEN** a player attempts to add a distinct seventeenth favorite +- **THEN** the server rejects the addition and retains the existing 16 favorites + +#### Scenario: Unfavorite a target +- **WHEN** a player removes a target from favorites +- **THEN** the system removes only its favorite record and leaves any recent-history record intact + +#### Scenario: Select or rename a favorite +- **WHEN** a player selects or renames an existing favorite +- **THEN** the system preserves its position in the favorite list + +### Requirement: Favorite target names +The target menu SHALL allow a player to assign a custom name of at most 64 characters to a favorite. A named favorite SHALL display only its custom name; an unnamed target SHALL display its translated configuration type, dimension, and coordinates. + +#### Scenario: Rename a favorite +- **WHEN** a player submits a non-empty valid custom name for a favorite +- **THEN** the system persists the name on the configurator and hides that row's type, dimension, and coordinates behind the custom name + +#### Scenario: Clear a favorite name +- **WHEN** a player submits an empty custom name for a named favorite +- **THEN** the system removes the custom name and restores the type, dimension, and coordinate label + +#### Scenario: Reject an oversized name +- **WHEN** a player submits a custom name longer than 64 characters +- **THEN** the server rejects the mutation and preserves the previous name + +#### Scenario: Rename a non-favorite target +- **WHEN** a client requests a rename for a target that is not currently a favorite +- **THEN** the server rejects the mutation + +### Requirement: Validated target selection +Selecting a recent or favorite row SHALL immediately reattach the configurator and close the menu only when the target is in the player's current dimension, its position is loaded, and its current block state supports the stored configuration type. + +#### Scenario: Select a valid target +- **WHEN** a player selects a stored target in the current dimension whose loaded block still supports its stored configuration type +- **THEN** the server attaches the configurator, promotes the target in recent history, synchronizes the item, and closes the menu + +#### Scenario: Select a target in another dimension +- **WHEN** a player selects a stored target outside the current dimension +- **THEN** the system leaves the configurator detached, keeps the record, and reports that the target is unavailable + +#### Scenario: Select an unloaded target +- **WHEN** a player selects a stored target whose position is not loaded +- **THEN** the system does not load the position, leaves the configurator detached, keeps the record, and reports that the target is unavailable + +#### Scenario: Select a changed or removed target +- **WHEN** a player selects a loaded stored target whose block no longer supports the stored configuration type +- **THEN** the system leaves the configurator detached, keeps the record, and reports that the target is unavailable + +### Requirement: Server-authoritative item mutations +The server SHALL accept target selection, favorite, unfavorite, and rename actions only for a detached Ultimate Configurator in the player's main hand and SHALL resolve each requested target against that stack's current stored records. + +#### Scenario: Mutate a stale or forged target +- **WHEN** a client requests an action for a target absent from the server-side configurator history and favorites +- **THEN** the server rejects the action without changing item NBT + +#### Scenario: Mutate a different item state +- **WHEN** a client requests an action while the main-hand item is not a detached Ultimate Configurator +- **THEN** the server rejects the action without changing either item diff --git a/openspec/changes/add-configurator-target-history/tasks.md b/openspec/changes/add-configurator-target-history/tasks.md new file mode 100644 index 00000000..d3a62289 --- /dev/null +++ b/openspec/changes/add-configurator-target-history/tasks.md @@ -0,0 +1,25 @@ +## 1. Target Data And Attachment + +- [x] 1.1 Add bounded NBT serialization and parsing for target records, three-entry unique MRU history, 16-entry newest-first favorites, and optional 64-character favorite names. +- [x] 1.2 Record recent targets in the shared active-mode save path while preserving history and favorites during detach. +- [x] 1.3 Add item mutation helpers for favorite toggle, rename/reset, and validated reattachment using current dimension, loaded position, and registered block mode. + +## 2. Networking And Interaction + +- [x] 2.1 Add and register a loader-neutral serverbound target action message that validates a detached main-hand configurator and resolves target identity from authoritative item NBT. +- [x] 2.2 Synchronize successful item mutations and report rejected, unavailable, and favorite-limit actions without loading target chunks. +- [x] 2.3 Open the target-history screen on client-side normal air use only when the Ultimate Configurator is detached, preserving all attached-mode air interactions. + +## 3. Target Menu + +- [x] 3.1 Add the loader-neutral client platform hook and non-pausing screen entry for the held detached configurator. +- [x] 3.2 Implement separate recent and newest-first favorite sections with scrolling or clipping, default type/dimension/coordinate labels, and custom-name-only favorite labels. +- [x] 3.3 Implement immediate row selection, favorite toggles, and inline favorite rename/reset controls with a 64-character client limit and server-authoritative refresh/close behavior. +- [x] 3.4 Add English and Ukrainian localization source entries for the screen, controls, empty states, and mutation feedback, then regenerate loader resources. + +## 4. Verification + +- [x] 4.1 Add server GameTests for MRU deduplication/trimming, detach retention, favorite ordering/limit, rename validation, malformed NBT handling, and target-selection validation. +- [x] 4.2 Add client GameTests for detached air-use opening, section rendering and labels, favorite/rename controls, successful selection closure, and attached-mode interaction precedence. +- [x] 4.3 Run the root Fabric and Forge GameTests under Xvfb with an explicit timeout and fix failures. +- [x] 4.4 Run the timed root multi-loader build and fix failures. diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreen.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreen.kt new file mode 100644 index 00000000..0264968f --- /dev/null +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreen.kt @@ -0,0 +1,142 @@ +package site.siredvin.peripheralworks.client.configurator + +import net.minecraft.client.gui.Font +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.EditBox +import net.minecraft.client.gui.components.Tooltip +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component +import site.siredvin.peripheralworks.common.item.UltimateConfigurator +import site.siredvin.peripheralworks.common.setup.Items +import site.siredvin.peripheralworks.data.ModText +import site.siredvin.peripheralworks.networking.ClientNetworking +import site.siredvin.peripheralworks.networking.ConfiguratorTargetActionMessage +import site.siredvin.peripheralworks.subsystem.configurator.ConfiguratorModeRegistry +import site.siredvin.peripheralworks.subsystem.configurator.ConfiguratorTarget + +class ConfiguratorTargetHistoryScreen : Screen(ModText.CONFIGURATOR_HISTORY_TITLE.text) { + private var page = 0 + private var snapshot = "" + private var recentEmpty = false + private var favoritesEmpty = false + + override fun isPauseScreen(): Boolean = false + + override fun init() { + val configurator = configurator() ?: return onClose() + val stack = minecraft?.player?.mainHandItem ?: return onClose() + val recent = configurator.getRecentTargets(stack) + val favorites = configurator.getFavoriteTargets(stack) + val panelWidth = (width - 24).coerceAtMost(420) + val left = (width - panelWidth) / 2 + + recentEmpty = recent.isEmpty() + favoritesEmpty = favorites.isEmpty() + recent.forEachIndexed { index, target -> addRecentRow(target, favorites.any(target::matches), left, 38 + index * 22, panelWidth) } + + val pageCount = ((favorites.size + FAVORITES_PER_PAGE - 1) / FAVORITES_PER_PAGE).coerceAtLeast(1) + page = page.coerceIn(0, pageCount - 1) + favorites.drop(page * FAVORITES_PER_PAGE).take(FAVORITES_PER_PAGE).forEachIndexed { index, target -> + addFavoriteRow(target, left, 126 + index * 46, panelWidth) + } + if (pageCount > 1) { + addRenderableWidget( + Button.builder(Component.literal("<")) { + page-- + rebuild() + }.bounds(left, 218, 24, 20).build(), + ).active = page > 0 + addRenderableWidget(Button.builder(Component.literal("${page + 1}/$pageCount")) {}.bounds(left + 28, 218, panelWidth - 56, 20).build()).active = false + addRenderableWidget( + Button.builder(Component.literal(">")) { + page++ + rebuild() + }.bounds(left + panelWidth - 24, 218, 24, 20).build(), + ).active = page + 1 < pageCount + } + snapshot = snapshot() + } + + private fun addRecentRow(target: ConfiguratorTarget, favorite: Boolean, left: Int, top: Int, panelWidth: Int) { + addTargetButton(target, left, top, panelWidth - 92) + addRenderableWidget( + Button.builder(if (favorite) ModText.CONFIGURATOR_HISTORY_UNFAVORITE.text else ModText.CONFIGURATOR_HISTORY_FAVORITE.text) { toggle(target) } + .bounds(left + panelWidth - 88, top, 88, 20).build(), + ) + } + + private fun addFavoriteRow(target: ConfiguratorTarget, left: Int, top: Int, panelWidth: Int) { + addTargetButton(target, left, top, panelWidth - 92) + addRenderableWidget(Button.builder(ModText.CONFIGURATOR_HISTORY_UNFAVORITE.text) { toggle(target) }.bounds(left + panelWidth - 88, top, 88, 20).build()) + val name = addRenderableWidget( + EditBox(font, left, top + 22, panelWidth - 126, 20, ModText.CONFIGURATOR_HISTORY_RENAME.text).apply { + setHint(ModText.CONFIGURATOR_HISTORY_RENAME.text) + setMaxLength(ConfiguratorTarget.MAX_NAME_LENGTH) + setValue(target.name.orEmpty()) + }, + ) + addRenderableWidget(Button.builder(ModText.CONFIGURATOR_HISTORY_APPLY.text) { rename(target, name.value) }.bounds(left + panelWidth - 122, top + 22, 58, 20).build()) + addRenderableWidget(Button.builder(ModText.CONFIGURATOR_HISTORY_RESET.text) { rename(target, "") }.bounds(left + panelWidth - 60, top + 22, 60, 20).build()) + } + + private fun addTargetButton(target: ConfiguratorTarget, left: Int, top: Int, width: Int) { + val label = targetLabel(target) + addRenderableWidget(LeftAlignedButton(left, top, width, label) { send(ConfiguratorTargetActionMessage.Action.SELECT, target) }).tooltip = Tooltip.create(defaultTargetLabel(target)) + } + + private fun targetLabel(target: ConfiguratorTarget): Component = target.name?.let(Component::literal) ?: defaultTargetLabel(target) + + private fun defaultTargetLabel(target: ConfiguratorTarget): Component = ConfiguratorModeRegistry.get(target.modeID)!!.description.copy() + .append(" | ${target.dimensionID} | ${target.pos.x}, ${target.pos.y}, ${target.pos.z}") + + private fun toggle(target: ConfiguratorTarget) = send(ConfiguratorTargetActionMessage.Action.TOGGLE_FAVORITE, target) + private fun rename(target: ConfiguratorTarget, name: String) = send(ConfiguratorTargetActionMessage.Action.RENAME, target, name) + + private fun send(action: ConfiguratorTargetActionMessage.Action, target: ConfiguratorTarget, name: String = "") { + ClientNetworking.sendToServer(ConfiguratorTargetActionMessage(action, target.dimensionID, target.pos, name)) + } + + private fun configurator(): UltimateConfigurator? { + val stack = minecraft?.player?.mainHandItem ?: return null + return (stack.item as? UltimateConfigurator)?.takeIf { stack.`is`(Items.ULTIMATE_CONFIGURATOR.get()) && it.getActiveMode(stack) == null } + } + + private fun snapshot(): String = minecraft?.player?.mainHandItem?.tag?.let { + "${it.get(UltimateConfigurator.RECENT_TARGETS)}:${it.get(UltimateConfigurator.FAVORITE_TARGETS)}" + }.orEmpty() + + private fun rebuild() { + clearWidgets() + init() + } + + override fun tick() { + if (configurator() == null) return onClose() + if (snapshot() != snapshot) rebuild() + } + + override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + renderBackground(graphics) + super.render(graphics, mouseX, mouseY, partialTick) + val panelWidth = (width - 24).coerceAtMost(420) + val left = (width - panelWidth) / 2 + graphics.drawCenteredString(font, title, width / 2, 12, 0xffffff) + graphics.drawString(font, ModText.CONFIGURATOR_HISTORY_RECENT.text, left, 27, 0xffffff) + graphics.drawString(font, ModText.CONFIGURATOR_HISTORY_FAVORITES.text, left, 115, 0xffffff) + if (recentEmpty) graphics.drawString(font, ModText.CONFIGURATOR_HISTORY_EMPTY_RECENT.text, left, 42, 0xaaaaaa) + if (favoritesEmpty) graphics.drawString(font, ModText.CONFIGURATOR_HISTORY_EMPTY_FAVORITES.text, left, 130, 0xaaaaaa) + } + + companion object { + private const val FAVORITES_PER_PAGE = 2 + } + + private class LeftAlignedButton(x: Int, y: Int, width: Int, message: Component, onPress: () -> Unit) : Button(x, y, width, 20, message, { onPress() }, DEFAULT_NARRATION) { + override fun renderString(graphics: GuiGraphics, font: Font, color: Int) { + graphics.enableScissor(x + 4, y, x + width - 4, y + height) + graphics.drawString(font, message, x + 4, y + (height - 8) / 2, color) + graphics.disableScissor() + } + } +} diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreenEntry.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreenEntry.kt new file mode 100644 index 00000000..75fe0c5a --- /dev/null +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/configurator/ConfiguratorTargetHistoryScreenEntry.kt @@ -0,0 +1,16 @@ +package site.siredvin.peripheralworks.client.configurator + +import net.minecraft.client.Minecraft +import site.siredvin.peripheralworks.common.item.UltimateConfigurator +import site.siredvin.peripheralworks.common.setup.Items + +object ConfiguratorTargetHistoryScreenEntry { + fun open() { + val minecraft = Minecraft.getInstance() + val stack = minecraft.player?.mainHandItem ?: return + val configurator = stack.item as? UltimateConfigurator ?: return + if (stack.`is`(Items.ULTIMATE_CONFIGURATOR.get()) && configurator.getActiveMode(stack) == null) { + minecraft.setScreen(ConfiguratorTargetHistoryScreen()) + } + } +} diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/common/item/UltimateConfigurator.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/common/item/UltimateConfigurator.kt index f465e6b8..1c6d7d7b 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/common/item/UltimateConfigurator.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/common/item/UltimateConfigurator.kt @@ -1,7 +1,9 @@ package site.siredvin.peripheralworks.common.item import net.minecraft.core.BlockPos +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtUtils +import net.minecraft.nbt.Tag import net.minecraft.network.chat.Component import net.minecraft.resources.ResourceLocation import net.minecraft.world.InteractionHand @@ -18,6 +20,8 @@ import site.siredvin.broccolium.modules.base.item.DescriptiveItem import site.siredvin.peripheralworks.data.ModTooltip import site.siredvin.peripheralworks.subsystem.configurator.ConfigurationMode import site.siredvin.peripheralworks.subsystem.configurator.ConfiguratorModeRegistry +import site.siredvin.peripheralworks.subsystem.configurator.ConfiguratorTarget +import site.siredvin.peripheralworks.xplat.ModClientPlatform class UltimateConfigurator : DescriptiveItem(Properties().stacksTo(1)) { @@ -25,6 +29,10 @@ class UltimateConfigurator : DescriptiveItem(Properties().stacksTo(1)) { const val ACTIVE_MOD_NAME = "activeMod" const val ACTIVE_MOD_POS = "activeModPos" const val ACTIVE_MOD_DIMENSION = "activeModDimension" + const val RECENT_TARGETS = "recentTargets" + const val FAVORITE_TARGETS = "favoriteTargets" + const val MAX_RECENT_TARGETS = 3 + const val MAX_FAVORITE_TARGETS = 16 fun isActiveModeDimension(stack: ItemStack, level: Level): Boolean = stack.tag?.getString(ACTIVE_MOD_DIMENSION) == level.dimension().location().toString() } @@ -52,23 +60,46 @@ class UltimateConfigurator : DescriptiveItem(Properties().stacksTo(1)) { if (!data.contains(ACTIVE_MOD_POS)) { return null } - @Suppress("DEPRECATION", "KotlinRedundantDiagnosticSuppress") - val configurationMode = ConfiguratorModeRegistry.get(ResourceLocation(data.getString(ACTIVE_MOD_NAME))) ?: return null + val modeID = ResourceLocation.tryParse(data.getString(ACTIVE_MOD_NAME)) ?: return null + val configurationMode = ConfiguratorModeRegistry.get(modeID) ?: return null return Pair( configurationMode, NbtUtils.readBlockPos(data.getCompound(ACTIVE_MOD_POS)), ) } - private fun saveActiveMode(stack: ItemStack, mode: ConfigurationMode, targetBlock: BlockPos, level: Level) { + fun getRecentTargets(stack: ItemStack): List = readTargets(stack, RECENT_TARGETS, MAX_RECENT_TARGETS) + + fun getFavoriteTargets(stack: ItemStack): List = readTargets(stack, FAVORITE_TARGETS, MAX_FAVORITE_TARGETS) + + private fun readTargets(stack: ItemStack, key: String, limit: Int): List { + val list = stack.tag?.getList(key, Tag.TAG_COMPOUND.toInt()) ?: return emptyList() + return buildList { + for (index in 0 until list.size) { + val target = ConfiguratorTarget.fromNBT(list.getCompound(index)) ?: continue + if (none(target::matches)) add(target) + if (size == limit) break + } + } + } + + private fun writeTargets(stack: ItemStack, key: String, targets: List, limit: Int) { + val list = ListTag() + targets.distinctBy { it.dimensionID to it.pos }.take(limit).forEach { list.add(it.toNBT()) } + stack.orCreateTag.put(key, list) + } + + fun saveActiveMode(stack: ItemStack, mode: ConfigurationMode, targetBlock: BlockPos, level: Level) { getActiveMode(stack)?.first?.clearData(stack) val data = stack.orCreateTag data.putString(ACTIVE_MOD_NAME, mode.modeID.toString()) data.put(ACTIVE_MOD_POS, NbtUtils.writeBlockPos(targetBlock)) data.putString(ACTIVE_MOD_DIMENSION, level.dimension().location().toString()) + val target = ConfiguratorTarget(mode.modeID, level.dimension().location(), targetBlock) + writeTargets(stack, RECENT_TARGETS, listOf(target) + getRecentTargets(stack).filterNot(target::matches), MAX_RECENT_TARGETS) } - private fun clearActiveMode(stack: ItemStack): ItemStack { + fun clearActiveMode(stack: ItemStack): ItemStack { val data = stack.tag ?: return stack getActiveMode(stack)?.first?.clearData(stack) data.remove(ACTIVE_MOD_NAME) @@ -77,6 +108,38 @@ class UltimateConfigurator : DescriptiveItem(Properties().stacksTo(1)) { return stack } + fun toggleFavorite(stack: ItemStack, target: ConfiguratorTarget): FavoriteResult { + val stored = (getRecentTargets(stack) + getFavoriteTargets(stack)).firstOrNull(target::matches) ?: return FavoriteResult.REJECTED + val favorites = getFavoriteTargets(stack) + if (favorites.any(target::matches)) { + writeTargets(stack, FAVORITE_TARGETS, favorites.filterNot(target::matches), MAX_FAVORITE_TARGETS) + return FavoriteResult.REMOVED + } + if (favorites.size >= MAX_FAVORITE_TARGETS) return FavoriteResult.LIMIT + writeTargets(stack, FAVORITE_TARGETS, listOf(stored.copy(name = null)) + favorites, MAX_FAVORITE_TARGETS) + return FavoriteResult.ADDED + } + + fun renameFavorite(stack: ItemStack, target: ConfiguratorTarget, name: String): Boolean { + if (name.length > ConfiguratorTarget.MAX_NAME_LENGTH) return false + val favorites = getFavoriteTargets(stack) + if (favorites.none(target::matches)) return false + writeTargets(stack, FAVORITE_TARGETS, favorites.map { if (it.matches(target)) it.copy(name = name.ifEmpty { null }) else it }, MAX_FAVORITE_TARGETS) + return true + } + + fun selectTarget(stack: ItemStack, level: Level, target: ConfiguratorTarget): SelectionResult { + val stored = (getRecentTargets(stack) + getFavoriteTargets(stack)).firstOrNull(target::matches) ?: return SelectionResult.REJECTED + if (stored.dimensionID != level.dimension().location() || !level.isLoaded(stored.pos)) return SelectionResult.UNAVAILABLE + val mode = ConfiguratorModeRegistry.get(level.getBlockState(stored.pos)) + if (mode?.modeID != stored.modeID) return SelectionResult.UNAVAILABLE + saveActiveMode(stack, mode, stored.pos, level) + return SelectionResult.SUCCESS + } + + enum class FavoriteResult { ADDED, REMOVED, LIMIT, REJECTED } + enum class SelectionResult { SUCCESS, UNAVAILABLE, REJECTED } + private fun tryActivateMode(stack: ItemStack, player: Player, hit: BlockHitResult, level: Level): InteractionResultHolder { if (player.pose == Pose.CROUCHING) { val targetState = level.getBlockState(hit.blockPos) @@ -103,7 +166,11 @@ class UltimateConfigurator : DescriptiveItem(Properties().stacksTo(1)) { if (player.pose == Pose.CROUCHING) { InteractionResultHolder.consume(clearActiveMode(itemStack)) } else { - val activeModePair = getActiveMode(itemStack) ?: return InteractionResultHolder.pass(itemStack) + val activeModePair = getActiveMode(itemStack) + if (activeModePair == null) { + if (level.isClientSide) ModClientPlatform.openConfiguratorTargetHistoryScreen() + return InteractionResultHolder.consume(itemStack) + } return activeModePair.first.onBlockMiss(activeModePair.second, itemStack, player, level) } } else { diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModEnLanguageProvider.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModEnLanguageProvider.kt index c30e218d..7a6a2386 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModEnLanguageProvider.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModEnLanguageProvider.kt @@ -121,6 +121,19 @@ class ModEnLanguageProvider( add(ModText.TARGET_RENDER_SETTINGS_TITLE, "Target Render Settings") add(ModText.TARGET_RENDER_SETTINGS_UNAVAILABLE, "The bound target is unavailable") add(ModText.TARGET_RENDER_SETTINGS_REQUEST_REJECTED, "Target render settings request rejected") + add(ModText.CONFIGURATOR_HISTORY_TITLE, "Configurator Targets") + add(ModText.CONFIGURATOR_HISTORY_RECENT, "Recent") + add(ModText.CONFIGURATOR_HISTORY_FAVORITES, "Favorites") + add(ModText.CONFIGURATOR_HISTORY_EMPTY_RECENT, "No recent targets") + add(ModText.CONFIGURATOR_HISTORY_EMPTY_FAVORITES, "No favorite targets") + add(ModText.CONFIGURATOR_HISTORY_FAVORITE, "Favorite") + add(ModText.CONFIGURATOR_HISTORY_UNFAVORITE, "Remove") + add(ModText.CONFIGURATOR_HISTORY_RENAME, "Favorite name") + add(ModText.CONFIGURATOR_HISTORY_APPLY, "Apply") + add(ModText.CONFIGURATOR_HISTORY_RESET, "Reset") + add(ModText.CONFIGURATOR_HISTORY_REQUEST_REJECTED, "Configurator target request rejected") + add(ModText.CONFIGURATOR_HISTORY_TARGET_UNAVAILABLE, "That configurator target is unavailable") + add(ModText.CONFIGURATOR_HISTORY_FAVORITE_LIMIT, "A configurator can have at most 16 favorites") add(ModText.TECH_REBORN_ENERGY, "Tech reborn energy") diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModText.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModText.kt index 15fa32e8..7e0fdc16 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModText.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModText.kt @@ -78,6 +78,19 @@ enum class ModText : TextRecord { TARGET_RENDER_SETTINGS_TITLE, TARGET_RENDER_SETTINGS_UNAVAILABLE, TARGET_RENDER_SETTINGS_REQUEST_REJECTED, + CONFIGURATOR_HISTORY_TITLE, + CONFIGURATOR_HISTORY_RECENT, + CONFIGURATOR_HISTORY_FAVORITES, + CONFIGURATOR_HISTORY_EMPTY_RECENT, + CONFIGURATOR_HISTORY_EMPTY_FAVORITES, + CONFIGURATOR_HISTORY_FAVORITE, + CONFIGURATOR_HISTORY_UNFAVORITE, + CONFIGURATOR_HISTORY_RENAME, + CONFIGURATOR_HISTORY_APPLY, + CONFIGURATOR_HISTORY_RESET, + CONFIGURATOR_HISTORY_REQUEST_REJECTED, + CONFIGURATOR_HISTORY_TARGET_UNAVAILABLE, + CONFIGURATOR_HISTORY_FAVORITE_LIMIT, ; override val textID: String by lazy { diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModUaLanguageProvider.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModUaLanguageProvider.kt index c063489f..865b7357 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModUaLanguageProvider.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/data/ModUaLanguageProvider.kt @@ -121,6 +121,19 @@ class ModUaLanguageProvider( add(ModText.TARGET_RENDER_SETTINGS_TITLE, "Налаштування відображення цілей") add(ModText.TARGET_RENDER_SETTINGS_UNAVAILABLE, "Прив'язана ціль недоступна") add(ModText.TARGET_RENDER_SETTINGS_REQUEST_REJECTED, "Запит налаштувань відображення цілей відхилено") + add(ModText.CONFIGURATOR_HISTORY_TITLE, "Цілі конфігуратора") + add(ModText.CONFIGURATOR_HISTORY_RECENT, "Останні") + add(ModText.CONFIGURATOR_HISTORY_FAVORITES, "Обрані") + add(ModText.CONFIGURATOR_HISTORY_EMPTY_RECENT, "Немає останніх цілей") + add(ModText.CONFIGURATOR_HISTORY_EMPTY_FAVORITES, "Немає обраних цілей") + add(ModText.CONFIGURATOR_HISTORY_FAVORITE, "Обрати") + add(ModText.CONFIGURATOR_HISTORY_UNFAVORITE, "Прибрати") + add(ModText.CONFIGURATOR_HISTORY_RENAME, "Назва обраної цілі") + add(ModText.CONFIGURATOR_HISTORY_APPLY, "Застосувати") + add(ModText.CONFIGURATOR_HISTORY_RESET, "Скинути") + add(ModText.CONFIGURATOR_HISTORY_REQUEST_REJECTED, "Запит цілі конфігуратора відхилено") + add(ModText.CONFIGURATOR_HISTORY_TARGET_UNAVAILABLE, "Ця ціль конфігуратора недоступна") + add(ModText.CONFIGURATOR_HISTORY_FAVORITE_LIMIT, "Конфігуратор може мати щонайбільше 16 обраних цілей") add(ModText.TECH_REBORN_ENERGY, "Енергія з Tech reborn") diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/ConfiguratorTargetActionMessage.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/ConfiguratorTargetActionMessage.kt new file mode 100644 index 00000000..6fbe8747 --- /dev/null +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/ConfiguratorTargetActionMessage.kt @@ -0,0 +1,65 @@ +package site.siredvin.peripheralworks.networking + +import net.minecraft.core.BlockPos +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.resources.ResourceLocation +import site.siredvin.peripheralworks.common.item.UltimateConfigurator +import site.siredvin.peripheralworks.common.setup.Items +import site.siredvin.peripheralworks.data.ModText +import site.siredvin.peripheralworks.subsystem.configurator.ConfiguratorTarget + +class ConfiguratorTargetActionMessage( + private val action: Action, + private val dimension: ResourceLocation, + private val pos: BlockPos, + private val name: String = "", +) : NetworkMessage { + enum class Action { SELECT, TOGGLE_FAVORITE, RENAME } + + constructor(buf: FriendlyByteBuf) : this( + buf.readEnum(Action::class.java), + buf.readResourceLocation(), + buf.readBlockPos(), + buf.readUtf(MAX_WIRE_NAME_LENGTH), + ) + + override fun type(): MessageType<*> = NetworkMessages.CONFIGURATOR_TARGET_ACTION + + override fun write(buf: FriendlyByteBuf) { + buf.writeEnum(action) + buf.writeResourceLocation(dimension) + buf.writeBlockPos(pos) + buf.writeUtf(name, MAX_WIRE_NAME_LENGTH) + } + + override fun handle(context: ServerNetworkContext) { + val player = context.getSender() + val stack = player.mainHandItem + val configurator = stack.item as? UltimateConfigurator + if (!stack.`is`(Items.ULTIMATE_CONFIGURATOR.get()) || configurator == null || configurator.getActiveMode(stack) != null) { + player.displayClientMessage(ModText.CONFIGURATOR_HISTORY_REQUEST_REJECTED.text, true) + return + } + val target = ConfiguratorTarget(PLACEHOLDER_MODE, dimension, pos) + val result = when (action) { + Action.SELECT -> when (configurator.selectTarget(stack, player.level(), target)) { + UltimateConfigurator.SelectionResult.SUCCESS -> null + UltimateConfigurator.SelectionResult.UNAVAILABLE -> ModText.CONFIGURATOR_HISTORY_TARGET_UNAVAILABLE + UltimateConfigurator.SelectionResult.REJECTED -> ModText.CONFIGURATOR_HISTORY_REQUEST_REJECTED + } + Action.TOGGLE_FAVORITE -> when (configurator.toggleFavorite(stack, target)) { + UltimateConfigurator.FavoriteResult.ADDED, UltimateConfigurator.FavoriteResult.REMOVED -> null + UltimateConfigurator.FavoriteResult.LIMIT -> ModText.CONFIGURATOR_HISTORY_FAVORITE_LIMIT + UltimateConfigurator.FavoriteResult.REJECTED -> ModText.CONFIGURATOR_HISTORY_REQUEST_REJECTED + } + Action.RENAME -> if (configurator.renameFavorite(stack, target, name)) null else ModText.CONFIGURATOR_HISTORY_REQUEST_REJECTED + } + if (result == null) player.inventoryMenu.broadcastChanges() else player.displayClientMessage(result.text, true) + } + + companion object { + @Suppress("DEPRECATION", "KotlinRedundantDiagnosticSuppress") + private val PLACEHOLDER_MODE = ResourceLocation("minecraft", "empty") + private const val MAX_WIRE_NAME_LENGTH = 256 + } +} diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/NetworkMessages.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/NetworkMessages.kt index 3c8889d4..f411be02 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/NetworkMessages.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/networking/NetworkMessages.kt @@ -36,6 +36,13 @@ object NetworkMessages { ::TargetRenderStyleMessage, ) + val CONFIGURATOR_TARGET_ACTION = registerServerbound( + 604, + "configurator_target_action", + ConfiguratorTargetActionMessage::class.java, + ::ConfiguratorTargetActionMessage, + ) + private fun > register( messages: MutableList>>, id: Int, diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/subsystem/configurator/ConfiguratorTarget.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/subsystem/configurator/ConfiguratorTarget.kt new file mode 100644 index 00000000..aaaa3e22 --- /dev/null +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/subsystem/configurator/ConfiguratorTarget.kt @@ -0,0 +1,41 @@ +package site.siredvin.peripheralworks.subsystem.configurator + +import net.minecraft.core.BlockPos +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.NbtUtils +import net.minecraft.nbt.Tag +import net.minecraft.resources.ResourceLocation + +data class ConfiguratorTarget( + val modeID: ResourceLocation, + val dimensionID: ResourceLocation, + val pos: BlockPos, + val name: String? = null, +) { + fun matches(other: ConfiguratorTarget): Boolean = dimensionID == other.dimensionID && pos == other.pos + + fun toNBT(): CompoundTag = CompoundTag().apply { + putString(MODE, modeID.toString()) + putString(DIMENSION, dimensionID.toString()) + put(POS, NbtUtils.writeBlockPos(pos)) + name?.takeIf { it.isNotEmpty() && it.length <= MAX_NAME_LENGTH }?.let { putString(NAME, it) } + } + + companion object { + const val MAX_NAME_LENGTH = 64 + private const val MODE = "mode" + private const val DIMENSION = "dimension" + private const val POS = "pos" + private const val NAME = "name" + + fun fromNBT(tag: CompoundTag): ConfiguratorTarget? { + if (!tag.contains(MODE, Tag.TAG_STRING.toInt()) || !tag.contains(DIMENSION, Tag.TAG_STRING.toInt()) || !tag.contains(POS, Tag.TAG_COMPOUND.toInt())) return null + val mode = ResourceLocation.tryParse(tag.getString(MODE))?.takeIf { ConfiguratorModeRegistry.get(it) != null } ?: return null + val dimension = ResourceLocation.tryParse(tag.getString(DIMENSION)) ?: return null + val posTag = tag.getCompound(POS) + if (!posTag.contains("X", Tag.TAG_ANY_NUMERIC.toInt()) || !posTag.contains("Y", Tag.TAG_ANY_NUMERIC.toInt()) || !posTag.contains("Z", Tag.TAG_ANY_NUMERIC.toInt())) return null + val name = tag.getString(NAME).takeIf { tag.contains(NAME, Tag.TAG_STRING.toInt()) && it.isNotEmpty() && it.length <= MAX_NAME_LENGTH } + return ConfiguratorTarget(mode, dimension, NbtUtils.readBlockPos(posTag), name) + } + } +} diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientInternalPlatform.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientInternalPlatform.kt index 34e79011..40381c34 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientInternalPlatform.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientInternalPlatform.kt @@ -10,4 +10,5 @@ interface ModClientInternalPlatform { fun registerBlockEntityRendererCallback(sup: Supplier, BlockEntityRendererProvider>>>) fun openNetworkManagerScreen(pos: BlockPos) fun openTargetRenderSettingsScreen(pos: BlockPos) + fun openConfiguratorTargetHistoryScreen() } diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientPlatform.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientPlatform.kt index 492f14f9..76a30e3d 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientPlatform.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/xplat/ModClientPlatform.kt @@ -37,4 +37,6 @@ object ModClientPlatform { fun openNetworkManagerScreen(pos: BlockPos) = baseInnerPlatform.openNetworkManagerScreen(pos) fun openTargetRenderSettingsScreen(pos: BlockPos) = baseInnerPlatform.openTargetRenderSettingsScreen(pos) + + fun openConfiguratorTargetHistoryScreen() = baseInnerPlatform.openConfiguratorTargetHistoryScreen() } diff --git a/projects/core/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/NetworkManagerClientGameTests.kt b/projects/core/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/NetworkManagerClientGameTests.kt index 27d28bcc..3a38b12a 100644 --- a/projects/core/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/NetworkManagerClientGameTests.kt +++ b/projects/core/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/NetworkManagerClientGameTests.kt @@ -1,5 +1,6 @@ package site.siredvin.peripheralworks.testmod +import net.minecraft.client.Minecraft import net.minecraft.client.gui.components.AbstractSliderButton import net.minecraft.client.gui.components.AbstractWidget import net.minecraft.client.gui.components.Button @@ -13,6 +14,7 @@ import net.minecraft.network.chat.CommonComponents import net.minecraft.world.InteractionHand import net.minecraft.world.entity.Pose import net.minecraft.world.item.ItemStack +import site.siredvin.peripheralworks.client.configurator.ConfiguratorTargetHistoryScreen import site.siredvin.peripheralworks.client.configurator.NetworkManagerColorPickerScreen import site.siredvin.peripheralworks.client.configurator.NetworkManagerScreen import site.siredvin.peripheralworks.client.configurator.TargetRenderSettingsScreen @@ -36,6 +38,85 @@ import java.io.File @TestGroup("network-manager-client") class NetworkManagerClientGameTests { + @ClientGameTest(template = "empty", timeoutTicks = 600) + fun managesDetachedConfiguratorTargetsAndPreservesAttachedUse(helper: GameTestHelper) { + val proxyPos = BlockPos(1, 1, 1) + val observerPos = BlockPos(3, 1, 1) + helper.startSequence() + .thenExecute { + helper.setBlock(proxyPos, Blocks.PERIPHERAL_PROXY.get()) + helper.setBlock(observerPos, Blocks.REMOTE_OBSERVER.get()) + val configurator = Items.ULTIMATE_CONFIGURATOR.get() as UltimateConfigurator + val stack = ItemStack(configurator) + configurator.saveActiveMode(stack, PeripheralProxyMode, helper.absolutePos(proxyPos), helper.level) + val proxyTarget = configurator.getRecentTargets(stack).first() + check(configurator.toggleFavorite(stack, proxyTarget) == UltimateConfigurator.FavoriteResult.ADDED) + check(configurator.renameFavorite(stack, proxyTarget, "Workshop Proxy")) + configurator.saveActiveMode(stack, RemoteObserverMode, helper.absolutePos(observerPos), helper.level) + configurator.clearActiveMode(stack) + player(helper).setItemInHand(InteractionHand.MAIN_HAND, stack) + } + .thenIdle(5) + .thenOnClient { + val player = minecraft.player ?: error("Client player is missing") + player.xRot = -90f + minecraft.gameMode!!.useItem(player, InteractionHand.MAIN_HAND) + val screen = minecraft.screen as? ConfiguratorTargetHistoryScreen ?: error("Detached configurator target screen did not open") + val labels = screen.children().filterIsInstance