diff --git a/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/.openspec.yaml b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/.openspec.yaml new file mode 100644 index 00000000..1b062d3a --- /dev/null +++ b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-04 diff --git a/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/design.md b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/design.md new file mode 100644 index 00000000..39033d89 --- /dev/null +++ b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/design.md @@ -0,0 +1,118 @@ +## Context + +UPW already adapts storage from an `AENetworkBlockEntity` into an `AEItemStorage`, and Tweakium's regular `item_storage` plugin provides item listing and named-peripheral transfers. It also exposes a turtle's inventory through `TurtlePeripheralOwner.storage` and provides `FuelBoon` through `TurtlePeripheralOwner.attachFuel()`. + +The wireless terminal use case differs in two ways. Its AE2 storage is discovered from item NBT and the turtle's current position rather than a stationary block entity, and its transfer endpoint is always the same turtle rather than a peripheral name. A normal `ItemStoragePlugin` therefore cannot be reused unchanged because it retains one storage object and requires named peripherals. + +AE2 15's standard Wireless Terminal stores its linked access-point position in item NBT. Vanilla resolves that block entity to discover the grid, scans the grid's access points for an active in-range access point, and normally drains terminal charge while a menu remains open. CC:Tweaked stores configurable turtle upgrade state separately from the upgrade's constant crafting item, so default upgrade behavior would discard the terminal's link, charge, cards, and other NBT when unequipped. + +## Goals / Non-Goals + +**Goals:** + +- Equip a linked standard AE2 Wireless Terminal directly, without introducing another item or recipe. +- Preserve the exact terminal item across equip, turtle persistence, and unequip. +- Expose network item listing and implicit transfers to and from the turtle inventory. +- Revalidate the linked grid and wireless range on every call without chunk loading. +- Charge turtle fuel rather than terminal AE power for mutation calls. +- Attribute AE2 storage mutations to the turtle owner. +- Publish a TypeScript contract aligned with the runtime API. +- Keep Fabric and Forge behavior and tests equivalent. + +**Non-Goals:** + +- Wireless Crafting Terminal or addon-terminal support. +- Fluid or addon `AEKeyType` access. +- AE2 crafting requests or terminal UI configuration. +- Cross-dimensional, infinite-range, or chunk-loading behavior. +- Terminal battery drain or charging while installed. +- A general redesign of Tweakium's item-storage plugins. + +## Decisions + +### The linked Wireless Terminal is the upgrade item + +Register an AE2-specific turtle upgrade whose custom crafting item is the standard Wireless Terminal. Require link NBT during suitability checks. Do not add a UPW bridge item or recipe. + +The upgrade must override the CC:Tweaked item/data round trip: copy the equipped stack into upgrade data and reconstruct it from that data in `getUpgradeItem`. The runtime peripheral must read and update the terminal state through the turtle side's authoritative upgrade data rather than retaining the serializer's default crafting stack. + +This keeps pairing in AE2's existing Wireless Access Point workflow and guarantees that unequipping returns the player's original terminal. A separate adapter item was rejected because it would duplicate link persistence and add a recipe without improving behavior. + +### Every call resolves a fresh wireless session + +Resolve the linked position from the stored terminal stack on every method call. Require the linked block entity to be already loaded and ticking, obtain its current grid, and search that grid for the nearest active access point in the turtle's current level whose range contains the turtle position. + +Do not cache `IGrid`, `MEStorage`, access-point, or block-entity instances. Turtles move, grids split, access points lose channels, and Lua can retain peripherals across all of those changes. A cached storage reference was rejected because it would allow stale or out-of-range access. + +Use AE2's public terminal/link and access-point APIs where possible, but perform the range calculation against the turtle position rather than constructing a menu host. Match vanilla's strict same-level and active-access-point checks and never call APIs that force a chunk load. + +### The turtle owner is the AE2 action source + +Wrap storage simulation and mutation in `TurtlePeripheralOwner.withPlayer(..., skipInventory = true)` and create a player `IActionSource` from that fake player. This preserves the turtle's owning profile when available and follows the project's existing fake-player conventions. + +Using `IActionSource.empty()` was rejected because it loses ownership attribution. Treating the linked access point as the acting machine was rejected because the access point is only the connection anchor, not the initiator. + +### A dedicated peripheral exposes item-storage-like methods + +Expose peripheral type `ae2_wireless_terminal` with: + +```text +items(detailed?, filter?) +pullItem(itemQuery?, limit?, toSlot?) +pushItem(fromSlotOrItemQuery?, limit?) +``` + +The method names use the turtle caller's perspective: `pullItem` moves AE2 to turtle and `pushItem` moves turtle to AE2. `pullItem` accepts an optional one-based destination slot. `pushItem` accepts either a one-based source slot or an item query as its first argument. + +Use Tweakium's existing item representation and item-query conversion. Reuse `PeripheralWorksConfig.itemStorageTransferLimit`. Implement transfer methods directly or through a narrowly scoped shared transfer primitive; do not add a generalized plugin abstraction solely for this peripheral. + +### A separate crafting monitor owns crafting jobs + +Register a second turtle upgrade using AE2's linked Wireless Crafting Terminal and peripheral type `ae2_crafting_monitor`. It exposes `scheduleCrafting`, `getCraftingJob`, `getCraftingJobs`, and `cancelCrafting`; the regular Wireless Terminal remains storage-only. Both upgrades preserve their complete equipped terminal stack and use the same fresh link/range resolver. + +Use one shared crafting-job peripheral plugin for stationary AE2 blocks and crafting monitor turtles, injecting only connection resolution and action-source handling. Return the submitted `ICraftingLink` UUID after the existing leading success value. Track links in a process-wide weak-key map scoped by weak crafting-service identity, so the cache does not keep completed jobs or grids alive. + +An unknown ID, an ID from another network, or a link already reclaimed by garbage collection is reported uniformly as a missing job. Job tracking intentionally does not survive server restart because AE2 has no public lookup-by-UUID API; using AE2 implementation internals to reconstruct links was rejected. + +### Resolve AE storage with a caller-supplied action source + +The existing `AEItemStorage` couples `MEStorage` operations to `IActionSource.ofMachine(entity)` and `entity.setChanged()`. The wireless peripheral needs the same item conversion and storage behavior with a player action source and no stationary entity. + +Minimally generalize the adapter to receive its action source and optional change callback, preserving the current block integration behavior. If the loader APIs prevent an identical change, keep equivalent narrow implementations in each loader. A second full AE item-storage implementation was rejected because it would duplicate key conversion and transfer semantics. + +### Fuel is charged once per validated transfer call + +Attach a `FuelBoon` with a maximum consumption rate consistent with existing turtle upgrades. After link, range, arguments, and slots are validated, require and consume one base fuel before executing either mutation. A valid operation that moves zero items still costs fuel; rejected calls and `items()` do not. The fuel consumption rate multiplier remains available through inherited Fuel API methods, and CC:Tweaked's fuel-disabled mode remains free. + +Per-item and distance-based charging were rejected by product decision. Terminal AE charge is retained but not consumed, avoiding an installed peripheral that must be repeatedly removed for charging. + +### The typed contract is a new additive source module + +Create `projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal.ts`. It should import `ItemQuery` from `@siredvin/typed-peripheral-api/item_storage`, item detail types and `IPeripheralProvider` from `@siredvin/typed-peripheral-base`, and `FuelApi` from `@siredvin/typed-peripheral-api/fuel`. + +Define listing overloads equivalent to `ItemStorageAPI`, redefine `pushItem` and `pullItem` with implicit turtle endpoints and optional slots, extend `FuelApi`, and export an `IPeripheralProvider` for `ae2_wireless_terminal`. Add `ae2CraftingMonitor.ts` for the separate crafting peripheral and update `integrations/ae2.ts` with the same job tracking surface for stationary AE blocks. Generated `.d.ts` and `.lua` files remain untracked build output. + +### Registration remains inside the optional AE2 integration + +Register the serializer, generated turtle upgrade data, client model, language entry, and peripheral only when AE2 integration loading runs. Use the existing integration hooks demonstrated by Nature's Compass and ProjectE. Render the terminal item slightly scaled down and facing upward as the turtle upgrade model rather than adding a custom model asset. + +Keep implementation and GameTests in the Fabric and Forge AE2 integration source sets because core must remain free of hard AE2 references. Preserve equivalent behavior despite the project's supported AE2 version difference. + +## Risks / Trade-offs + +- [Default CC:Tweaked upgrade serialization returns a constant crafting item] -> Explicitly test complete terminal NBT through equip, save/load, and unequip. +- [A turtle can move or the AE2 grid can change between calls] -> Resolve the linked grid, access point, range, storage, and action source for every call. +- [The linked access-point chunk may be unloaded even when another network access point is nearby] -> Match vanilla terminal anchoring and fail without loading the chunk. +- [Fuel can be consumed when a valid transfer moves zero items] -> Document and test the product-selected per-operation semantics. +- [Fabric and Forge AE2 source is duplicated] -> Keep public behavior and GameTests equivalent and avoid loader-specific behavior in the typed contract. +- [Transfer simulation and mutation could use different action sources] -> Build one owner-derived action source per call and use it for both phases. +- [Terminal Energy Cards no longer affect runtime cost] -> Preserve them for exact item round trips but document that turtle fuel replaces terminal charge while installed. + +## Migration Plan + +This is additive and introduces no world migration. Register the optional upgrade and generated data when AE2 is present, add the typed source, and verify both loaders. Removing the feature unregisters the upgrade; players should unequip terminals before rollback so no equipped upgrade data becomes inaccessible. + +## Open Questions + +- Whether future support for Wireless Crafting Terminals should use a second upgrade ID with the same peripheral contract. +- Whether future AE2 versions expose a public wireless-session helper that can replace the small vanilla-equivalent range resolver. diff --git a/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/proposal.md b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/proposal.md new file mode 100644 index 00000000..923f9bd7 --- /dev/null +++ b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/proposal.md @@ -0,0 +1,32 @@ +## Why + +Turtles cannot directly use a linked AE2 wireless terminal to inspect network items or exchange items with their own inventory. Existing item-storage methods require a separately named source or target peripheral, which is awkward for a turtle whose local 16-slot inventory is the intended endpoint. + +## What Changes + +- Allow a linked standard AE2 Wireless Terminal to be equipped directly as a turtle peripheral upgrade while preserving the terminal's complete item state. +- Add an AE2 wireless terminal peripheral that lists network items and transfers items between the AE2 network and the owning turtle's inventory without a peripheral-name argument. +- Apply vanilla AE2 link, loaded-network, active-access-point, dimension, and wireless-range constraints on every peripheral call without loading chunks. +- Charge one turtle fuel for each valid item-transfer operation through Tweakium's fuel boon while leaving the terminal's AE charge unchanged. +- Add the proposed typed contract at `projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal.ts` and keep generated declarations as build output. +- Add a separate Crafting Monitor turtle upgrade backed by AE2's linked Wireless Crafting Terminal for crafting requests, weak job tracking, and cancellation. +- Keep item storage on the standard Wireless Terminal and crafting control on the Wireless Crafting Terminal; defer fluid storage and cross-dimensional access. + +## Capabilities + +### New Capabilities + +- `ae2-wireless-terminal-turtle-upgrade`: Direct terminal equipping, state preservation, wireless connection validation, network item inspection, turtle-local transfers, fuel charging, a separate Wireless Crafting Terminal monitor upgrade, weak crafting-job tracking, and typed Lua contracts. + +### Modified Capabilities + +None. + +## Impact + +- AE2 integrations in the Fabric and Forge projects. +- Turtle upgrade registration, model registration, language data, and generated upgrade data. +- Persistent CC:Tweaked turtle upgrade NBT containing the equipped terminal state. +- Tweakium turtle ownership, inventory storage, item-query, and fuel-boon integration. +- Typed peripheral sources for the wireless terminal and crafting monitor. +- Fabric and Forge GameTests covering connection constraints, transfers, fuel, and item-state round trips. diff --git a/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/specs/ae2-wireless-terminal-turtle-upgrade/spec.md b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/specs/ae2-wireless-terminal-turtle-upgrade/spec.md new file mode 100644 index 00000000..70a216ae --- /dev/null +++ b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/specs/ae2-wireless-terminal-turtle-upgrade/spec.md @@ -0,0 +1,150 @@ +## ADDED Requirements + +### Requirement: Equip a linked AE2 Wireless Terminal directly +The system SHALL register the standard AE2 Wireless Terminal as the crafting item for a turtle peripheral upgrade with peripheral type `ae2_wireless_terminal`. The system SHALL accept only a terminal containing a valid AE2 access-point link and SHALL NOT require a separate UPW item or crafting recipe. + +#### Scenario: Equip a linked terminal +- **WHEN** a turtle equips a standard AE2 Wireless Terminal containing an access-point link +- **THEN** the turtle gains an `ae2_wireless_terminal` peripheral on that side + +#### Scenario: Reject an unlinked terminal +- **WHEN** a turtle attempts to equip a standard AE2 Wireless Terminal without an access-point link +- **THEN** the item is not accepted as the wireless terminal upgrade + +#### Scenario: Do not accept other terminal variants +- **WHEN** a turtle attempts to equip an AE2 Wireless Crafting Terminal or another terminal variant +- **THEN** the item is not accepted as this upgrade + +### Requirement: Preserve the complete terminal item state +The system SHALL copy the equipped terminal's complete item state into persistent turtle upgrade data and SHALL reconstruct that state when the upgrade is unequipped. Peripheral use SHALL NOT consume or modify the terminal's AE charge. + +#### Scenario: Terminal survives an equip round trip +- **WHEN** a linked, charged, named terminal with installed Energy Cards is equipped, saved, loaded, and unequipped +- **THEN** the returned terminal retains its link, charge, name, upgrades, and all other item NBT + +#### Scenario: Peripheral calls leave terminal charge unchanged +- **WHEN** the turtle lists or transfers items through the upgrade +- **THEN** the terminal's stored AE charge remains unchanged + +### Requirement: Resolve a live vanilla wireless connection for every call +The system SHALL resolve the terminal's linked access point and AE2 grid on every peripheral call, then require an active wireless access point from that grid in the turtle's current dimension and within that access point's range. Resolution SHALL NOT load chunks and SHALL NOT retain a grid or storage reference across calls. + +#### Scenario: Use an active access point in range +- **WHEN** the linked network is loaded and has an active access point whose range contains the turtle +- **THEN** the peripheral call operates on that network + +#### Scenario: Linked access point is unavailable +- **WHEN** the terminal is unlinked, the linked access point is missing or unloaded, or its grid is unavailable +- **THEN** the call fails with an operational error and does not load the access point's chunk + +#### Scenario: Turtle is outside wireless coverage +- **WHEN** no active access point from the linked grid is in the turtle's dimension and range +- **THEN** the call fails with an out-of-range operational error + +#### Scenario: Turtle moves between calls +- **WHEN** the turtle moves out of range after a successful call +- **THEN** the next call revalidates its position and fails rather than using a cached connection + +### Requirement: List network items with item-storage query semantics +The peripheral SHALL expose `items(detailed?, filter?)` with the same detailed/base representations and item-query matching used by the regular `item_storage` API. Listing SHALL include only item keys and SHALL NOT include AE2 fluids or addon key types. + +#### Scenario: List detailed items +- **WHEN** Lua calls `items()` or `items(true)` while connected +- **THEN** the peripheral returns the matching network items using detailed item representations + +#### Scenario: List filtered base items +- **WHEN** Lua calls `items(false, filter)` while connected +- **THEN** the peripheral returns only matching network items using base item representations + +### Requirement: Push network items into the turtle inventory +The peripheral SHALL expose `pullItem(itemQuery?, limit?, toSlot?)`, which moves matching items from the connected AE2 network into the owning turtle's 16-slot inventory. `toSlot`, when present, SHALL use one-based turtle slot numbering. The operation SHALL respect the configured item-storage transfer limit. + +#### Scenario: Pull into any turtle slot +- **WHEN** Lua calls `pullItem(query, limit)` with matching network items and available turtle capacity +- **THEN** up to the effective limit is extracted from AE2 and inserted into the turtle inventory and the moved count is returned + +#### Scenario: Pull into a selected turtle slot +- **WHEN** Lua calls `pullItem(query, limit, toSlot)` with a valid compatible destination slot +- **THEN** items are inserted only into that turtle slot and the moved count is returned + +#### Scenario: Reject an invalid destination slot +- **WHEN** `toSlot` is outside the inclusive range 1 through 16 +- **THEN** the call fails without moving items or consuming fuel + +### Requirement: Pull turtle items into the network +The peripheral SHALL expose `pushItem(fromSlotOrItemQuery?, limit?)`, which moves items from the owning turtle's inventory into the connected AE2 network. A numeric first argument SHALL select a one-based turtle source slot; a string or table SHALL use the regular item-query semantics across the turtle inventory. The operation SHALL respect the configured item-storage transfer limit. + +#### Scenario: Push from any turtle slot +- **WHEN** Lua calls `pushItem(query, limit)` with matching turtle items and available AE2 capacity +- **THEN** up to the effective limit is removed from the turtle inventory, inserted into AE2, and the moved count is returned + +#### Scenario: Push from a selected turtle slot +- **WHEN** Lua calls `pushItem(fromSlot, limit)` with a valid source slot +- **THEN** only items from that turtle slot are offered to AE2 and the moved count is returned + +#### Scenario: Reject an invalid source slot +- **WHEN** `fromSlot` is outside the inclusive range 1 through 16 +- **THEN** the call fails without moving items or consuming fuel + +### Requirement: Charge turtle fuel per valid transfer call +The peripheral SHALL use a Tweakium `FuelBoon` and consume one base turtle fuel for each connected, validated `pushItem` or `pullItem` operation. Fuel SHALL be charged per call regardless of the number of items moved, including when zero items move after validation. Read-only calls and calls rejected before transfer execution SHALL consume no fuel. Turtles configured with fuel disabled SHALL execute without fuel consumption. + +#### Scenario: Successful transfer consumes fuel +- **WHEN** a connected turtle with fuel calls `pushItem` or `pullItem` +- **THEN** one base fuel, adjusted by the configured fuel consumption rate, is consumed + +#### Scenario: Valid no-op transfer consumes fuel +- **WHEN** a connected turtle calls a transfer method but no item matches or the destination accepts nothing +- **THEN** the method returns zero and consumes one base fuel + +#### Scenario: Insufficient fuel prevents transfer +- **WHEN** a fuel-enabled turtle lacks the required fuel for a transfer operation +- **THEN** the call fails and no items move + +#### Scenario: Connection failure does not consume fuel +- **WHEN** a transfer call fails link, grid, dimension, range, or slot validation +- **THEN** no turtle fuel is consumed + +### Requirement: Attribute AE2 mutations to the turtle owner +The system SHALL perform AE2 insertion and extraction with a player action source derived from the turtle's owning-player fake player, following the existing Tweakium turtle-owner mechanism. + +#### Scenario: Transfer uses turtle owner context +- **WHEN** a turtle transfers items with AE2 +- **THEN** both simulated and committed AE2 storage operations use the turtle owner's action source + +### Requirement: Publish a typed peripheral contract +The typed project SHALL define the storage contract in `integrations/ae2WirelessTerminal.ts` and the crafting contract in `integrations/ae2CraftingMonitor.ts`. The storage contract SHALL declare the `ae2_wireless_terminal` provider, item listing overloads, implicit-turtle transfer signatures, and inherited fuel methods. The crafting contract SHALL declare the `ae2_crafting_monitor` provider and job methods. The stationary AE2 contract SHALL expose the same job type, IDs, lookup, listing, and cancellation methods. Generated `.d.ts` and `.lua` files SHALL remain build output. + +#### Scenario: Compile the typed contract +- **WHEN** the typed-peripheral project is built +- **THEN** the new source compiles and produces declarations matching the runtime Lua surface + +#### Scenario: Resolve the peripheral provider +- **WHEN** a TypeScript consumer uses the exported wireless terminal provider +- **THEN** it resolves peripherals whose runtime type is `ae2_wireless_terminal` + +### Requirement: Request and weakly track AE2 crafting jobs +The system SHALL register an `ae2_crafting_monitor` turtle upgrade using an exact linked AE2 Wireless Crafting Terminal and preserve that stack unchanged. A shared crafting-job peripheral plugin SHALL serve stationary AE2 peripherals and crafting monitor upgrades with injected connection and action-source behavior. Successful `scheduleCrafting` calls SHALL retain the existing leading `true` result and additionally return the submitted AE2 crafting-link UUID. The server process SHALL weakly track submitted links per AE2 crafting service without retaining links or grids solely for tracking. The regular `ae2_wireless_terminal` peripheral SHALL remain storage-only. + +#### Scenario: Request a tracked crafting job +- **WHEN** AE2 accepts a crafting request +- **THEN** the call returns `true` and a job ID that can be queried or canceled while the weak link remains available + +#### Scenario: Query a tracked crafting job +- **WHEN** Lua queries a known job ID through a stationary AE2 peripheral or in-range crafting monitor on the same network +- **THEN** it receives the ID, target, requested amount, and `running`, `done`, or `canceled` state + +#### Scenario: Cancel a running job +- **WHEN** Lua cancels a known running job +- **THEN** the AE2 crafting link is canceled and the call returns true + +#### Scenario: Handle a missing job +- **WHEN** an ID is unknown, belongs to another network, or its weakly cached link has been reclaimed +- **THEN** lookup and cancellation return `nil` and a not-found error without throwing + +### Requirement: Keep item storage local-dimensional +The first version's storage API SHALL NOT expose fluids, terminal user-interface settings, cross-dimensional access, or Wireless Crafting Terminal support. Crafting requests MAY target AE2 item or fluid patterns through the existing mode parameter. + +#### Scenario: Network contains non-item keys +- **WHEN** the connected AE2 network contains fluids or addon-defined keys +- **THEN** `items()` omits those keys and transfer methods operate only on items diff --git a/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/tasks.md b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/tasks.md new file mode 100644 index 00000000..704371d6 --- /dev/null +++ b/openspec/changes/add-ae2-wireless-terminal-turtle-upgrade/tasks.md @@ -0,0 +1,45 @@ +## 1. AE2 Storage And Wireless Resolution + +- [x] 1.1 Generalize the Fabric and Forge `AEItemStorage` adapters to accept a caller-supplied `IActionSource` and change callback while preserving stationary ME block behavior. +- [x] 1.2 Add equivalent Fabric and Forge wireless resolvers that reconstruct the stored terminal, resolve its loaded linked access point and current grid, and select an active same-level access point containing the turtle in range without loading chunks. +- [x] 1.3 Return distinct operational errors for invalid stored state, unavailable linked network, and out-of-range access, and ensure each peripheral method resolves a fresh session. + +## 2. Turtle Upgrade State And Registration + +- [x] 2.1 Implement the AE2 wireless terminal turtle upgrade and peripheral in both loader integration source sets using a `TurtlePeripheralOwner` with an attached `FuelBoon`. +- [x] 2.2 Persist the complete equipped Wireless Terminal stack in authoritative side upgrade data and reconstruct it unchanged on unequip, while rejecting unlinked terminals and non-standard terminal variants. +- [x] 2.3 Register the optional `ae2_wireless_terminal` serializer, generated turtle upgrade data, scaled upward-facing terminal model, and English and Ukrainian language entries through the existing AE2 integration hooks. + +## 3. Lua Item API + +- [x] 3.1 Implement `items(detailed?, filter?)` with the regular item-storage representations and query semantics while excluding fluids and addon keys. +- [x] 3.2 Implement `pullItem(itemQuery?, limit?, toSlot?)` from AE2 into the turtle inventory with one-based slot validation and the configured item-storage transfer limit. +- [x] 3.3 Implement `pushItem(fromSlotOrItemQuery?, limit?)` from the turtle inventory into AE2 with one-based slot validation and the configured item-storage transfer limit. +- [x] 3.4 Use one owner-derived player `IActionSource` for transfer simulation and mutation, consume one base fuel after connection and argument validation for every transfer call including valid zero-move calls, and leave terminal AE charge unchanged. + +## 4. Typed Peripheral Contract + +- [x] 4.1 Create `projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal.ts` with item listing overloads, implicit-turtle transfer signatures, optional one-based slot parameters, inherited `FuelApi`, and an `ae2_wireless_terminal` provider. +- [x] 4.2 Add a TypeScript fixture that resolves the provider and type-checks detailed/base listing plus push, pull, slot, and fuel calls. +- [x] 4.3 Run `./gradlew :typescript-tests:compileTestLua --no-daemon` with the required timeout and log capture, and verify generated declarations remain build output. + +## 5. Multi-Loader GameTests + +- [x] 5.1 Add equivalent Fabric and Forge GameTests that equip a linked terminal and verify link, charge, Energy Cards, name, and arbitrary NBT survive save/load and unequip. +- [x] 5.2 Test in-range listing, filtered representations, both transfer directions, explicit turtle slots, transfer limits, and owner-attributed AE2 simulation and mutation. +- [x] 5.3 Test one-fuel-per-call behavior for successful and valid zero-move transfers, insufficient and disabled fuel, free read calls, and no fuel loss on connection or slot validation errors. +- [x] 5.4 Test missing or unloaded linked access points, unavailable grids, inactive and out-of-range access points, turtle movement between calls, same-dimension enforcement, no chunk loading, and unchanged terminal AE charge. + +## 6. Verification + +- [x] 6.1 Run formatting and targeted compile checks for both loaders and fix all failures. +- [x] 6.2 Run the root `gameTest` task for Fabric and Forge under `xvfb-run` with the required timeout and log capture, and verify all existing and new GameTests pass. +- [x] 6.3 Run the timed root `build --no-daemon` with complete log capture and verify the multi-loader build and typed-peripheral outputs pass. + +## 7. Crafting Job Tracking + +- [x] 7.1 Return AE2 crafting-link UUIDs from successful stationary and crafting-monitor `scheduleCrafting` calls while preserving existing response ordering and errors. +- [x] 7.2 Extract one shared stationary/turtle crafting-job peripheral plugin with injected context, weakly cached links, lookup, listing, cancellation, terminal-state, and explicit missing-job responses. +- [x] 7.3 Register a separate `ae2_crafting_monitor` upgrade using a linked Wireless Crafting Terminal and remove crafting methods from the regular wireless terminal. +- [x] 7.4 Publish separate crafting-monitor typings and exercise runtime missing-job responses through the crafting monitor. +- [x] 7.5 Run datagen, strict OpenSpec validation, the minimal multi-loader GameTests, and the timed root build. diff --git a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/turtle/ScaledItemModeller.kt b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/turtle/ScaledItemModeller.kt index 0af79cef..25a6a70f 100644 --- a/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/turtle/ScaledItemModeller.kt +++ b/projects/core/src/main/kotlin/site/siredvin/peripheralworks/client/turtle/ScaledItemModeller.kt @@ -9,13 +9,14 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade import dan200.computercraft.api.turtle.TurtleSide import org.joml.Quaternionf -class ScaledItemModeller(scaleFactor: Float, modelPixelSize: Int = 16) : TurtleUpgradeModeller { +class ScaledItemModeller(scaleFactor: Float, modelPixelSize: Int = 16, xRotationDegrees: Float = 0f, heightShift: Float = 0f) : TurtleUpgradeModeller { companion object { - fun buildMatrix(side: TurtleSide, scaleFactor: Float, modelPixelSize: Int): Transformation { + fun buildMatrix(side: TurtleSide, scaleFactor: Float, modelPixelSize: Int, xRotationDegrees: Float = 0f, heightShift: Float = 0f): Transformation { val shiftFactor = (1 - scaleFactor) / (2 * scaleFactor) val stack = PoseStack() - stack.translate(0.5f, 0.5f, 0.5f) + stack.translate(0.5f, 0.5f + heightShift, 0.5f) + stack.mulPose(Quaternionf().rotateLocalX(xRotationDegrees * 0.017453292f)) stack.mulPose(Quaternionf().rotateLocalY(90f * 0.017453292f)) stack.translate(-0.5f, -0.5f, -0.5f) stack.pushPose() @@ -37,8 +38,8 @@ class ScaledItemModeller(scaleFactor: Float, modelPixelSize: } } - private val leftTransformation = buildMatrix(TurtleSide.LEFT, scaleFactor, modelPixelSize) - private val rightTransformation = buildMatrix(TurtleSide.RIGHT, scaleFactor, modelPixelSize) + private val leftTransformation = buildMatrix(TurtleSide.LEFT, scaleFactor, modelPixelSize, xRotationDegrees, heightShift) + private val rightTransformation = buildMatrix(TurtleSide.RIGHT, scaleFactor, modelPixelSize, xRotationDegrees, heightShift) override fun getModel( upgrade: T, diff --git a/projects/core/src/testMod/resources/gameteststructures/peripheralworksgametests.ae2_wireless_terminal.snbt b/projects/core/src/testMod/resources/gameteststructures/peripheralworksgametests.ae2_wireless_terminal.snbt new file mode 100644 index 00000000..ad1e4e5b --- /dev/null +++ b/projects/core/src/testMod/resources/gameteststructures/peripheralworksgametests.ae2_wireless_terminal.snbt @@ -0,0 +1 @@ +{size:[7,4,7],entities:[],data:[{pos:[3,0,2],state:"minecraft:polished_andesite"},{pos:[3,1,2],state:"computercraft:turtle_normal{facing:south,waterlogged:false}",nbt:{ComputerId:1,Fuel:10,Items:[{Count:8b,Slot:15b,id:"minecraft:gold_ingot"}],Label:"peripheralworksgametests.ae2_wireless_terminal",On:0b,Owner:{LowerId:-6876936588741668278L,Name:"Dev",UpperId:4039158846114182220L},Slot:0,id:"computercraft:turtle_normal"}}],palette:["minecraft:polished_andesite","computercraft:turtle_normal{facing:south,waterlogged:false}"],DataVersion:3465} diff --git a/projects/fabric/build.gradle.kts b/projects/fabric/build.gradle.kts index 79564349..bac1b263 100644 --- a/projects/fabric/build.gradle.kts +++ b/projects/fabric/build.gradle.kts @@ -35,8 +35,11 @@ fabricShaking { } if (minimalTestEnvironment) { - sourceSets.main { kotlin.exclude("site/siredvin/peripheralworks/integrations/**") } - tasks.named("compileKotlin") { exclude("**/integrations/**") } + val excludedIntegrations = file("src/main/kotlin/site/siredvin/peripheralworks/integrations").listFiles()!! + .filter { it.isDirectory && it.name != "ae2" } + .map { "**/integrations/${it.name}/**" } + sourceSets.main { kotlin.exclude(excludedIntegrations) } + tasks.named("compileKotlin") { exclude(excludedIntegrations) } } val testMod = sourceSets.create("testMod") { @@ -112,10 +115,8 @@ repositories { } dependencies { - if (!minimalTestEnvironment) { - modApi(libs.bundles.externalMods.fabric.integrations.api) { - exclude("net.fabricmc.fabric-api") - } + modApi(libs.bundles.externalMods.fabric.integrations.api) { + exclude("net.fabricmc.fabric-api") } modImplementation(libs.bundles.fabric.core) @@ -135,7 +136,9 @@ dependencies { exclude("net.fabricmc", "fabric-loader") } - if (!minimalTestEnvironment) { + if (minimalTestEnvironment) { + modImplementation(libs.ae2.fabric) + } else { libs.bundles.externalMods.fabric.integrations.full.get().map { modCompileOnly(it) } libs.bundles.externalMods.fabric.integrations.active.get().map { modRuntimeOnly(it) } libs.bundles.externalMods.fabric.integrations.activedep.get().map { modRuntimeOnly(it) } diff --git a/projects/fabric/src/generated/resources/.cache/02059454eca80d9e0c9e9e11fd54b2d59e706782 b/projects/fabric/src/generated/resources/.cache/02059454eca80d9e0c9e9e11fd54b2d59e706782 index d08372e6..3e0235d6 100644 --- a/projects/fabric/src/generated/resources/.cache/02059454eca80d9e0c9e9e11fd54b2d59e706782 +++ b/projects/fabric/src/generated/resources/.cache/02059454eca80d9e0c9e9e11fd54b2d59e706782 @@ -1,2 +1,2 @@ -// 1.20.1 2026-07-26T10:53:36.338538167 Unlimited Peripheral Works/ComputerLanguageuk_ua -fff76a15a1370138c05a5a2a960863839e43eb78 assets/peripheralworks/lang/uk_ua.json +// 1.20.1 2026-08-04T16:34:08.472242312 Unlimited Peripheral Works/ComputerLanguageuk_ua +c8d94a3ecb2aff5520d03b37fc5b860dae444a99 assets/peripheralworks/lang/uk_ua.json diff --git a/projects/fabric/src/generated/resources/.cache/0bc303fc01858bc5bb03e4fc1dd179f5647fa6e9 b/projects/fabric/src/generated/resources/.cache/0bc303fc01858bc5bb03e4fc1dd179f5647fa6e9 index fa8d7928..59f5c056 100644 --- a/projects/fabric/src/generated/resources/.cache/0bc303fc01858bc5bb03e4fc1dd179f5647fa6e9 +++ b/projects/fabric/src/generated/resources/.cache/0bc303fc01858bc5bb03e4fc1dd179f5647fa6e9 @@ -1,2 +1,2 @@ -// 1.20.1 2026-07-26T10:53:36.337098203 Unlimited Peripheral Works/ComputerLanguageen_us -da2584953c12d8da1c2136f644791cf4b53e98e7 assets/peripheralworks/lang/en_us.json +// 1.20.1 2026-08-04T16:34:08.470505558 Unlimited Peripheral Works/ComputerLanguageen_us +cd4e92fe3d59cdde19bfb8dc20fb68a284246336 assets/peripheralworks/lang/en_us.json diff --git a/projects/fabric/src/generated/resources/.cache/531a261a2f38c8bf4ccd118f4d3ba50c31382e4c b/projects/fabric/src/generated/resources/.cache/531a261a2f38c8bf4ccd118f4d3ba50c31382e4c index bf7ce213..236403ab 100644 --- a/projects/fabric/src/generated/resources/.cache/531a261a2f38c8bf4ccd118f4d3ba50c31382e4c +++ b/projects/fabric/src/generated/resources/.cache/531a261a2f38c8bf4ccd118f4d3ba50c31382e4c @@ -1,4 +1,4 @@ -// 1.20.1 2026-07-26T10:53:36.338088049 Unlimited Peripheral Works/Recipes +// 1.20.1 2026-08-04T16:34:08.471901553 Unlimited Peripheral Works/Recipes 379f6f63779c41d1be5a1de6c1315cdc0e712956 data/peripheralworks/recipes/display_pedestal.json a2696e406e9032af7d01eb6c0b534b706f6d9f5f data/minecraft/recipes/card_clean.json 09177d9534a5723cf4e8af294cb05de7b5b398e2 data/minecraft/recipes/statue_cloning.json diff --git a/projects/fabric/src/generated/resources/.cache/607a26b838614ef5b743d5bbf3ae3d2c499369d8 b/projects/fabric/src/generated/resources/.cache/607a26b838614ef5b743d5bbf3ae3d2c499369d8 index 8d86d20d..9223751e 100644 --- a/projects/fabric/src/generated/resources/.cache/607a26b838614ef5b743d5bbf3ae3d2c499369d8 +++ b/projects/fabric/src/generated/resources/.cache/607a26b838614ef5b743d5bbf3ae3d2c499369d8 @@ -1,4 +1,4 @@ -// 1.20.1 2026-07-26T10:53:36.334653245 Unlimited Peripheral Works/Pocket Computer Upgrades +// 1.20.1 2026-08-04T16:34:08.469052612 Unlimited Peripheral Works/Pocket Computer Upgrades 2d73257c0a967655f5a7acfa66bf9c39308db3f1 data/peripheralworks/computercraft/pocket_upgrades/ultimate_sensor.json 911a593d4aafc165c692b127978a82c7f08b960e data/peripheralworks/computercraft/pocket_upgrades/hologram_projector.json c6585d2541a807aca0f82eb85fd2e9aa81dd677c data/peripheralworks/computercraft/pocket_upgrades/universal_scanner.json diff --git a/projects/fabric/src/generated/resources/.cache/6395424bffbf73e09f2569a090a0c7904acfe74e b/projects/fabric/src/generated/resources/.cache/6395424bffbf73e09f2569a090a0c7904acfe74e index 790bc1f0..ce87ace6 100644 --- a/projects/fabric/src/generated/resources/.cache/6395424bffbf73e09f2569a090a0c7904acfe74e +++ b/projects/fabric/src/generated/resources/.cache/6395424bffbf73e09f2569a090a0c7904acfe74e @@ -1,2 +1,2 @@ -// 1.20.1 2026-07-26T10:53:36.336781249 Unlimited Peripheral Works/Tags for minecraft:entity_type +// 1.20.1 2026-08-04T16:34:08.470360861 Unlimited Peripheral Works/Tags for minecraft:entity_type fd653137ea41bed52ba37a69079d9c9c67d5c0c5 data/peripheralworks/tags/entity_types/link_blocklist.json diff --git a/projects/fabric/src/generated/resources/.cache/71c885c52f014a226c863b3b638bfcbefe557470 b/projects/fabric/src/generated/resources/.cache/71c885c52f014a226c863b3b638bfcbefe557470 index 824a5473..7e64f3f9 100644 --- a/projects/fabric/src/generated/resources/.cache/71c885c52f014a226c863b3b638bfcbefe557470 +++ b/projects/fabric/src/generated/resources/.cache/71c885c52f014a226c863b3b638bfcbefe557470 @@ -1,7 +1,7 @@ -// 1.20.1 2026-07-26T10:53:36.337291381 Unlimited Peripheral Works/minecraft:block Loot Table +// 1.20.1 2026-08-04T16:34:08.4706229 Unlimited Peripheral Works/minecraft:block Loot Table c41e20b1f8780d66f6a04d3be5bd08af1a4f0067 data/peripheralworks/loot_tables/blocks/display_pedestal.json -fbbfca4d72a5799857020a3d05f2541ca11c4d71 data/peripheralworks/loot_tables/blocks/item_pedestal.json 49ceaf9817f5ac29e3613656fc964267d13e4ec4 data/peripheralworks/loot_tables/blocks/reality_forger.json +fbbfca4d72a5799857020a3d05f2541ca11c4d71 data/peripheralworks/loot_tables/blocks/item_pedestal.json 320af2c7b0ae9a8a6323b415204098004eeceb0c data/peripheralworks/loot_tables/blocks/statue_workbench.json d687230cc9ef8c2e199ff471525d3244bd321197 data/peripheralworks/loot_tables/blocks/map_pedestal.json a6c257852185d4ed159fd5d346ab4b2c7fa5238e data/peripheralworks/loot_tables/blocks/remote_observer.json diff --git a/projects/fabric/src/generated/resources/.cache/767876eefc79819d4fe92ae5cc7876886174715a b/projects/fabric/src/generated/resources/.cache/767876eefc79819d4fe92ae5cc7876886174715a index cbb90ecf..f7e01d1a 100644 --- a/projects/fabric/src/generated/resources/.cache/767876eefc79819d4fe92ae5cc7876886174715a +++ b/projects/fabric/src/generated/resources/.cache/767876eefc79819d4fe92ae5cc7876886174715a @@ -1,4 +1,4 @@ -// 1.20.1 2026-07-26T10:53:36.335494128 Unlimited Peripheral Works/Block State Definitions +// 1.20.1 2026-08-04T16:34:08.46966642 Unlimited Peripheral Works/Block State Definitions 36c244142cb28bcce875cbcb68f6102141bf78d5 assets/peripheralworks/blockstates/peripheral_casing.json efd5ba47bc8f73def3c977d8887af54ef4ad1370 assets/peripheralworks/models/block/map_pedestal.json c8fb84d1128c18b2958d371b6b86c1b3ce6ef08e assets/peripheralworks/blockstates/ultimate_sensor.json @@ -10,8 +10,8 @@ fa09940646b26b32e317d46436ba6e5708942a3d assets/peripheralworks/models/block/sta 767287600ba5632ed8a38b3d2675cc8ff924a609 assets/peripheralworks/models/item/flexible_reality_anchor.json c23515b1dd22a9ab692eb1d023862f1e952c93ff assets/peripheralworks/blockstates/map_pedestal.json 778759f975277f38234eec1b10e5b25082f838e1 assets/peripheralworks/models/item/peripheralium_hub.json -3f9d98dbf30a19bef2f380a22aa02a21e5c2a49d assets/peripheralworks/blockstates/universal_scanner.json d51f30f284b2cf682dcc04372764ce53718de77c assets/peripheralworks/blockstates/informative_registry.json +3f9d98dbf30a19bef2f380a22aa02a21e5c2a49d assets/peripheralworks/blockstates/universal_scanner.json c584ae8cb969815c79c844b4063142c7b18acc5a assets/peripheralworks/models/turtle/hologram_projector_left.json 8d77c1f92bd14f27cd16d72a9ea98032581def5c assets/peripheralworks/models/item/netherite_peripheralium_hub.json a83df0770a4d517d4f4d3bb447d0eda00bcfc4a1 assets/peripheralworks/blockstates/statue_workbench.json diff --git a/projects/fabric/src/generated/resources/.cache/82679a7db4466355c130a976d950498c822528b1 b/projects/fabric/src/generated/resources/.cache/82679a7db4466355c130a976d950498c822528b1 index e13d22ce..b8b6da49 100644 --- a/projects/fabric/src/generated/resources/.cache/82679a7db4466355c130a976d950498c822528b1 +++ b/projects/fabric/src/generated/resources/.cache/82679a7db4466355c130a976d950498c822528b1 @@ -1,6 +1,8 @@ -// 1.20.1 2026-07-26T10:53:36.337809388 Unlimited Peripheral Works/Turtle Upgrades +// 1.20.1 2026-08-04T16:34:08.470975171 Unlimited Peripheral Works/Turtle Upgrades c6585d2541a807aca0f82eb85fd2e9aa81dd677c data/peripheralworks/computercraft/turtle_upgrades/universal_scanner.json +938c77ddc4b7e3d1f4b7a7a1adcf4c610c64f1d7 data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json 2246ea1d5805e9afda36c4f1265f520598315dc9 data/peripheralworks/computercraft/turtle_upgrades/netherite_peripheralium_hub.json +25be7c06a318cf62fa70a4f814e8e0370076cd20 data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json 2d73257c0a967655f5a7acfa66bf9c39308db3f1 data/peripheralworks/computercraft/turtle_upgrades/ultimate_sensor.json 911a593d4aafc165c692b127978a82c7f08b960e data/peripheralworks/computercraft/turtle_upgrades/hologram_projector.json d718e1fc9579bfe9b1ed23e3c237508eaab815a9 data/peripheralworks/computercraft/turtle_upgrades/natures_compass.json diff --git a/projects/fabric/src/generated/resources/.cache/fc9938a5ed47eba4f6288c566e1715997233e407 b/projects/fabric/src/generated/resources/.cache/fc9938a5ed47eba4f6288c566e1715997233e407 index e1279b16..d4c9c247 100644 --- a/projects/fabric/src/generated/resources/.cache/fc9938a5ed47eba4f6288c566e1715997233e407 +++ b/projects/fabric/src/generated/resources/.cache/fc9938a5ed47eba4f6288c566e1715997233e407 @@ -1,4 +1,4 @@ -// 1.20.1 2026-07-26T10:53:36.337583137 Unlimited Peripheral Works/Tags for minecraft:block +// 1.20.1 2026-08-04T16:34:08.470845684 Unlimited Peripheral Works/Tags for minecraft:block b5037659257ff31abadd72db3bd95e96e1a4af16 data/peripheralworks/tags/blocks/peripheral_proxy_forbidden.json 4e7a5645e6b3b9adc27b24377cbd15d93f6587bd data/computercraft/tags/blocks/peripheral_hub_ignore.json acf9379ee065125e563d0d56d0e3ea5db91e9815 data/peripheralworks/tags/blocks/reality_forger_forbidden.json diff --git a/projects/fabric/src/generated/resources/.cache/ff035bb697732aad9f15f284633c652c364b9404 b/projects/fabric/src/generated/resources/.cache/ff035bb697732aad9f15f284633c652c364b9404 index 275cb0cb..48afa092 100644 --- a/projects/fabric/src/generated/resources/.cache/ff035bb697732aad9f15f284633c652c364b9404 +++ b/projects/fabric/src/generated/resources/.cache/ff035bb697732aad9f15f284633c652c364b9404 @@ -1,3 +1,3 @@ -// 1.20.1 2026-07-26T10:53:36.338807729 Unlimited Peripheral Works/Tags for minecraft:item +// 1.20.1 2026-08-04T16:34:08.472438595 Unlimited Peripheral Works/Tags for minecraft:item 31939d3b804f748a932a97bb3d1ca071c930070b data/peripheralworks/tags/items/peripheral_proxy_forbidden.json d419b420963d6122db39dd885d326a0aa4bfc8eb data/peripheralworks/tags/items/reality_forger_forbidden.json diff --git a/projects/fabric/src/generated/resources/assets/peripheralworks/lang/en_us.json b/projects/fabric/src/generated/resources/assets/peripheralworks/lang/en_us.json index 809a2943..155ca3fb 100644 --- a/projects/fabric/src/generated/resources/assets/peripheralworks/lang/en_us.json +++ b/projects/fabric/src/generated/resources/assets/peripheralworks/lang/en_us.json @@ -157,6 +157,8 @@ "tooltip.peripheralworks.remote_observer_range": " §6Max range of observed block: %s", "tooltip.peripheralworks.universal_scanner_free_range": " §6Cost-free scan range: %s", "tooltip.peripheralworks.universal_scanner_max_range": " §6Max scan range: %s", + "turtle.peripheralworks.ae2_crafting_monitor": "AE crafting monitor", + "turtle.peripheralworks.ae2_wireless_terminal": "AE terminal", "turtle.peripheralworks.hologram_projector": "Projecting", "turtle.peripheralworks.natures_compass": "Nature Compassing", "turtle.peripheralworks.netherite_peripheralium_hub": "Netherite Hub", diff --git a/projects/fabric/src/generated/resources/assets/peripheralworks/lang/uk_ua.json b/projects/fabric/src/generated/resources/assets/peripheralworks/lang/uk_ua.json index dd05d965..2d1fe21b 100644 --- a/projects/fabric/src/generated/resources/assets/peripheralworks/lang/uk_ua.json +++ b/projects/fabric/src/generated/resources/assets/peripheralworks/lang/uk_ua.json @@ -157,6 +157,8 @@ "tooltip.peripheralworks.remote_observer_range": " §6Максимальна дальність стостерігання: %s", "tooltip.peripheralworks.universal_scanner_free_range": " §6Бескоштовний радіус сканування: %s", "tooltip.peripheralworks.universal_scanner_max_range": " §6Максимальний радіус сканування: %s", + "turtle.peripheralworks.ae2_crafting_monitor": "AE монітор крафтингу", + "turtle.peripheralworks.ae2_wireless_terminal": "AE термінальна", "turtle.peripheralworks.hologram_projector": "Проекуюча", "turtle.peripheralworks.natures_compass": "Природновідчуваюча", "turtle.peripheralworks.netherite_peripheralium_hub": "З вбудованим незеритовим осередком", diff --git a/projects/fabric/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json b/projects/fabric/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json new file mode 100644 index 00000000..f9e8b972 --- /dev/null +++ b/projects/fabric/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json @@ -0,0 +1,12 @@ +{ + "fabric:load_conditions": [ + { + "condition": "fabric:all_mods_loaded", + "values": [ + "ae2" + ] + } + ], + "type": "peripheralworks:ae2_crafting_monitor", + "item": "ae2:wireless_crafting_terminal" +} \ No newline at end of file diff --git a/projects/fabric/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json b/projects/fabric/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json new file mode 100644 index 00000000..f18faf2c --- /dev/null +++ b/projects/fabric/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json @@ -0,0 +1,12 @@ +{ + "fabric:load_conditions": [ + { + "condition": "fabric:all_mods_loaded", + "values": [ + "ae2" + ] + } + ], + "type": "peripheralworks:ae2_wireless_terminal", + "item": "ae2:wireless_terminal" +} \ No newline at end of file diff --git a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingJobs.kt b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingJobs.kt new file mode 100644 index 00000000..80c62987 --- /dev/null +++ b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingJobs.kt @@ -0,0 +1,159 @@ +package site.siredvin.peripheralworks.integrations.ae2 + +import appeng.api.networking.crafting.CalculationStrategy +import appeng.api.networking.crafting.ICraftingLink +import appeng.api.networking.crafting.ICraftingService +import appeng.api.networking.security.IActionSource +import appeng.blockentity.grid.AENetworkBlockEntity +import appeng.core.definitions.AEItems +import dan200.computercraft.api.lua.LuaFunction +import dan200.computercraft.api.lua.MethodResult +import net.minecraft.core.BlockPos +import net.minecraft.core.Direction +import net.minecraft.world.level.Level +import site.siredvin.broccolium.modules.platform.PlatformToolkit +import site.siredvin.peripheralworks.api.PeripheralPluginProvider +import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.buildKey +import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.keyCounterToLua +import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.stackToMap +import site.siredvin.tweakium.modules.peripheral.api.IPeripheralPlugin +import site.siredvin.tweakium.modules.peripheral.owner.TurtlePeripheralOwner +import java.lang.ref.WeakReference +import java.util.Collections +import java.util.Locale +import java.util.Optional +import java.util.WeakHashMap + +object AE2CraftingJobs { + private data class Job(val service: WeakReference, val target: Map, val amount: Long) + + // ponytail: job counts are tiny; scan weak keys until UUID lookup is proven necessary. + private val jobs = Collections.synchronizedMap(WeakHashMap()) + + fun schedule( + level: Level, + service: ICraftingService, + source: IActionSource, + mode: String, + id: String, + amount: Optional, + targetCPU: Optional, + ): MethodResult { + val publicAmount = amount.orElse(if (mode == "item") 1 else 1000) + if (publicAmount <= 0) return MethodResult.of(null, "Amount must be positive") + val key = buildKey(mode, id) + val realAmount = try { + if (mode == "fluid") Math.multiplyExact(publicAmount, PlatformToolkit.get().fluidCompactDivider.toLong()) else publicAmount + } catch (_: ArithmeticException) { + return MethodResult.of(null, "Amount is too large") + } + val plan = service.beginCraftingCalculation(level, { source }, key, realAmount, CalculationStrategy.REPORT_MISSING_ITEMS).get() + if (!plan.missingItems().isEmpty) return MethodResult.of(false, "Missing items", keyCounterToLua(plan.missingItems())) + val cpu = if (targetCPU.isPresent) { + service.cpus.firstOrNull { it.name?.string == targetCPU.get() } + ?: return MethodResult.of(null, "Cannot find target CPU") + } else { + null + } + val submitted = service.submitJob(plan, null, cpu, false, source) + if (!submitted.successful()) { + return MethodResult.of(null, "Cannot submit crafting job: ${submitted.errorCode()?.name?.lowercase(Locale.ROOT) ?: "unknown error"}") + } + val link = submitted.link() ?: return MethodResult.of(null, "AE2 did not return a crafting job") + val jobID = link.craftingID.toString() + jobs[link] = Job(WeakReference(service), stackToMap(plan.finalOutput()), publicAmount) + return MethodResult.of(true, jobID) + } + + fun get(service: ICraftingService, jobID: String): MethodResult { + val job = find(service, jobID) ?: return missing(jobID) + return MethodResult.of(toMap(job.first, job.second)) + } + + fun getAll(service: ICraftingService): List> = synchronized(jobs) { + jobs.mapNotNull { (link, job) -> if (job.service.get() === service) toMap(link, job) else null } + } + + fun cancel(service: ICraftingService, jobID: String): MethodResult { + val (link) = find(service, jobID) ?: return missing(jobID) + if (link.isCanceled) return MethodResult.of(false, "Crafting job '$jobID' is already canceled") + if (link.isDone) return MethodResult.of(false, "Crafting job '$jobID' is already done") + link.cancel() + return MethodResult.of(true) + } + + private fun find(service: ICraftingService, jobID: String): Pair? = synchronized(jobs) { + jobs.entries.firstOrNull { (link, job) -> link.craftingID.toString() == jobID && job.service.get() === service } + ?.let { it.key to it.value } + } + + private fun toMap(link: ICraftingLink, job: Job): Map = mapOf( + "id" to link.craftingID.toString(), + "state" to when { + link.isCanceled -> "canceled" + link.isDone -> "done" + else -> "running" + }, + "target" to job.target, + "amount" to job.amount, + ) + + private fun missing(jobID: String): MethodResult = MethodResult.of(null, "Crafting job '$jobID' was not found") +} + +class AE2CraftingJobsPlugin private constructor( + private val resolve: () -> Context?, + private val withActionSource: (((IActionSource) -> MethodResult) -> MethodResult), + private val unavailableMessage: String, +) : IPeripheralPlugin { + private data class Context(val level: Level, val service: ICraftingService) + + @LuaFunction(mainThread = false) + fun scheduleCrafting(mode: String, id: String, amount: Optional, targetCPU: Optional): MethodResult { + val context = resolve() ?: return unavailable() + return withActionSource { source -> AE2CraftingJobs.schedule(context.level, context.service, source, mode, id, amount, targetCPU) } + } + + @LuaFunction(mainThread = true) + fun getCraftingJob(jobID: String): MethodResult { + val context = resolve() ?: return unavailable() + return AE2CraftingJobs.get(context.service, jobID) + } + + @LuaFunction(mainThread = true) + fun getCraftingJobs(): List> = resolve()?.let { AE2CraftingJobs.getAll(it.service) } ?: emptyList() + + @LuaFunction(mainThread = true) + fun cancelCrafting(jobID: String): MethodResult { + val context = resolve() ?: return unavailable() + return AE2CraftingJobs.cancel(context.service, jobID) + } + + private fun unavailable(): MethodResult = MethodResult.of(null, unavailableMessage) + + companion object { + fun forMachine(level: Level, entity: AENetworkBlockEntity) = AE2CraftingJobsPlugin( + resolve = { entity.mainNode.grid?.craftingService?.let { Context(level, it) } }, + withActionSource = { callback -> callback(IActionSource.ofMachine(entity)) }, + unavailableMessage = "AE2 network is not connected", + ) + + fun forTurtle(owner: TurtlePeripheralOwner) = AE2CraftingJobsPlugin( + resolve = { + owner.level?.let { level -> Context(level, resolveWirelessSession(owner, AEItems.WIRELESS_CRAFTING_TERMINAL.asItem()).craftingService) } + }, + withActionSource = { callback -> owner.withPlayer({ callback(IActionSource.ofPlayer(it.fakePlayer)) }, skipInventory = true) }, + unavailableMessage = "Linked AE2 network is unavailable", + ) + } +} + +object AE2CraftingJobsPluginProvider : PeripheralPluginProvider { + override val pluginType = "ae2_crafting_jobs" + + override fun provide(level: Level, pos: BlockPos, side: Direction): IPeripheralPlugin? { + if (!Configuration.enableMEInterface) return null + val entity = level.getBlockEntity(pos) as? AENetworkBlockEntity ?: return null + return AE2CraftingJobsPlugin.forMachine(level, entity) + } +} diff --git a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingMonitor.kt b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingMonitor.kt new file mode 100644 index 00000000..c3ea8767 --- /dev/null +++ b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingMonitor.kt @@ -0,0 +1,48 @@ +package site.siredvin.peripheralworks.integrations.ae2 + +import appeng.core.definitions.AEItems +import dan200.computercraft.api.turtle.ITurtleAccess +import dan200.computercraft.api.turtle.TurtleSide +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import site.siredvin.peripheralworks.PeripheralWorksCore +import site.siredvin.tweakium.modules.peripheral.OwnedPeripheral +import site.siredvin.tweakium.modules.peripheral.owner.TurtlePeripheralOwner +import site.siredvin.tweakium.modules.turtle.PeripheralTurtleUpgrade + +class AE2CraftingMonitorUpgrade(stack: ItemStack) : PeripheralTurtleUpgrade(UPGRADE_ID, stack) { + override fun buildPeripheral(turtle: ITurtleAccess, side: TurtleSide): AE2CraftingMonitorPeripheral = AE2CraftingMonitorPeripheral.create(turtle, side) + + override fun getUpgradeData(stack: ItemStack): CompoundTag = CompoundTag().apply { + put(AE2_TERMINAL_TAG, stack.save(CompoundTag())) + } + + override fun getUpgradeItem(upgradeData: CompoundTag): ItemStack = if (upgradeData.contains(AE2_TERMINAL_TAG, Tag.TAG_COMPOUND.toInt())) { + ItemStack.of(upgradeData.getCompound(AE2_TERMINAL_TAG)) + } else { + craftingItem + } + + override fun isItemSuitable(stack: ItemStack): Boolean = AEItems.WIRELESS_CRAFTING_TERMINAL.isSameAs(stack) && + AEItems.WIRELESS_CRAFTING_TERMINAL.asItem().getLinkedPosition(stack) != null + + companion object { + val UPGRADE_ID = ResourceLocation(PeripheralWorksCore.MOD_ID, AE2CraftingMonitorPeripheral.TYPE) + } +} + +class AE2CraftingMonitorPeripheral private constructor(owner: TurtlePeripheralOwner) : OwnedPeripheral(TYPE, owner) { + override val isEnabled = true + + init { + addPlugin(AE2CraftingJobsPlugin.forTurtle(owner)) + } + + companion object { + const val TYPE = "ae2_crafting_monitor" + + fun create(turtle: ITurtleAccess, side: TurtleSide): AE2CraftingMonitorPeripheral = AE2CraftingMonitorPeripheral(TurtlePeripheralOwner(turtle, side)) + } +} diff --git a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2WirelessTerminal.kt b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2WirelessTerminal.kt new file mode 100644 index 00000000..af5633b5 --- /dev/null +++ b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2WirelessTerminal.kt @@ -0,0 +1,153 @@ +package site.siredvin.peripheralworks.integrations.ae2 + +import appeng.api.implementations.blockentities.IWirelessAccessPoint +import appeng.api.networking.crafting.ICraftingService +import appeng.api.networking.security.IActionSource +import appeng.api.storage.MEStorage +import appeng.blockentity.networking.WirelessAccessPointBlockEntity +import appeng.core.definitions.AEItems +import appeng.items.tools.powered.WirelessTerminalItem +import dan200.computercraft.api.lua.IArguments +import dan200.computercraft.api.lua.LuaException +import dan200.computercraft.api.lua.LuaFunction +import dan200.computercraft.api.turtle.ITurtleAccess +import dan200.computercraft.api.turtle.TurtleSide +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import site.siredvin.peripheralworks.PeripheralWorksCore +import site.siredvin.peripheralworks.common.configuration.PeripheralWorksConfig +import site.siredvin.tweakium.modules.peripheral.OwnedPeripheral +import site.siredvin.tweakium.modules.peripheral.api.IPeripheralPlugin +import site.siredvin.tweakium.modules.peripheral.boon.PeripheralOwnerBoonKey +import site.siredvin.tweakium.modules.peripheral.owner.TurtlePeripheralOwner +import site.siredvin.tweakium.modules.peripheral.representation.LuaRepresentation +import site.siredvin.tweakium.modules.peripheral.representation.RepresentationMode +import site.siredvin.tweakium.modules.plugins.PeripheralPluginUtils +import site.siredvin.tweakium.modules.turtle.PeripheralTurtleUpgrade +import java.util.Optional +import java.util.function.Predicate +import kotlin.math.min + +internal const val AE2_TERMINAL_TAG = "terminal" + +internal data class AE2WirelessSession(val storage: MEStorage, val craftingService: ICraftingService) + +internal fun resolveWirelessSession(owner: TurtlePeripheralOwner, terminal: WirelessTerminalItem): AE2WirelessSession { + val data = owner.turtle.getUpgradeNBTData(owner.side) + if (!data.contains(AE2_TERMINAL_TAG, Tag.TAG_COMPOUND.toInt())) throw LuaException("Invalid stored wireless terminal") + val stack = ItemStack.of(data.getCompound(AE2_TERMINAL_TAG)) + if (stack.item !== terminal || terminal.getLinkedPosition(stack) == null) throw LuaException("Invalid stored wireless terminal") + val level = owner.level ?: throw LuaException("Linked AE2 network is unavailable") + val grid = terminal.getLinkedGrid(stack, level, null) ?: throw LuaException("Linked AE2 network is unavailable") + val inRange = grid.getMachines(WirelessAccessPointBlockEntity::class.java).any { accessPoint -> + isInWirelessRange(accessPoint, level, owner.pos) + } + if (!inRange) throw LuaException("Turtle is outside wireless range") + return AE2WirelessSession(grid.storageService.inventory, grid.craftingService) +} + +private fun isInWirelessRange(accessPoint: IWirelessAccessPoint, level: net.minecraft.world.level.Level, pos: net.minecraft.core.BlockPos): Boolean = accessPoint.isActive && accessPoint.location.level === level && accessPoint.location.pos.distSqr(pos) < accessPoint.range * accessPoint.range + +class AE2WirelessTerminalUpgrade(stack: ItemStack) : PeripheralTurtleUpgrade(UPGRADE_ID, stack) { + override fun buildPeripheral(turtle: ITurtleAccess, side: TurtleSide): AE2WirelessTerminalPeripheral = AE2WirelessTerminalPeripheral.create(turtle, side) + + override fun getUpgradeData(stack: ItemStack): CompoundTag = CompoundTag().apply { + put(AE2_TERMINAL_TAG, stack.save(CompoundTag())) + } + + override fun getUpgradeItem(upgradeData: CompoundTag): ItemStack = if (upgradeData.contains(AE2_TERMINAL_TAG, Tag.TAG_COMPOUND.toInt())) { + ItemStack.of(upgradeData.getCompound(AE2_TERMINAL_TAG)) + } else { + craftingItem + } + + override fun isItemSuitable(stack: ItemStack): Boolean = AEItems.WIRELESS_TERMINAL.isSameAs(stack) && + AEItems.WIRELESS_TERMINAL.asItem().getLinkedPosition(stack) != null + + companion object { + val UPGRADE_ID = ResourceLocation(PeripheralWorksCore.MOD_ID, AE2WirelessTerminalPeripheral.TYPE) + } +} + +class AE2WirelessTerminalPeripheral private constructor(owner: TurtlePeripheralOwner) : OwnedPeripheral(TYPE, owner) { + override val isEnabled = true + + init { + addPlugin(AE2WirelessTerminalPlugin(owner)) + } + + companion object { + const val TYPE = "ae2_wireless_terminal" + + fun create(turtle: ITurtleAccess, side: TurtleSide): AE2WirelessTerminalPeripheral { + val owner = TurtlePeripheralOwner(turtle, side).attachFuel() + return AE2WirelessTerminalPeripheral(owner) + } + } +} + +private class AE2WirelessTerminalPlugin(private val owner: TurtlePeripheralOwner) : IPeripheralPlugin { + private fun resolve(): AE2WirelessSession = resolveWirelessSession(owner, AEItems.WIRELESS_TERMINAL.asItem()) + + private fun validateTransfer(itemQuery: Any?, limit: Optional, slot: Optional): Pair, Pair> { + val predicate = PeripheralPluginUtils.itemQueryToPredicate(itemQuery) + val transferLimit = min(PeripheralWorksConfig.itemStorageTransferLimit, limit.orElse(Int.MAX_VALUE)) + if (transferLimit < 0) throw LuaException("Limit must be non-negative") + val inventorySize = owner.storage!!.size + val storageSlot = slot.map { it - 1 }.orElse(-1) + if (storageSlot !in -1 until inventorySize) throw LuaException("Slot must be between 1 and $inventorySize") + return predicate to (transferLimit to storageSlot) + } + + private fun validatePush(fromSlotOrItemQuery: Any?, limit: Optional): Pair, Pair> { + if (fromSlotOrItemQuery !is Number) return validateTransfer(fromSlotOrItemQuery, limit, Optional.empty()) + val fromSlot = fromSlotOrItemQuery.toInt() + if (fromSlotOrItemQuery.toDouble() != fromSlot.toDouble()) throw LuaException("Slot must be an integer") + return validateTransfer(null, limit, Optional.of(fromSlot)) + } + + private fun consumeFuel() { + val fuel = owner.getBoon(PeripheralOwnerBoonKey.FUEL)!! + if (!fuel.consumeFuel(1, false)) throw LuaException("Not enough fuel") + } + + @LuaFunction(mainThread = true) + fun getFuelMaxLevel(): Int = owner.getBoon(PeripheralOwnerBoonKey.FUEL)!!.maxFuelLevel + + @LuaFunction(mainThread = true) + fun items(arguments: IArguments): List> { + val storage = AEItemStorage(resolve().storage, IActionSource.empty()) {} + val mode = if (arguments.optBoolean(0, true)) RepresentationMode.DETAILED else RepresentationMode.BASE + val predicate = PeripheralPluginUtils.itemQueryToPredicate(arguments.get(1)) + return storage.getContent().asSequence().filter(predicate::test).map { LuaRepresentation.forItemStack(it, mode) }.toList() + } + + @LuaFunction(mainThread = true) + fun pullItem(itemQuery: Any?, limit: Optional, toSlot: Optional): Int { + val session = resolve() + val (predicate, transfer) = validateTransfer(itemQuery, limit, toSlot) + consumeFuel() + return owner.withPlayer({ player -> + AEItemStorage(session.storage, IActionSource.ofPlayer(player.fakePlayer)) {} + .moveTo(owner.storage!!, transfer.first, transfer.second, predicate) + }, skipInventory = true) + } + + @LuaFunction(mainThread = true) + fun pushItem(fromSlotOrItemQuery: Any?, limit: Optional): Int { + val session = resolve() + val (predicate, transfer) = validatePush(fromSlotOrItemQuery, limit) + consumeFuel() + return owner.withPlayer({ player -> + owner.storage!!.moveTo( + AEItemStorage(session.storage, IActionSource.ofPlayer(player.fakePlayer)) {}, + transfer.first, + transfer.second, + -1, + predicate, + ) + }, skipInventory = true) + } +} diff --git a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt index 5366982a..559cad3b 100644 --- a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt +++ b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt @@ -11,7 +11,13 @@ import site.siredvin.broccolium.modules.storage.base.api.SomethingOperator import site.siredvin.broccolium.modules.storage.item.ItemStorageUtils import java.util.function.Predicate -class AEItemStorage(private val storage: MEStorage, private val entity: AENetworkBlockEntity) : AgnosticStorage { +class AEItemStorage( + private val storage: MEStorage, + private val actionSource: IActionSource, + private val changeCallback: () -> Unit, +) : AgnosticStorage { + constructor(storage: MEStorage, entity: AENetworkBlockEntity) : this(storage, IActionSource.ofMachine(entity), entity::setChanged) + override fun getContent(): Iterator { return storage.availableStacks.mapNotNull { if (it.key !is AEItemKey) return@mapNotNull null @@ -20,7 +26,7 @@ class AEItemStorage(private val storage: MEStorage, private val entity: AENetwor } override fun setChanged() { - entity.setChanged() + changeCallback() } override val maxStackSize: Int @@ -29,7 +35,7 @@ class AEItemStorage(private val storage: MEStorage, private val entity: AENetwor get() = ItemStorageUtils override fun store(stack: ItemStack, simulate: Boolean): ItemStack { - val insertedAmount = storage.insert(AEItemKey.of(stack), stack.count.toLong(), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, IActionSource.ofMachine(entity)) + val insertedAmount = storage.insert(AEItemKey.of(stack), stack.count.toLong(), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, actionSource) if (insertedAmount == 0L) return stack stack.shrink(insertedAmount.toInt()) return stack @@ -43,7 +49,7 @@ class AEItemStorage(private val storage: MEStorage, private val entity: AENetwor } return@find predicate.test(aeKey.toStack(it.longValue.toInt())) } ?: return ItemStack.EMPTY - val extractedAmount = storage.extract(itemToTransfer.key, minOf(limit.toLong(), itemToTransfer.longValue), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, IActionSource.ofMachine(entity)) + val extractedAmount = storage.extract(itemToTransfer.key, minOf(limit.toLong(), itemToTransfer.longValue), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, actionSource) if (extractedAmount == 0L) return ItemStack.EMPTY return (itemToTransfer.key as AEItemKey).toStack(extractedAmount.toInt()) } diff --git a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt index fba7bb2f..a205e751 100644 --- a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt +++ b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt @@ -2,7 +2,10 @@ package site.siredvin.peripheralworks.integrations.ae2 import appeng.blockentity.grid.AENetworkBlockEntity import appeng.core.definitions.AEBlockEntities +import appeng.core.definitions.AEItems import dan200.computercraft.api.peripheral.PeripheralLookup +import dan200.computercraft.api.turtle.ITurtleUpgrade +import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser import net.minecraft.core.BlockPos import net.minecraft.core.Direction import net.minecraft.world.item.ItemStack @@ -14,7 +17,13 @@ import site.siredvin.broccolium.modules.storage.energy.api.AgnosticEnergyStorage import site.siredvin.broccolium.modules.storage.fluid.AgnosticFluidStorageLookup import site.siredvin.broccolium.modules.storage.fluid.api.AgnosticFluidStorage import site.siredvin.broccolium.modules.storage.item.AgnosticItemStorageLookup +import site.siredvin.peripheralworks.PeripheralWorksClientCore +import site.siredvin.peripheralworks.client.turtle.ScaledItemModeller import site.siredvin.peripheralworks.computercraft.ComputerCraftProxy +import site.siredvin.peripheralworks.data.ModEnLanguageProvider +import site.siredvin.peripheralworks.data.ModTurtleUpgradeDataProvider +import site.siredvin.peripheralworks.data.ModUaLanguageProvider +import site.siredvin.peripheralworks.xplat.ModPlatform class Integration : Runnable { @@ -42,6 +51,34 @@ class Integration : Runnable { } override fun run() { + val wirelessTerminalUpgrade = ModPlatform.registerTurtleUpgrade( + AE2WirelessTerminalUpgrade.UPGRADE_ID, + TurtleUpgradeSerialiser.simpleWithCustomItem { _, stack -> AE2WirelessTerminalUpgrade(stack) }, + ) + ModTurtleUpgradeDataProvider.hookUpgrade { + it.simpleWithCustomItem(AE2WirelessTerminalUpgrade.UPGRADE_ID, wirelessTerminalUpgrade.get(), AEItems.WIRELESS_TERMINAL.asItem()).requireMod("ae2") + } + PeripheralWorksClientCore.EXTRA_TURTLE_MODEL_PROVIDERS.add { + @Suppress("UNCHECKED_CAST") + Pair(wirelessTerminalUpgrade.get() as TurtleUpgradeSerialiser, ScaledItemModeller(0.75f, heightShift = 0.15f)) + } + ModEnLanguageProvider.addHook { it.addTurtle(AE2WirelessTerminalUpgrade.UPGRADE_ID, "AE terminal") } + ModUaLanguageProvider.addHook { it.addTurtle(AE2WirelessTerminalUpgrade.UPGRADE_ID, "AE термінальна") } + + val craftingMonitorUpgrade = ModPlatform.registerTurtleUpgrade( + AE2CraftingMonitorUpgrade.UPGRADE_ID, + TurtleUpgradeSerialiser.simpleWithCustomItem { _, stack -> AE2CraftingMonitorUpgrade(stack) }, + ) + ModTurtleUpgradeDataProvider.hookUpgrade { + it.simpleWithCustomItem(AE2CraftingMonitorUpgrade.UPGRADE_ID, craftingMonitorUpgrade.get(), AEItems.WIRELESS_CRAFTING_TERMINAL.asItem()).requireMod("ae2") + } + PeripheralWorksClientCore.EXTRA_TURTLE_MODEL_PROVIDERS.add { + @Suppress("UNCHECKED_CAST") + Pair(craftingMonitorUpgrade.get() as TurtleUpgradeSerialiser, ScaledItemModeller(0.75f, heightShift = 0.15f)) + } + ModEnLanguageProvider.addHook { it.addTurtle(AE2CraftingMonitorUpgrade.UPGRADE_ID, "AE crafting monitor") } + ModUaLanguageProvider.addHook { it.addTurtle(AE2CraftingMonitorUpgrade.UPGRADE_ID, "AE монітор крафтингу") } + if (Configuration.enableStorageIntegrations) { AgnosticItemStorageLookup.addBlockLookup(::extractItemStorage) AgnosticFluidStorageLookup.addBlockLookup(::extractFluidStorage) @@ -49,6 +86,7 @@ class Integration : Runnable { } if (Configuration.enableMEInterface) { ComputerCraftProxy.addProvider(MENetworkBlockPlugin.Provider) + ComputerCraftProxy.addProvider(AE2CraftingJobsPluginProvider) ComputerCraftProxy.addProvider(AE2CableObjectProvider) ComputerCraftProxy.addProvider(AE2InterfaceObjectProvider) ComputerCraftProxy.addProvider(AE2PatternProviderObjectProvider) diff --git a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt index eb12e95f..9163a582 100644 --- a/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt +++ b/projects/fabric/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt @@ -1,7 +1,5 @@ package site.siredvin.peripheralworks.integrations.ae2 -import appeng.api.networking.crafting.CalculationStrategy -import appeng.api.networking.security.IActionSource import appeng.api.stacks.AEFluidKey import appeng.api.stacks.AEItemKey import appeng.api.stacks.AEKey @@ -12,17 +10,13 @@ import net.minecraft.core.BlockPos import net.minecraft.core.Direction import net.minecraft.world.level.Level import site.siredvin.broccolium.modules.platform.PlatformRegistries -import site.siredvin.broccolium.modules.platform.PlatformToolkit import site.siredvin.peripheralworks.api.PeripheralPluginProvider import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.buildKey import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.genericStackToMap -import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.keyCounterToLua import site.siredvin.tweakium.modules.peripheral.api.IPeripheralPlugin import site.siredvin.tweakium.modules.peripheral.representation.LuaRepresentation -import java.util.* -import kotlin.NoSuchElementException -class MENetworkBlockPlugin(private val level: Level, private val entity: AENetworkBlockEntity) : IPeripheralPlugin { +class MENetworkBlockPlugin(private val entity: AENetworkBlockEntity) : IPeripheralPlugin { companion object { const val PLUGIN_TYPE = "ae2" } @@ -39,7 +33,7 @@ class MENetworkBlockPlugin(private val level: Level, private val entity: AENetwo if (entity !is AENetworkBlockEntity) { return null } - return MENetworkBlockPlugin(level, entity) + return MENetworkBlockPlugin(entity) } } @@ -167,42 +161,4 @@ class MENetworkBlockPlugin(private val level: Level, private val entity: AENetwo } return MethodResult.of(craftingList) } - - @LuaFunction(mainThread = false) - fun scheduleCrafting(mode: String, id_key: String, amount: Optional, targetCPU: Optional): MethodResult { - val craftingService = entity.mainNode.grid?.craftingService ?: return MethodResult.of(null, "AE2 network is not connected") - - val key = buildKey(mode, id_key) - val source = IActionSource.ofMachine(entity) - val realAmount = if (mode == "item") { - amount.orElse(1) - } else { - amount.orElse(1000) * PlatformToolkit.get().fluidCompactDivider - }.toLong() - val future = craftingService.beginCraftingCalculation( - level, - { source }, - key, - realAmount, - CalculationStrategy.REPORT_MISSING_ITEMS, - ) - val plan = future.get() - - if (!plan.missingItems().isEmpty) { - return MethodResult.of(false, "Missing items", keyCounterToLua(plan.missingItems())) - } - val realTargetCPU = if (targetCPU.isPresent) { - try { - craftingService.cpus.first { - it.name != null && it.name!!.string.equals(targetCPU.get()) - } - } catch (e: NoSuchElementException) { - return MethodResult.of(null, "Cannot find target CPU") - } - } else { - null - } - craftingService.submitJob(plan, null, realTargetCPU, false, source) - return MethodResult.of(true) - } } diff --git a/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/AE2WirelessTerminalGameTests.kt b/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/AE2WirelessTerminalGameTests.kt new file mode 100644 index 00000000..49bbcc0d --- /dev/null +++ b/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/AE2WirelessTerminalGameTests.kt @@ -0,0 +1,156 @@ +package site.siredvin.peripheralworks.testmod + +import appeng.api.config.Actionable +import appeng.api.networking.security.IActionSource +import appeng.api.stacks.AEItemKey +import appeng.blockentity.networking.WirelessAccessPointBlockEntity +import appeng.blockentity.storage.ChestBlockEntity +import appeng.core.definitions.AEBlocks +import appeng.core.definitions.AEItems +import appeng.items.tools.powered.WirelessTerminalItem +import dan200.computercraft.api.turtle.TurtleSide +import dan200.computercraft.api.upgrades.UpgradeData +import dan200.computercraft.shared.config.Config +import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity +import net.minecraft.core.BlockPos +import net.minecraft.core.GlobalPos +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestAssertException +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import site.siredvin.peripheralworks.integrations.ae2.AE2CraftingMonitorUpgrade +import site.siredvin.peripheralworks.integrations.ae2.AE2WirelessTerminalUpgrade +import site.siredvin.testiarium.api.TestGroup +import site.siredvin.testiarium.api.thenExecuteFailFast +import site.siredvin.testiarium.cct.CctComputerState + +@TestGroup("peripheralworks") +class AE2WirelessTerminalGameTests { + @GameTest(template = FIXTURE, batch = FIXTURE, timeoutTicks = 12000) + fun wirelessTerminal(helper: GameTestHelper) { + val turtle = findTurtle(helper) + val accessPointPos = turtle.blockPos.offset(0, 0, 1) + val energyPos = accessPointPos.offset(0, 0, 1) + val chestPos = energyPos.offset(1, 0, 0) + helper.level.setBlockAndUpdate(accessPointPos, AEBlocks.WIRELESS_ACCESS_POINT.block().defaultBlockState()) + helper.level.setBlockAndUpdate(energyPos, AEBlocks.CREATIVE_ENERGY_CELL.block().defaultBlockState()) + helper.level.setBlockAndUpdate(chestPos, AEBlocks.CHEST.block().defaultBlockState()) + (helper.level.getBlockEntity(chestPos) as ChestBlockEntity).setCell(AEItems.ITEM_CELL_1K.stack()) + + val terminalItem = AEItems.WIRELESS_TERMINAL.asItem() + val terminal = AEItems.WIRELESS_TERMINAL.stack().apply { + hoverName = Component.literal("Test terminal") + orCreateTag.putString("upw_test", "preserved") + } + WirelessTerminalItem.LINKABLE_HANDLER.link(terminal, GlobalPos.of(helper.level.dimension(), accessPointPos)) + terminalItem.injectAEPower(terminal, 400.0, Actionable.MODULATE) + terminalItem.getUpgrades(terminal).setItemDirect(0, AEItems.ENERGY_CARD.stack()) + val initialCharge = terminalItem.getAECurrentPower(terminal) + val upgrade = AE2WirelessTerminalUpgrade(AEItems.WIRELESS_TERMINAL.stack()) + check(ItemStack.matches(terminal, upgrade.getUpgradeItem(upgrade.getUpgradeData(terminal)))) + turtle.access.setUpgradeWithData(TurtleSide.LEFT, UpgradeData.of(upgrade, upgrade.getUpgradeData(terminal))) + val craftingTerminal = AEItems.WIRELESS_CRAFTING_TERMINAL.stack() + WirelessTerminalItem.LINKABLE_HANDLER.link(craftingTerminal, GlobalPos.of(helper.level.dimension(), accessPointPos)) + val craftingMonitor = AE2CraftingMonitorUpgrade(AEItems.WIRELESS_CRAFTING_TERMINAL.stack()) + check(ItemStack.matches(craftingTerminal, craftingMonitor.getUpgradeItem(craftingMonitor.getUpgradeData(craftingTerminal)))) + turtle.access.setUpgradeWithData(TurtleSide.RIGHT, UpgradeData.of(craftingMonitor, craftingMonitor.getUpgradeData(craftingTerminal))) + + helper.startSequence() + .thenIdle(10) + .thenExecuteFailFast { + val accessPoint = helper.level.getBlockEntity(accessPointPos) as WirelessAccessPointBlockEntity + check(accessPoint.isActive) { "Wireless access point did not become active" } + val inserted = accessPoint.grid!!.storageService.inventory.insert(AEItemKey.of(Items.STONE), 64, Actionable.MODULATE, IActionSource.empty()) + check(inserted == 64L) { "Failed to seed AE2 item storage" } + turtle.createServerComputer().turnOn() + } + .thenWaitUntil { await("initial") } + .thenExecuteFailFast { + state().check("initial") + check(turtle.access.fuelLevel == 7) { "Expected three fuel-consuming calls, got ${turtle.access.fuelLevel}" } + check(turtle.contents[0].count == 5 && turtle.contents[0].`is`(Items.STONE)) + check(turtle.contents[15].count == 5 && turtle.contents[15].`is`(Items.GOLD_INGOT)) + turtle.access.fuelLevel = 0 + } + .thenWaitUntil { await("empty-fuel") } + .thenExecuteFailFast { + state().check("empty-fuel") + Config.turtlesNeedFuel = false + turtle.access.fuelLevel = 1 + } + .thenWaitUntil { await("disabled") } + .thenExecuteFailFast { + state().check("disabled") + Config.turtlesNeedFuel = true + turtle.access.fuelLevel = 2 + } + .thenWaitUntil { await("restored") } + .thenExecuteFailFast { + state().check("restored") + check(turtle.access.teleportTo(helper.level, turtle.blockPos.offset(18, 0, 0))) + } + .thenWaitUntil { await("out-of-range") } + .thenExecuteFailFast { + state().check("out-of-range") + check(turtle.access.teleportTo(helper.level, accessPointPos.offset(0, 0, -1))) + } + .thenWaitUntil { await("returned") } + .thenExecuteFailFast { + state().check("returned") + helper.level.removeBlock(energyPos, false) + } + .thenWaitUntil { await("inactive") } + .thenExecuteFailFast { + state().check("inactive") + helper.level.setBlockAndUpdate(energyPos, AEBlocks.CREATIVE_ENERGY_CELL.block().defaultBlockState()) + } + .thenWaitUntil { await("reactivated") } + .thenExecuteFailFast { + state().check("reactivated") + val data = turtle.access.getUpgradeNBTData(TurtleSide.LEFT) + val stored = ItemStack.of(data.getCompound("terminal")) + WirelessTerminalItem.LINKABLE_HANDLER.link(stored, GlobalPos.of(helper.level.dimension(), UNLOADED_POS)) + data.put("terminal", stored.save(net.minecraft.nbt.CompoundTag())) + turtle.access.updateUpgradeNBTData(TurtleSide.LEFT) + check(helper.level.chunkSource.getChunkNow(UNLOADED_POS.x shr 4, UNLOADED_POS.z shr 4) == null) + } + .thenWaitUntil { await(CctComputerState.DONE) } + .thenExecuteFailFast { + state().check(CctComputerState.DONE) + check(helper.level.chunkSource.getChunkNow(UNLOADED_POS.x shr 4, UNLOADED_POS.z shr 4) == null) { "Wireless resolution loaded the linked chunk" } + val stored = turtle.access.getUpgradeWithData(TurtleSide.LEFT)!!.upgradeItem + check(stored.hoverName.string == "Test terminal") + check(stored.tag?.getString("upw_test") == "preserved") + check(terminalItem.getUpgrades(stored).getInstalledUpgrades(AEItems.ENERGY_CARD) == 1) + check(terminalItem.getAECurrentPower(stored) == initialCharge) { "Peripheral use changed terminal charge" } + } + .thenSucceed() + } + + private fun state() = CctComputerState.get(FIXTURE) ?: throw GameTestAssertException("Computer '$FIXTURE' has not started") + + private fun findTurtle(helper: GameTestHelper): TurtleBlockEntity { + for (x in 0 until 7) { + for (y in 0 until 4) { + for (z in 0 until 7) { + val entity = helper.getBlockEntity(BlockPos(x, y, z)) + if (entity is TurtleBlockEntity) return entity + } + } + } + throw GameTestAssertException("Fixture turtle is missing") + } + + private fun await(marker: String) { + val state = state() + if (state.isDone(CctComputerState.DONE)) state.check(CctComputerState.DONE) + if (!state.isDone(marker)) throw GameTestAssertException("Computer '$FIXTURE' has not reached $marker") + } + + companion object { + private const val FIXTURE = "peripheralworksgametests.ae2_wireless_terminal" + private val UNLOADED_POS = BlockPos(1_000_000, 64, 1_000_000) + } +} diff --git a/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/FabricPeripheralWorksTestMod.kt b/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/FabricPeripheralWorksTestMod.kt index 606e6f0e..915b4694 100644 --- a/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/FabricPeripheralWorksTestMod.kt +++ b/projects/fabric/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/FabricPeripheralWorksTestMod.kt @@ -18,6 +18,7 @@ object FabricPeripheralWorksTestMod : ModInitializer { } Testiarium.register(PeripheralWorksGameTests::class.java) Testiarium.register(AE2ConfigurableObjectsGameTests::class.java) + Testiarium.register(AE2WirelessTerminalGameTests::class.java) if (FabricLoader.getInstance().environmentType == EnvType.CLIENT) { Testiarium.register(Class.forName("site.siredvin.peripheralworks.testmod.NetworkManagerClientGameTests")) } diff --git a/projects/forge/build.gradle.kts b/projects/forge/build.gradle.kts index fe507fc9..7330d9df 100644 --- a/projects/forge/build.gradle.kts +++ b/projects/forge/build.gradle.kts @@ -35,8 +35,11 @@ forgeShaking { } if (minimalTestEnvironment) { - sourceSets.main { kotlin.exclude("site/siredvin/peripheralworks/integrations/**") } - tasks.named("compileKotlin") { exclude("**/integrations/**") } + val excludedIntegrations = file("src/main/kotlin/site/siredvin/peripheralworks/integrations").listFiles()!! + .filter { it.isDirectory && it.name != "ae2" } + .map { "**/integrations/${it.name}/**" } + sourceSets.main { kotlin.exclude(excludedIntegrations) } + tasks.named("compileKotlin") { exclude(excludedIntegrations) } } val testMod = sourceSets.create("testMod") { @@ -74,7 +77,9 @@ dependencies { // runtimeOnly(fg.deobf("com.simibubi.create:create-1.20.1:6.0.0-84:all")) compileOnly(fg.deobf("net.createmod.ponder:Ponder-Forge-1.20.1:1.0.51")) - if (!minimalTestEnvironment) { + if (minimalTestEnvironment) { + implementation(fg.deobf(libs.ae2.forge.get())) + } else { libs.bundles.externalMods.forge.integrations.full.get().map { compileOnly(fg.deobf(it)) } libs.bundles.externalMods.forge.integrations.raw.full.get().map { compileOnly(it) } libs.bundles.externalMods.forge.integrations.active.get().map { runtimeOnly(fg.deobf(it)) } diff --git a/projects/forge/src/generated/resources/.cache/332c3938fc847411e7f47874e3456ed3599bef6a b/projects/forge/src/generated/resources/.cache/332c3938fc847411e7f47874e3456ed3599bef6a index 7b8cde38..b1757680 100644 --- a/projects/forge/src/generated/resources/.cache/332c3938fc847411e7f47874e3456ed3599bef6a +++ b/projects/forge/src/generated/resources/.cache/332c3938fc847411e7f47874e3456ed3599bef6a @@ -1,2 +1,2 @@ -// 1.20.1 2026-07-25T21:13:37.742801759 vanilla/ComputerLanguageen_us -3f6387e39a99555b8f87230e64cceff8e6c70935 assets/peripheralworks/lang/en_us.json +// 1.20.1 2026-08-04T16:34:49.330806371 vanilla/ComputerLanguageen_us +7a102403804ca9dec57c9e80f1bd1e44d5b1db50 assets/peripheralworks/lang/en_us.json diff --git a/projects/forge/src/generated/resources/.cache/42bea422736702abc417b05c462ae52120b042de b/projects/forge/src/generated/resources/.cache/42bea422736702abc417b05c462ae52120b042de index a537db37..22db064b 100644 --- a/projects/forge/src/generated/resources/.cache/42bea422736702abc417b05c462ae52120b042de +++ b/projects/forge/src/generated/resources/.cache/42bea422736702abc417b05c462ae52120b042de @@ -1,4 +1,6 @@ -// 1.20.1 2025-11-08T18:25:34.6607103 vanilla/Turtle Upgrades +// 1.20.1 2026-08-04T16:34:49.329673094 vanilla/Turtle Upgrades +d240fa0f18606d9d4e6781d8797dd80ffc38e237 data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json +3853625a634297d38a4ed83cc9b58f38bce9211c data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json 911a593d4aafc165c692b127978a82c7f08b960e data/peripheralworks/computercraft/turtle_upgrades/hologram_projector.json d205e7849ba7d32f6e008c44a81fb296064072dd data/peripheralworks/computercraft/turtle_upgrades/natures_compass.json 2246ea1d5805e9afda36c4f1265f520598315dc9 data/peripheralworks/computercraft/turtle_upgrades/netherite_peripheralium_hub.json diff --git a/projects/forge/src/generated/resources/.cache/442eea4c4e28c24d3ee14e17147972c3af1159eb b/projects/forge/src/generated/resources/.cache/442eea4c4e28c24d3ee14e17147972c3af1159eb index db87304b..00d1c4e2 100644 --- a/projects/forge/src/generated/resources/.cache/442eea4c4e28c24d3ee14e17147972c3af1159eb +++ b/projects/forge/src/generated/resources/.cache/442eea4c4e28c24d3ee14e17147972c3af1159eb @@ -1,2 +1,2 @@ -// 1.20.1 2026-07-25T21:13:37.743030755 vanilla/ComputerLanguageuk_ua -a11c85b7543def12c6a94487874731a025496b0d assets/peripheralworks/lang/uk_ua.json +// 1.20.1 2026-08-04T16:34:49.331013825 vanilla/ComputerLanguageuk_ua +c0023a66893302d84e0e97d0383f9a9701a75f6a assets/peripheralworks/lang/uk_ua.json diff --git a/projects/forge/src/generated/resources/assets/peripheralworks/lang/en_us.json b/projects/forge/src/generated/resources/assets/peripheralworks/lang/en_us.json index f9cde705..5f7d8760 100644 --- a/projects/forge/src/generated/resources/assets/peripheralworks/lang/en_us.json +++ b/projects/forge/src/generated/resources/assets/peripheralworks/lang/en_us.json @@ -165,6 +165,8 @@ "tooltip.peripheralworks.remote_observer_range": " §6Max range of observed block: %s", "tooltip.peripheralworks.universal_scanner_free_range": " §6Cost-free scan range: %s", "tooltip.peripheralworks.universal_scanner_max_range": " §6Max scan range: %s", + "turtle.peripheralworks.ae2_crafting_monitor": "AE crafting monitor", + "turtle.peripheralworks.ae2_wireless_terminal": "AE terminal", "turtle.peripheralworks.hologram_projector": "Projecting", "turtle.peripheralworks.natures_compass": "Nature Compassing", "turtle.peripheralworks.netherite_peripheralium_hub": "Netherite Hub", diff --git a/projects/forge/src/generated/resources/assets/peripheralworks/lang/uk_ua.json b/projects/forge/src/generated/resources/assets/peripheralworks/lang/uk_ua.json index 55210b4a..ee788785 100644 --- a/projects/forge/src/generated/resources/assets/peripheralworks/lang/uk_ua.json +++ b/projects/forge/src/generated/resources/assets/peripheralworks/lang/uk_ua.json @@ -165,6 +165,8 @@ "tooltip.peripheralworks.remote_observer_range": " §6Максимальна дальність стостерігання: %s", "tooltip.peripheralworks.universal_scanner_free_range": " §6Бескоштовний радіус сканування: %s", "tooltip.peripheralworks.universal_scanner_max_range": " §6Максимальний радіус сканування: %s", + "turtle.peripheralworks.ae2_crafting_monitor": "AE монітор крафтингу", + "turtle.peripheralworks.ae2_wireless_terminal": "AE термінальна", "turtle.peripheralworks.hologram_projector": "Проекуюча", "turtle.peripheralworks.natures_compass": "Природновідчуваюча", "turtle.peripheralworks.netherite_peripheralium_hub": "З вбудованим незеритовим осередком", diff --git a/projects/forge/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json b/projects/forge/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json new file mode 100644 index 00000000..dfd7f4d7 --- /dev/null +++ b/projects/forge/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_crafting_monitor.json @@ -0,0 +1,10 @@ +{ + "type": "peripheralworks:ae2_crafting_monitor", + "forge:conditions": [ + { + "type": "forge:mod_loaded", + "modid": "ae2" + } + ], + "item": "ae2:wireless_crafting_terminal" +} \ No newline at end of file diff --git a/projects/forge/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json b/projects/forge/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json new file mode 100644 index 00000000..348f7cca --- /dev/null +++ b/projects/forge/src/generated/resources/data/peripheralworks/computercraft/turtle_upgrades/ae2_wireless_terminal.json @@ -0,0 +1,10 @@ +{ + "type": "peripheralworks:ae2_wireless_terminal", + "forge:conditions": [ + { + "type": "forge:mod_loaded", + "modid": "ae2" + } + ], + "item": "ae2:wireless_terminal" +} \ No newline at end of file diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/ForgePeripheralWorks.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/ForgePeripheralWorks.kt index 3679eb90..2aee35ef 100644 --- a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/ForgePeripheralWorks.kt +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/ForgePeripheralWorks.kt @@ -93,7 +93,6 @@ object ForgePeripheralWorks { loader.maybeLoadIntegration("occultism").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("easy_villagers").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("toms_storage").ifPresent { (it as Runnable).run() } - loader.maybeLoadIntegration("ae2").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("mna").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("deepresonance").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("powah").ifPresent { (it as Runnable).run() } @@ -126,6 +125,7 @@ object ForgePeripheralWorks { loader.maybeLoadIntegration("naturescompass").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("ars_nouveau").ifPresent { (it as Runnable).run() } loader.maybeLoadIntegration("projecte").ifPresent { (it as Runnable).run() } + loader.maybeLoadIntegration("ae2").ifPresent { (it as Runnable).run() } } @Suppress("MemberVisibilityCanBePrivate") diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingJobs.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingJobs.kt new file mode 100644 index 00000000..80c62987 --- /dev/null +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingJobs.kt @@ -0,0 +1,159 @@ +package site.siredvin.peripheralworks.integrations.ae2 + +import appeng.api.networking.crafting.CalculationStrategy +import appeng.api.networking.crafting.ICraftingLink +import appeng.api.networking.crafting.ICraftingService +import appeng.api.networking.security.IActionSource +import appeng.blockentity.grid.AENetworkBlockEntity +import appeng.core.definitions.AEItems +import dan200.computercraft.api.lua.LuaFunction +import dan200.computercraft.api.lua.MethodResult +import net.minecraft.core.BlockPos +import net.minecraft.core.Direction +import net.minecraft.world.level.Level +import site.siredvin.broccolium.modules.platform.PlatformToolkit +import site.siredvin.peripheralworks.api.PeripheralPluginProvider +import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.buildKey +import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.keyCounterToLua +import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.stackToMap +import site.siredvin.tweakium.modules.peripheral.api.IPeripheralPlugin +import site.siredvin.tweakium.modules.peripheral.owner.TurtlePeripheralOwner +import java.lang.ref.WeakReference +import java.util.Collections +import java.util.Locale +import java.util.Optional +import java.util.WeakHashMap + +object AE2CraftingJobs { + private data class Job(val service: WeakReference, val target: Map, val amount: Long) + + // ponytail: job counts are tiny; scan weak keys until UUID lookup is proven necessary. + private val jobs = Collections.synchronizedMap(WeakHashMap()) + + fun schedule( + level: Level, + service: ICraftingService, + source: IActionSource, + mode: String, + id: String, + amount: Optional, + targetCPU: Optional, + ): MethodResult { + val publicAmount = amount.orElse(if (mode == "item") 1 else 1000) + if (publicAmount <= 0) return MethodResult.of(null, "Amount must be positive") + val key = buildKey(mode, id) + val realAmount = try { + if (mode == "fluid") Math.multiplyExact(publicAmount, PlatformToolkit.get().fluidCompactDivider.toLong()) else publicAmount + } catch (_: ArithmeticException) { + return MethodResult.of(null, "Amount is too large") + } + val plan = service.beginCraftingCalculation(level, { source }, key, realAmount, CalculationStrategy.REPORT_MISSING_ITEMS).get() + if (!plan.missingItems().isEmpty) return MethodResult.of(false, "Missing items", keyCounterToLua(plan.missingItems())) + val cpu = if (targetCPU.isPresent) { + service.cpus.firstOrNull { it.name?.string == targetCPU.get() } + ?: return MethodResult.of(null, "Cannot find target CPU") + } else { + null + } + val submitted = service.submitJob(plan, null, cpu, false, source) + if (!submitted.successful()) { + return MethodResult.of(null, "Cannot submit crafting job: ${submitted.errorCode()?.name?.lowercase(Locale.ROOT) ?: "unknown error"}") + } + val link = submitted.link() ?: return MethodResult.of(null, "AE2 did not return a crafting job") + val jobID = link.craftingID.toString() + jobs[link] = Job(WeakReference(service), stackToMap(plan.finalOutput()), publicAmount) + return MethodResult.of(true, jobID) + } + + fun get(service: ICraftingService, jobID: String): MethodResult { + val job = find(service, jobID) ?: return missing(jobID) + return MethodResult.of(toMap(job.first, job.second)) + } + + fun getAll(service: ICraftingService): List> = synchronized(jobs) { + jobs.mapNotNull { (link, job) -> if (job.service.get() === service) toMap(link, job) else null } + } + + fun cancel(service: ICraftingService, jobID: String): MethodResult { + val (link) = find(service, jobID) ?: return missing(jobID) + if (link.isCanceled) return MethodResult.of(false, "Crafting job '$jobID' is already canceled") + if (link.isDone) return MethodResult.of(false, "Crafting job '$jobID' is already done") + link.cancel() + return MethodResult.of(true) + } + + private fun find(service: ICraftingService, jobID: String): Pair? = synchronized(jobs) { + jobs.entries.firstOrNull { (link, job) -> link.craftingID.toString() == jobID && job.service.get() === service } + ?.let { it.key to it.value } + } + + private fun toMap(link: ICraftingLink, job: Job): Map = mapOf( + "id" to link.craftingID.toString(), + "state" to when { + link.isCanceled -> "canceled" + link.isDone -> "done" + else -> "running" + }, + "target" to job.target, + "amount" to job.amount, + ) + + private fun missing(jobID: String): MethodResult = MethodResult.of(null, "Crafting job '$jobID' was not found") +} + +class AE2CraftingJobsPlugin private constructor( + private val resolve: () -> Context?, + private val withActionSource: (((IActionSource) -> MethodResult) -> MethodResult), + private val unavailableMessage: String, +) : IPeripheralPlugin { + private data class Context(val level: Level, val service: ICraftingService) + + @LuaFunction(mainThread = false) + fun scheduleCrafting(mode: String, id: String, amount: Optional, targetCPU: Optional): MethodResult { + val context = resolve() ?: return unavailable() + return withActionSource { source -> AE2CraftingJobs.schedule(context.level, context.service, source, mode, id, amount, targetCPU) } + } + + @LuaFunction(mainThread = true) + fun getCraftingJob(jobID: String): MethodResult { + val context = resolve() ?: return unavailable() + return AE2CraftingJobs.get(context.service, jobID) + } + + @LuaFunction(mainThread = true) + fun getCraftingJobs(): List> = resolve()?.let { AE2CraftingJobs.getAll(it.service) } ?: emptyList() + + @LuaFunction(mainThread = true) + fun cancelCrafting(jobID: String): MethodResult { + val context = resolve() ?: return unavailable() + return AE2CraftingJobs.cancel(context.service, jobID) + } + + private fun unavailable(): MethodResult = MethodResult.of(null, unavailableMessage) + + companion object { + fun forMachine(level: Level, entity: AENetworkBlockEntity) = AE2CraftingJobsPlugin( + resolve = { entity.mainNode.grid?.craftingService?.let { Context(level, it) } }, + withActionSource = { callback -> callback(IActionSource.ofMachine(entity)) }, + unavailableMessage = "AE2 network is not connected", + ) + + fun forTurtle(owner: TurtlePeripheralOwner) = AE2CraftingJobsPlugin( + resolve = { + owner.level?.let { level -> Context(level, resolveWirelessSession(owner, AEItems.WIRELESS_CRAFTING_TERMINAL.asItem()).craftingService) } + }, + withActionSource = { callback -> owner.withPlayer({ callback(IActionSource.ofPlayer(it.fakePlayer)) }, skipInventory = true) }, + unavailableMessage = "Linked AE2 network is unavailable", + ) + } +} + +object AE2CraftingJobsPluginProvider : PeripheralPluginProvider { + override val pluginType = "ae2_crafting_jobs" + + override fun provide(level: Level, pos: BlockPos, side: Direction): IPeripheralPlugin? { + if (!Configuration.enableMEInterface) return null + val entity = level.getBlockEntity(pos) as? AENetworkBlockEntity ?: return null + return AE2CraftingJobsPlugin.forMachine(level, entity) + } +} diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingMonitor.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingMonitor.kt new file mode 100644 index 00000000..e9e944ea --- /dev/null +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2CraftingMonitor.kt @@ -0,0 +1,48 @@ +package site.siredvin.peripheralworks.integrations.ae2 + +import appeng.core.definitions.AEItems +import dan200.computercraft.api.turtle.ITurtleAccess +import dan200.computercraft.api.turtle.TurtleSide +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import site.siredvin.peripheralworks.PeripheralWorksCore +import site.siredvin.tweakium.modules.peripheral.OwnedPeripheral +import site.siredvin.tweakium.modules.peripheral.owner.TurtlePeripheralOwner +import site.siredvin.tweakium.modules.turtle.PeripheralTurtleUpgrade + +class AE2CraftingMonitorUpgrade(stack: ItemStack) : PeripheralTurtleUpgrade(UPGRADE_ID, stack) { + override fun buildPeripheral(turtle: ITurtleAccess, side: TurtleSide): AE2CraftingMonitorPeripheral = AE2CraftingMonitorPeripheral.create(turtle, side) + + override fun getUpgradeData(stack: ItemStack): CompoundTag = CompoundTag().apply { + put(AE2_TERMINAL_TAG, stack.save(CompoundTag())) + } + + override fun getUpgradeItem(upgradeData: CompoundTag): ItemStack = if (upgradeData.contains(AE2_TERMINAL_TAG, Tag.TAG_COMPOUND.toInt())) { + ItemStack.of(upgradeData.getCompound(AE2_TERMINAL_TAG)) + } else { + craftingItem + } + + override fun isItemSuitable(stack: ItemStack): Boolean = AEItems.WIRELESS_CRAFTING_TERMINAL.isSameAs(stack) && + AEItems.WIRELESS_CRAFTING_TERMINAL.asItem().getLinkedPosition(stack) != null + + companion object { + val UPGRADE_ID = ResourceLocation.fromNamespaceAndPath(PeripheralWorksCore.MOD_ID, AE2CraftingMonitorPeripheral.TYPE) + } +} + +class AE2CraftingMonitorPeripheral private constructor(owner: TurtlePeripheralOwner) : OwnedPeripheral(TYPE, owner) { + override val isEnabled = true + + init { + addPlugin(AE2CraftingJobsPlugin.forTurtle(owner)) + } + + companion object { + const val TYPE = "ae2_crafting_monitor" + + fun create(turtle: ITurtleAccess, side: TurtleSide): AE2CraftingMonitorPeripheral = AE2CraftingMonitorPeripheral(TurtlePeripheralOwner(turtle, side)) + } +} diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2WirelessTerminal.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2WirelessTerminal.kt new file mode 100644 index 00000000..4b1d9b59 --- /dev/null +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AE2WirelessTerminal.kt @@ -0,0 +1,153 @@ +package site.siredvin.peripheralworks.integrations.ae2 + +import appeng.api.implementations.blockentities.IWirelessAccessPoint +import appeng.api.networking.crafting.ICraftingService +import appeng.api.networking.security.IActionSource +import appeng.api.storage.MEStorage +import appeng.blockentity.networking.WirelessAccessPointBlockEntity +import appeng.core.definitions.AEItems +import appeng.items.tools.powered.WirelessTerminalItem +import dan200.computercraft.api.lua.IArguments +import dan200.computercraft.api.lua.LuaException +import dan200.computercraft.api.lua.LuaFunction +import dan200.computercraft.api.turtle.ITurtleAccess +import dan200.computercraft.api.turtle.TurtleSide +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import site.siredvin.peripheralworks.PeripheralWorksCore +import site.siredvin.peripheralworks.common.configuration.PeripheralWorksConfig +import site.siredvin.tweakium.modules.peripheral.OwnedPeripheral +import site.siredvin.tweakium.modules.peripheral.api.IPeripheralPlugin +import site.siredvin.tweakium.modules.peripheral.boon.PeripheralOwnerBoonKey +import site.siredvin.tweakium.modules.peripheral.owner.TurtlePeripheralOwner +import site.siredvin.tweakium.modules.peripheral.representation.LuaRepresentation +import site.siredvin.tweakium.modules.peripheral.representation.RepresentationMode +import site.siredvin.tweakium.modules.plugins.PeripheralPluginUtils +import site.siredvin.tweakium.modules.turtle.PeripheralTurtleUpgrade +import java.util.Optional +import java.util.function.Predicate +import kotlin.math.min + +internal const val AE2_TERMINAL_TAG = "terminal" + +internal data class AE2WirelessSession(val storage: MEStorage, val craftingService: ICraftingService) + +internal fun resolveWirelessSession(owner: TurtlePeripheralOwner, terminal: WirelessTerminalItem): AE2WirelessSession { + val data = owner.turtle.getUpgradeNBTData(owner.side) + if (!data.contains(AE2_TERMINAL_TAG, Tag.TAG_COMPOUND.toInt())) throw LuaException("Invalid stored wireless terminal") + val stack = ItemStack.of(data.getCompound(AE2_TERMINAL_TAG)) + if (stack.item !== terminal || terminal.getLinkedPosition(stack) == null) throw LuaException("Invalid stored wireless terminal") + val level = owner.level ?: throw LuaException("Linked AE2 network is unavailable") + val grid = terminal.getLinkedGrid(stack, level, null) ?: throw LuaException("Linked AE2 network is unavailable") + val inRange = grid.getMachines(WirelessAccessPointBlockEntity::class.java).any { accessPoint -> + isInWirelessRange(accessPoint, level, owner.pos) + } + if (!inRange) throw LuaException("Turtle is outside wireless range") + return AE2WirelessSession(grid.storageService.inventory, grid.craftingService) +} + +private fun isInWirelessRange(accessPoint: IWirelessAccessPoint, level: net.minecraft.world.level.Level, pos: net.minecraft.core.BlockPos): Boolean = accessPoint.isActive && accessPoint.location.level === level && accessPoint.location.pos.distSqr(pos) < accessPoint.range * accessPoint.range + +class AE2WirelessTerminalUpgrade(stack: ItemStack) : PeripheralTurtleUpgrade(UPGRADE_ID, stack) { + override fun buildPeripheral(turtle: ITurtleAccess, side: TurtleSide): AE2WirelessTerminalPeripheral = AE2WirelessTerminalPeripheral.create(turtle, side) + + override fun getUpgradeData(stack: ItemStack): CompoundTag = CompoundTag().apply { + put(AE2_TERMINAL_TAG, stack.save(CompoundTag())) + } + + override fun getUpgradeItem(upgradeData: CompoundTag): ItemStack = if (upgradeData.contains(AE2_TERMINAL_TAG, Tag.TAG_COMPOUND.toInt())) { + ItemStack.of(upgradeData.getCompound(AE2_TERMINAL_TAG)) + } else { + craftingItem + } + + override fun isItemSuitable(stack: ItemStack): Boolean = AEItems.WIRELESS_TERMINAL.isSameAs(stack) && + AEItems.WIRELESS_TERMINAL.asItem().getLinkedPosition(stack) != null + + companion object { + val UPGRADE_ID = ResourceLocation.fromNamespaceAndPath(PeripheralWorksCore.MOD_ID, AE2WirelessTerminalPeripheral.TYPE) + } +} + +class AE2WirelessTerminalPeripheral private constructor(owner: TurtlePeripheralOwner) : OwnedPeripheral(TYPE, owner) { + override val isEnabled = true + + init { + addPlugin(AE2WirelessTerminalPlugin(owner)) + } + + companion object { + const val TYPE = "ae2_wireless_terminal" + + fun create(turtle: ITurtleAccess, side: TurtleSide): AE2WirelessTerminalPeripheral { + val owner = TurtlePeripheralOwner(turtle, side).attachFuel() + return AE2WirelessTerminalPeripheral(owner) + } + } +} + +private class AE2WirelessTerminalPlugin(private val owner: TurtlePeripheralOwner) : IPeripheralPlugin { + private fun resolve(): AE2WirelessSession = resolveWirelessSession(owner, AEItems.WIRELESS_TERMINAL.asItem()) + + private fun validateTransfer(itemQuery: Any?, limit: Optional, slot: Optional): Pair, Pair> { + val predicate = PeripheralPluginUtils.itemQueryToPredicate(itemQuery) + val transferLimit = min(PeripheralWorksConfig.itemStorageTransferLimit, limit.orElse(Int.MAX_VALUE)) + if (transferLimit < 0) throw LuaException("Limit must be non-negative") + val inventorySize = owner.storage!!.size + val storageSlot = slot.map { it - 1 }.orElse(-1) + if (storageSlot !in -1 until inventorySize) throw LuaException("Slot must be between 1 and $inventorySize") + return predicate to (transferLimit to storageSlot) + } + + private fun validatePush(fromSlotOrItemQuery: Any?, limit: Optional): Pair, Pair> { + if (fromSlotOrItemQuery !is Number) return validateTransfer(fromSlotOrItemQuery, limit, Optional.empty()) + val fromSlot = fromSlotOrItemQuery.toInt() + if (fromSlotOrItemQuery.toDouble() != fromSlot.toDouble()) throw LuaException("Slot must be an integer") + return validateTransfer(null, limit, Optional.of(fromSlot)) + } + + private fun consumeFuel() { + val fuel = owner.getBoon(PeripheralOwnerBoonKey.FUEL)!! + if (!fuel.consumeFuel(1, false)) throw LuaException("Not enough fuel") + } + + @LuaFunction(mainThread = true) + fun getFuelMaxLevel(): Int = owner.getBoon(PeripheralOwnerBoonKey.FUEL)!!.maxFuelLevel + + @LuaFunction(mainThread = true) + fun items(arguments: IArguments): List> { + val storage = AEItemStorage(resolve().storage, IActionSource.empty()) {} + val mode = if (arguments.optBoolean(0, true)) RepresentationMode.DETAILED else RepresentationMode.BASE + val predicate = PeripheralPluginUtils.itemQueryToPredicate(arguments.get(1)) + return storage.getContent().asSequence().filter(predicate::test).map { LuaRepresentation.forItemStack(it, mode) }.toList() + } + + @LuaFunction(mainThread = true) + fun pullItem(itemQuery: Any?, limit: Optional, toSlot: Optional): Int { + val session = resolve() + val (predicate, transfer) = validateTransfer(itemQuery, limit, toSlot) + consumeFuel() + return owner.withPlayer({ player -> + AEItemStorage(session.storage, IActionSource.ofPlayer(player.fakePlayer)) {} + .moveTo(owner.storage!!, transfer.first, transfer.second, predicate) + }, skipInventory = true) + } + + @LuaFunction(mainThread = true) + fun pushItem(fromSlotOrItemQuery: Any?, limit: Optional): Int { + val session = resolve() + val (predicate, transfer) = validatePush(fromSlotOrItemQuery, limit) + consumeFuel() + return owner.withPlayer({ player -> + owner.storage!!.moveTo( + AEItemStorage(session.storage, IActionSource.ofPlayer(player.fakePlayer)) {}, + transfer.first, + transfer.second, + -1, + predicate, + ) + }, skipInventory = true) + } +} diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt index 6cd14154..514a8aa3 100644 --- a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/AEItemStorage.kt @@ -11,7 +11,13 @@ import site.siredvin.broccolium.modules.storage.base.api.SomethingOperator import site.siredvin.broccolium.modules.storage.item.ItemStorageUtils import java.util.function.Predicate -class AEItemStorage(private val storage: MEStorage, private val entity: AENetworkBlockEntity) : AgnosticStorage { +class AEItemStorage( + private val storage: MEStorage, + private val actionSource: IActionSource, + private val changeCallback: () -> Unit, +) : AgnosticStorage { + constructor(storage: MEStorage, entity: AENetworkBlockEntity) : this(storage, IActionSource.ofMachine(entity), entity::setChanged) + override fun getContent(): Iterator { return storage.availableStacks.mapNotNull { if (it.key !is AEItemKey) return@mapNotNull null @@ -25,11 +31,11 @@ class AEItemStorage(private val storage: MEStorage, private val entity: AENetwor get() = ItemStorageUtils override fun setChanged() { - entity.setChanged() + changeCallback() } override fun store(stack: ItemStack, simulate: Boolean): ItemStack { - val insertedAmount = storage.insert(AEItemKey.of(stack), stack.count.toLong(), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, IActionSource.ofMachine(entity)) + val insertedAmount = storage.insert(AEItemKey.of(stack), stack.count.toLong(), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, actionSource) if (insertedAmount == 0L) return stack stack.shrink(insertedAmount.toInt()) return stack @@ -43,7 +49,7 @@ class AEItemStorage(private val storage: MEStorage, private val entity: AENetwor } return@find predicate.test(aeKey.toStack(it.longValue.toInt())) } ?: return ItemStack.EMPTY - val extractedAmount = storage.extract(itemToTransfer.key, minOf(limit.toLong(), itemToTransfer.longValue), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, IActionSource.ofMachine(entity)) + val extractedAmount = storage.extract(itemToTransfer.key, minOf(limit.toLong(), itemToTransfer.longValue), if (simulate) Actionable.SIMULATE else Actionable.MODULATE, actionSource) if (extractedAmount == 0L) return ItemStack.EMPTY return (itemToTransfer.key as AEItemKey).toStack(extractedAmount.toInt()) } diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt index 6e3cd8bc..67b28205 100644 --- a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/Integration.kt @@ -1,6 +1,9 @@ package site.siredvin.peripheralworks.integrations.ae2 import appeng.blockentity.grid.AENetworkBlockEntity +import appeng.core.definitions.AEItems +import dan200.computercraft.api.turtle.ITurtleUpgrade +import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser import net.minecraft.core.BlockPos import net.minecraft.core.Direction import net.minecraft.world.item.ItemStack @@ -12,7 +15,13 @@ import site.siredvin.broccolium.modules.storage.energy.api.AgnosticEnergyStorage import site.siredvin.broccolium.modules.storage.fluid.AgnosticFluidStorageLookup import site.siredvin.broccolium.modules.storage.fluid.api.AgnosticFluidStorage import site.siredvin.broccolium.modules.storage.item.AgnosticItemStorageLookup +import site.siredvin.peripheralworks.PeripheralWorksClientCore +import site.siredvin.peripheralworks.client.turtle.ScaledItemModeller import site.siredvin.peripheralworks.computercraft.ComputerCraftProxy +import site.siredvin.peripheralworks.data.ModEnLanguageProvider +import site.siredvin.peripheralworks.data.ModTurtleUpgradeDataProvider +import site.siredvin.peripheralworks.data.ModUaLanguageProvider +import site.siredvin.peripheralworks.xplat.ModPlatform class Integration : Runnable { @@ -40,6 +49,34 @@ class Integration : Runnable { } override fun run() { + val wirelessTerminalUpgrade = ModPlatform.registerTurtleUpgrade( + AE2WirelessTerminalUpgrade.UPGRADE_ID, + TurtleUpgradeSerialiser.simpleWithCustomItem { _, stack -> AE2WirelessTerminalUpgrade(stack) }, + ) + ModTurtleUpgradeDataProvider.hookUpgrade { + it.simpleWithCustomItem(AE2WirelessTerminalUpgrade.UPGRADE_ID, wirelessTerminalUpgrade.get(), AEItems.WIRELESS_TERMINAL.asItem()).requireMod("ae2") + } + PeripheralWorksClientCore.EXTRA_TURTLE_MODEL_PROVIDERS.add { + @Suppress("UNCHECKED_CAST") + Pair(wirelessTerminalUpgrade.get() as TurtleUpgradeSerialiser, ScaledItemModeller(0.75f, heightShift = 0.15f)) + } + ModEnLanguageProvider.addHook { it.addTurtle(AE2WirelessTerminalUpgrade.UPGRADE_ID, "AE terminal") } + ModUaLanguageProvider.addHook { it.addTurtle(AE2WirelessTerminalUpgrade.UPGRADE_ID, "AE термінальна") } + + val craftingMonitorUpgrade = ModPlatform.registerTurtleUpgrade( + AE2CraftingMonitorUpgrade.UPGRADE_ID, + TurtleUpgradeSerialiser.simpleWithCustomItem { _, stack -> AE2CraftingMonitorUpgrade(stack) }, + ) + ModTurtleUpgradeDataProvider.hookUpgrade { + it.simpleWithCustomItem(AE2CraftingMonitorUpgrade.UPGRADE_ID, craftingMonitorUpgrade.get(), AEItems.WIRELESS_CRAFTING_TERMINAL.asItem()).requireMod("ae2") + } + PeripheralWorksClientCore.EXTRA_TURTLE_MODEL_PROVIDERS.add { + @Suppress("UNCHECKED_CAST") + Pair(craftingMonitorUpgrade.get() as TurtleUpgradeSerialiser, ScaledItemModeller(0.75f, heightShift = 0.15f)) + } + ModEnLanguageProvider.addHook { it.addTurtle(AE2CraftingMonitorUpgrade.UPGRADE_ID, "AE crafting monitor") } + ModUaLanguageProvider.addHook { it.addTurtle(AE2CraftingMonitorUpgrade.UPGRADE_ID, "AE монітор крафтингу") } + if (Configuration.enableStorageIntegrations) { AgnosticItemStorageLookup.addBlockLookup(::extractItemStorage) AgnosticFluidStorageLookup.addBlockLookup(::extractFluidStorage) @@ -47,6 +84,7 @@ class Integration : Runnable { } if (Configuration.enableMEInterface) { ComputerCraftProxy.addProvider(MENetworkBlockPlugin.Provider) + ComputerCraftProxy.addProvider(AE2CraftingJobsPluginProvider) ComputerCraftProxy.addProvider(AE2CableObjectProvider) ComputerCraftProxy.addProvider(AE2InterfaceObjectProvider) ComputerCraftProxy.addProvider(AE2PatternProviderObjectProvider) diff --git a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt index eb12e95f..9163a582 100644 --- a/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt +++ b/projects/forge/src/main/kotlin/site/siredvin/peripheralworks/integrations/ae2/MENetworkBlockPlugin.kt @@ -1,7 +1,5 @@ package site.siredvin.peripheralworks.integrations.ae2 -import appeng.api.networking.crafting.CalculationStrategy -import appeng.api.networking.security.IActionSource import appeng.api.stacks.AEFluidKey import appeng.api.stacks.AEItemKey import appeng.api.stacks.AEKey @@ -12,17 +10,13 @@ import net.minecraft.core.BlockPos import net.minecraft.core.Direction import net.minecraft.world.level.Level import site.siredvin.broccolium.modules.platform.PlatformRegistries -import site.siredvin.broccolium.modules.platform.PlatformToolkit import site.siredvin.peripheralworks.api.PeripheralPluginProvider import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.buildKey import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.genericStackToMap -import site.siredvin.peripheralworks.integrations.ae2.AE2Helper.keyCounterToLua import site.siredvin.tweakium.modules.peripheral.api.IPeripheralPlugin import site.siredvin.tweakium.modules.peripheral.representation.LuaRepresentation -import java.util.* -import kotlin.NoSuchElementException -class MENetworkBlockPlugin(private val level: Level, private val entity: AENetworkBlockEntity) : IPeripheralPlugin { +class MENetworkBlockPlugin(private val entity: AENetworkBlockEntity) : IPeripheralPlugin { companion object { const val PLUGIN_TYPE = "ae2" } @@ -39,7 +33,7 @@ class MENetworkBlockPlugin(private val level: Level, private val entity: AENetwo if (entity !is AENetworkBlockEntity) { return null } - return MENetworkBlockPlugin(level, entity) + return MENetworkBlockPlugin(entity) } } @@ -167,42 +161,4 @@ class MENetworkBlockPlugin(private val level: Level, private val entity: AENetwo } return MethodResult.of(craftingList) } - - @LuaFunction(mainThread = false) - fun scheduleCrafting(mode: String, id_key: String, amount: Optional, targetCPU: Optional): MethodResult { - val craftingService = entity.mainNode.grid?.craftingService ?: return MethodResult.of(null, "AE2 network is not connected") - - val key = buildKey(mode, id_key) - val source = IActionSource.ofMachine(entity) - val realAmount = if (mode == "item") { - amount.orElse(1) - } else { - amount.orElse(1000) * PlatformToolkit.get().fluidCompactDivider - }.toLong() - val future = craftingService.beginCraftingCalculation( - level, - { source }, - key, - realAmount, - CalculationStrategy.REPORT_MISSING_ITEMS, - ) - val plan = future.get() - - if (!plan.missingItems().isEmpty) { - return MethodResult.of(false, "Missing items", keyCounterToLua(plan.missingItems())) - } - val realTargetCPU = if (targetCPU.isPresent) { - try { - craftingService.cpus.first { - it.name != null && it.name!!.string.equals(targetCPU.get()) - } - } catch (e: NoSuchElementException) { - return MethodResult.of(null, "Cannot find target CPU") - } - } else { - null - } - craftingService.submitJob(plan, null, realTargetCPU, false, source) - return MethodResult.of(true) - } } diff --git a/projects/forge/src/testMod/java/site/siredvin/peripheralworks/testmod/ForgePeripheralWorksTestMod.java b/projects/forge/src/testMod/java/site/siredvin/peripheralworks/testmod/ForgePeripheralWorksTestMod.java index 23d99bb7..c945db07 100644 --- a/projects/forge/src/testMod/java/site/siredvin/peripheralworks/testmod/ForgePeripheralWorksTestMod.java +++ b/projects/forge/src/testMod/java/site/siredvin/peripheralworks/testmod/ForgePeripheralWorksTestMod.java @@ -25,6 +25,7 @@ public ForgePeripheralWorksTestMod() { }); Testiarium.register(PeripheralWorksGameTests.class); Testiarium.register(AE2ConfigurableObjectsGameTests.class); + Testiarium.register(AE2WirelessTerminalGameTests.class); DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ClientTests::register); ForgeTestiarium.registerTests(); } diff --git a/projects/forge/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/AE2WirelessTerminalGameTests.kt b/projects/forge/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/AE2WirelessTerminalGameTests.kt new file mode 100644 index 00000000..49bbcc0d --- /dev/null +++ b/projects/forge/src/testMod/kotlin/site/siredvin/peripheralworks/testmod/AE2WirelessTerminalGameTests.kt @@ -0,0 +1,156 @@ +package site.siredvin.peripheralworks.testmod + +import appeng.api.config.Actionable +import appeng.api.networking.security.IActionSource +import appeng.api.stacks.AEItemKey +import appeng.blockentity.networking.WirelessAccessPointBlockEntity +import appeng.blockentity.storage.ChestBlockEntity +import appeng.core.definitions.AEBlocks +import appeng.core.definitions.AEItems +import appeng.items.tools.powered.WirelessTerminalItem +import dan200.computercraft.api.turtle.TurtleSide +import dan200.computercraft.api.upgrades.UpgradeData +import dan200.computercraft.shared.config.Config +import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity +import net.minecraft.core.BlockPos +import net.minecraft.core.GlobalPos +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestAssertException +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import site.siredvin.peripheralworks.integrations.ae2.AE2CraftingMonitorUpgrade +import site.siredvin.peripheralworks.integrations.ae2.AE2WirelessTerminalUpgrade +import site.siredvin.testiarium.api.TestGroup +import site.siredvin.testiarium.api.thenExecuteFailFast +import site.siredvin.testiarium.cct.CctComputerState + +@TestGroup("peripheralworks") +class AE2WirelessTerminalGameTests { + @GameTest(template = FIXTURE, batch = FIXTURE, timeoutTicks = 12000) + fun wirelessTerminal(helper: GameTestHelper) { + val turtle = findTurtle(helper) + val accessPointPos = turtle.blockPos.offset(0, 0, 1) + val energyPos = accessPointPos.offset(0, 0, 1) + val chestPos = energyPos.offset(1, 0, 0) + helper.level.setBlockAndUpdate(accessPointPos, AEBlocks.WIRELESS_ACCESS_POINT.block().defaultBlockState()) + helper.level.setBlockAndUpdate(energyPos, AEBlocks.CREATIVE_ENERGY_CELL.block().defaultBlockState()) + helper.level.setBlockAndUpdate(chestPos, AEBlocks.CHEST.block().defaultBlockState()) + (helper.level.getBlockEntity(chestPos) as ChestBlockEntity).setCell(AEItems.ITEM_CELL_1K.stack()) + + val terminalItem = AEItems.WIRELESS_TERMINAL.asItem() + val terminal = AEItems.WIRELESS_TERMINAL.stack().apply { + hoverName = Component.literal("Test terminal") + orCreateTag.putString("upw_test", "preserved") + } + WirelessTerminalItem.LINKABLE_HANDLER.link(terminal, GlobalPos.of(helper.level.dimension(), accessPointPos)) + terminalItem.injectAEPower(terminal, 400.0, Actionable.MODULATE) + terminalItem.getUpgrades(terminal).setItemDirect(0, AEItems.ENERGY_CARD.stack()) + val initialCharge = terminalItem.getAECurrentPower(terminal) + val upgrade = AE2WirelessTerminalUpgrade(AEItems.WIRELESS_TERMINAL.stack()) + check(ItemStack.matches(terminal, upgrade.getUpgradeItem(upgrade.getUpgradeData(terminal)))) + turtle.access.setUpgradeWithData(TurtleSide.LEFT, UpgradeData.of(upgrade, upgrade.getUpgradeData(terminal))) + val craftingTerminal = AEItems.WIRELESS_CRAFTING_TERMINAL.stack() + WirelessTerminalItem.LINKABLE_HANDLER.link(craftingTerminal, GlobalPos.of(helper.level.dimension(), accessPointPos)) + val craftingMonitor = AE2CraftingMonitorUpgrade(AEItems.WIRELESS_CRAFTING_TERMINAL.stack()) + check(ItemStack.matches(craftingTerminal, craftingMonitor.getUpgradeItem(craftingMonitor.getUpgradeData(craftingTerminal)))) + turtle.access.setUpgradeWithData(TurtleSide.RIGHT, UpgradeData.of(craftingMonitor, craftingMonitor.getUpgradeData(craftingTerminal))) + + helper.startSequence() + .thenIdle(10) + .thenExecuteFailFast { + val accessPoint = helper.level.getBlockEntity(accessPointPos) as WirelessAccessPointBlockEntity + check(accessPoint.isActive) { "Wireless access point did not become active" } + val inserted = accessPoint.grid!!.storageService.inventory.insert(AEItemKey.of(Items.STONE), 64, Actionable.MODULATE, IActionSource.empty()) + check(inserted == 64L) { "Failed to seed AE2 item storage" } + turtle.createServerComputer().turnOn() + } + .thenWaitUntil { await("initial") } + .thenExecuteFailFast { + state().check("initial") + check(turtle.access.fuelLevel == 7) { "Expected three fuel-consuming calls, got ${turtle.access.fuelLevel}" } + check(turtle.contents[0].count == 5 && turtle.contents[0].`is`(Items.STONE)) + check(turtle.contents[15].count == 5 && turtle.contents[15].`is`(Items.GOLD_INGOT)) + turtle.access.fuelLevel = 0 + } + .thenWaitUntil { await("empty-fuel") } + .thenExecuteFailFast { + state().check("empty-fuel") + Config.turtlesNeedFuel = false + turtle.access.fuelLevel = 1 + } + .thenWaitUntil { await("disabled") } + .thenExecuteFailFast { + state().check("disabled") + Config.turtlesNeedFuel = true + turtle.access.fuelLevel = 2 + } + .thenWaitUntil { await("restored") } + .thenExecuteFailFast { + state().check("restored") + check(turtle.access.teleportTo(helper.level, turtle.blockPos.offset(18, 0, 0))) + } + .thenWaitUntil { await("out-of-range") } + .thenExecuteFailFast { + state().check("out-of-range") + check(turtle.access.teleportTo(helper.level, accessPointPos.offset(0, 0, -1))) + } + .thenWaitUntil { await("returned") } + .thenExecuteFailFast { + state().check("returned") + helper.level.removeBlock(energyPos, false) + } + .thenWaitUntil { await("inactive") } + .thenExecuteFailFast { + state().check("inactive") + helper.level.setBlockAndUpdate(energyPos, AEBlocks.CREATIVE_ENERGY_CELL.block().defaultBlockState()) + } + .thenWaitUntil { await("reactivated") } + .thenExecuteFailFast { + state().check("reactivated") + val data = turtle.access.getUpgradeNBTData(TurtleSide.LEFT) + val stored = ItemStack.of(data.getCompound("terminal")) + WirelessTerminalItem.LINKABLE_HANDLER.link(stored, GlobalPos.of(helper.level.dimension(), UNLOADED_POS)) + data.put("terminal", stored.save(net.minecraft.nbt.CompoundTag())) + turtle.access.updateUpgradeNBTData(TurtleSide.LEFT) + check(helper.level.chunkSource.getChunkNow(UNLOADED_POS.x shr 4, UNLOADED_POS.z shr 4) == null) + } + .thenWaitUntil { await(CctComputerState.DONE) } + .thenExecuteFailFast { + state().check(CctComputerState.DONE) + check(helper.level.chunkSource.getChunkNow(UNLOADED_POS.x shr 4, UNLOADED_POS.z shr 4) == null) { "Wireless resolution loaded the linked chunk" } + val stored = turtle.access.getUpgradeWithData(TurtleSide.LEFT)!!.upgradeItem + check(stored.hoverName.string == "Test terminal") + check(stored.tag?.getString("upw_test") == "preserved") + check(terminalItem.getUpgrades(stored).getInstalledUpgrades(AEItems.ENERGY_CARD) == 1) + check(terminalItem.getAECurrentPower(stored) == initialCharge) { "Peripheral use changed terminal charge" } + } + .thenSucceed() + } + + private fun state() = CctComputerState.get(FIXTURE) ?: throw GameTestAssertException("Computer '$FIXTURE' has not started") + + private fun findTurtle(helper: GameTestHelper): TurtleBlockEntity { + for (x in 0 until 7) { + for (y in 0 until 4) { + for (z in 0 until 7) { + val entity = helper.getBlockEntity(BlockPos(x, y, z)) + if (entity is TurtleBlockEntity) return entity + } + } + } + throw GameTestAssertException("Fixture turtle is missing") + } + + private fun await(marker: String) { + val state = state() + if (state.isDone(CctComputerState.DONE)) state.check(CctComputerState.DONE) + if (!state.isDone(marker)) throw GameTestAssertException("Computer '$FIXTURE' has not reached $marker") + } + + companion object { + private const val FIXTURE = "peripheralworksgametests.ae2_wireless_terminal" + private val UNLOADED_POS = BlockPos(1_000_000, 64, 1_000_000) + } +} diff --git a/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2.ts b/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2.ts index d587c5a8..ea369ae9 100644 --- a/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2.ts +++ b/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2.ts @@ -13,6 +13,12 @@ export type AE2Crafting = { progress: number; CPU?: string; }; +export type AE2CraftingJob = { + id: string; + state: "running" | "done" | "canceled"; + target: object; + amount: number; +}; /** @noSelf **/ export interface AE2NetworkAPI extends IPeripheral { @@ -25,13 +31,16 @@ export interface AE2NetworkAPI extends IPeripheral { getCraftableFluids(): { name: string }[]; getPatternsFor(mode: "item" | "fluid", id: string): AE2Pattern[]; getActiveCraftings(): Fallible; + getCraftingJob(jobId: string): Fallible; + getCraftingJobs(): AE2CraftingJob[]; + cancelCrafting(jobId: string): Fallible; scheduleCrafting( mode: "item" | "fluid", id: string, amount?: number, targetCPU?: string ): LuaMultiReturn< - | [true] + | [true, string] | [null, string] | [false, string, LuaTable] >; diff --git a/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2CraftingMonitor.ts b/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2CraftingMonitor.ts new file mode 100644 index 00000000..5ee600af --- /dev/null +++ b/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2CraftingMonitor.ts @@ -0,0 +1,23 @@ +import { Fallible } from "../types"; +import { AE2CraftingJob } from "./ae2"; +import { IPeripheralProvider } from "@siredvin/typed-peripheral-base"; + +/** @noSelf **/ +export interface AE2CraftingMonitorAPI extends IPeripheral { + scheduleCrafting( + mode: "item" | "fluid", + id: string, + amount?: number, + targetCPU?: string + ): LuaMultiReturn< + | [true, string] + | [null, string] + | [false, string, LuaTable] + >; + getCraftingJob(jobId: string): Fallible; + getCraftingJobs(): AE2CraftingJob[]; + cancelCrafting(jobId: string): Fallible; +} + +export const ae2CraftingMonitorProvider = + new IPeripheralProvider("ae2_crafting_monitor"); diff --git a/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal.ts b/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal.ts new file mode 100644 index 00000000..49fc8776 --- /dev/null +++ b/projects/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal.ts @@ -0,0 +1,25 @@ +import { FuelApi } from "@siredvin/typed-peripheral-api/fuel"; +import { ItemQuery } from "@siredvin/typed-peripheral-api/item_storage"; +import { + ExtendedItemDetail, + IPeripheralProvider, + ShortItemDetail, +} from "@siredvin/typed-peripheral-base"; + +/** @noSelf **/ +export interface AE2WirelessTerminalAPI extends FuelApi { + items(): LuaTable; + items( + detailed: true, + filter?: ItemQuery + ): LuaTable; + items( + detailed: false, + filter?: ItemQuery + ): LuaTable; + pullItem(itemQuery?: ItemQuery, limit?: number, toSlot?: number): number; + pushItem(fromSlotOrItemQuery?: number | ItemQuery, limit?: number): number; +} + +export const ae2WirelessTerminalProvider = + new IPeripheralProvider("ae2_wireless_terminal"); diff --git a/projects/typescript-tests/build.mjs b/projects/typescript-tests/build.mjs index 66f71984..ef2221a8 100644 --- a/projects/typescript-tests/build.mjs +++ b/projects/typescript-tests/build.mjs @@ -16,7 +16,7 @@ for (const peripheral of [ "universal_scanner", "ultimate_sensor", "item_pedestal", "map_pedestal", "display_pedestal", "remote_observer", "peripheral_proxy", "reality_forger", "recipe_registry", "informative_registry", "statue_workbench", "entity_link", "network_manager", - "hologram_projector", "ae2_configurable_objects", + "hologram_projector", "ae2_configurable_objects", "ae2_wireless_terminal", ]) { const result = spawnSync( resolve("node_modules/.bin/tstl"), diff --git a/projects/typescript-tests/src/ae2_wireless_terminal.ts b/projects/typescript-tests/src/ae2_wireless_terminal.ts new file mode 100644 index 00000000..605d32d2 --- /dev/null +++ b/projects/typescript-tests/src/ae2_wireless_terminal.ts @@ -0,0 +1,79 @@ +import { ae2CraftingMonitorProvider } from "@siredvin/typed-peripheral-unlimitedperipheralworks/integrations/ae2CraftingMonitor"; +import { ae2WirelessTerminalProvider } from "@siredvin/typed-peripheral-unlimitedperipheralworks/integrations/ae2WirelessTerminal"; + +/** @noSelf **/ +interface TestApi { + ok(marker?: string): void; +} +declare const test: TestApi; + +const check = (value: unknown, message: string): void => { + if (!value) throw message; +}; + +const terminal = ae2WirelessTerminalProvider.findOrThrow(); +const craftingMonitor = ae2CraftingMonitorProvider.findOrThrow(); +const [missingJob, missingJobError] = craftingMonitor.getCraftingJob("missing"); +check(missingJob === null && typeof missingJobError === "string" && missingJobError.includes("not found"), "Missing crafting job was not reported"); +const [missingCancel, missingCancelError] = craftingMonitor.cancelCrafting("missing"); +check(missingCancel === null && typeof missingCancelError === "string" && missingCancelError.includes("not found"), "Missing crafting job cancellation was not reported"); +craftingMonitor.getCraftingJobs(); +const typecheckCraftingRequest = (): void => { + const [scheduled, jobId] = craftingMonitor.scheduleCrafting("item", "minecraft:stone", 1); + if (scheduled) craftingMonitor.getCraftingJob(jobId); +}; +const detailed = terminal.items(); +terminal.items(true); +const filtered = terminal.items(false, { name: "minecraft:stone" }); +check(detailed[1]?.name === "minecraft:stone" && filtered[1]?.name === "minecraft:stone", "Stone was not listed"); +const initialFuel = terminal.getFuelLevel(); +check(terminal.pullItem("minecraft:stone", 5, 1) === 5, "Stone was not pulled into slot 1"); +check(terminal.pullItem("minecraft:dirt") === 0, "Missing dirt unexpectedly moved"); +check(terminal.pushItem(16, 3) === 3, "Gold was not pushed from slot 16"); +check(terminal.getFuelLevel() === initialFuel - 3, "Transfers did not consume one fuel each"); +const fuelAfterTransfers = terminal.getFuelLevel(); +check(!pcall(() => terminal.pushItem(17))[0], "Invalid slot was accepted"); +check(terminal.getFuelLevel() === fuelAfterTransfers, "Invalid slot consumed fuel"); +terminal.items(); +check(terminal.getFuelLevel() === fuelAfterTransfers, "Item listing consumed fuel"); +check(terminal.getFuelMaxLevel() >= terminal.getFuelLevel(), "Fuel exceeded its maximum"); +check(terminal.getFuelConsumptionRate() === 1, "Unexpected fuel consumption rate"); +terminal.setFuelConsumptionRate(1); +test.ok("initial"); +while (terminal.getFuelLevel() !== 0) sleep(0.05); +check(!pcall(() => terminal.pushItem("minecraft:dirt"))[0], "Transfer succeeded without fuel"); +check(terminal.getFuelLevel() === 0, "Failed transfer changed fuel"); +test.ok("empty-fuel"); +while (terminal.getFuelLevel() === 0) sleep(0.05); +check(terminal.pushItem("minecraft:dirt") === 0, "Valid zero-move transfer returned items"); +check(terminal.getFuelLevel() === 1, "Fuel-disabled transfer consumed fuel"); +test.ok("disabled"); +while (terminal.getFuelLevel() === 1) sleep(0.05); +check(terminal.pushItem("minecraft:dirt") === 0, "Valid zero-move transfer returned items"); +check(terminal.getFuelLevel() === 1, "Valid zero-move transfer did not consume fuel"); +test.ok("restored"); +let failure = ""; +while (!failure.includes("outside wireless range")) { + const [ok, error] = pcall(() => terminal.items()); + failure = ok ? "" : `${error}`; + if (!failure) sleep(0.05); +} +test.ok("out-of-range"); +while (!pcall(() => terminal.items())[0]) sleep(0.05); +test.ok("returned"); +failure = ""; +while (!failure.includes("outside wireless range")) { + const [ok, error] = pcall(() => terminal.items()); + failure = ok ? "" : `${error}`; + if (!failure) sleep(0.05); +} +test.ok("inactive"); +while (!pcall(() => terminal.items())[0]) sleep(0.05); +test.ok("reactivated"); +failure = ""; +while (!failure.includes("unavailable")) { + const [ok, error] = pcall(() => terminal.items()); + failure = ok ? "" : `${error}`; + if (!failure) sleep(0.05); +} +test.ok();