diff --git a/docs/gui-screens.md b/docs/gui-screens.md index 2e3fca9e..7ba7e6de 100644 --- a/docs/gui-screens.md +++ b/docs/gui-screens.md @@ -4,6 +4,10 @@ > open through `block-right-click`, and use one generic menu with custom value packets. > Rendering and optional JEI integration live exclusively on the client. +Machines can opt into [inventory colours, side drawers and item-provided filter editors](inventory-automation.md) +through `ScreenHost.screenAutomation()`. This adds the selectors, fixed addon/filter slots and +embedded editor without adding per-filter XML to each screen. + ## Implementation map | Piece | Where | State | @@ -431,6 +435,13 @@ The standard player inventory (three rows of nine, a 4 px gap, then the hotbar) bottom automatically. Write `` only to place it somewhere else, or set `inventory="none"` on `` to leave it out. +Optional `` goes after the main body and before tabs/popouts. Its button +children form a right-aligned row above the player inventory (for example redstone and pause +toggles). Automation's reusable `InventoryColorButtonBar` shares that row on the left, with +scrollable overflow. The row is allocated once, and both sides align with the inventory edges. +Use ordinary button declarations inside it; no spacer or coordinates are needed. This element +requires a player inventory and is available to every screen, not just companion machines. + ## Layout - **Boxes.** `vbox` stacks children top to bottom and `hbox` left to right, so text of any @@ -700,7 +711,7 @@ When JEI is installed: Pop-out tabs are 22×24 rectangles, coloured by kind: yellow information, amber warning, red error. Clicking one opens a matching drawer outward from that edge, with the icon retained on a darker rail as the close control. Its body is up to 150 pixels wide and - extends to the main panel's bottom. Text wraps to the available width; scroll inside + fits its text, capped at the main panel's bottom and the screen edge. Text wraps to the available width; scroll inside to read longer content. Opening another drawer on the same edge replaces the open drawer; tabs remain visible and clickable on the rail. diff --git a/docs/gui-sprites.md b/docs/gui-sprites.md index 70569c55..4c86103d 100644 --- a/docs/gui-sprites.md +++ b/docs/gui-sprites.md @@ -25,9 +25,12 @@ icons and progress arrows are full colour. | `radio`, `radio_selected` | 10×10 | none | Bevelled circle: raised when off, pressed in when selected | | `progress_arrow`, `progress_arrow_filled` | 24×16 | none | Bevelled blue-grey arrow / brighter blue progress fill, with matching outlines and glossy highlights | | `upgrade_speed`, `upgrade_energy`, `upgrade_capacity` | 16×16 | none | Outline shown in an empty upgrade slot | -| `icon_sides` | 16×16 | none | White cube icon for the side-configuration button | +| `icon_sides` | 16×16 | none | Small white cube for generic controls | +| `icon_side_config` | 16×16 | none | Central machine with connected cyan/orange ports for the side-configuration drawer | +| `side_mode_input`, `side_mode_output`, `side_mode_both`, `side_mode_disabled` | 14×14 | none | Outlined cyan insertion arrow, orange extraction arrow, opposing arrows, or grey cross. The drawer draws 7×7 corner badges: input at top-right, output at bottom-right, both badges for bidirectional sides, or a bottom-right cross when disabled. | | `icon_pause`, `icon_resume` | 16×16 | none | Gold pause bars and green play triangle; `_a` and `_b` variants identify independent lanes | | `button_flat`, `button_flat_highlighted` | 14×14 | nine-slice, border 1 | Compact button with a uniform dark outline and flat grey fill; hover lightens only the interior | +| `icon_filter_whitelist`, `icon_filter_blacklist` | 10×10 | none | White/black list cards on the filter's flat grey mode toggle | | `icon_redstone_ignore`, `icon_redstone_high`, `icon_redstone_low` | 16×16 | none | Redstone torch with × / + / − mode badge | | `icon_info` | 16×16 | none | Pop-out `info`: white serif "i" with a dark outline and transparent background | | `icon_warning` | 16×16 | none | Pop-out `warning`: yellow triangle, black "!" | diff --git a/docs/inventory-automation.md b/docs/inventory-automation.md new file mode 100644 index 00000000..737b6fe3 --- /dev/null +++ b/docs/inventory-automation.md @@ -0,0 +1,173 @@ +# Inventory colours, sides and addons + +Implement `ScreenHost.screenAutomation()` to opt a machine into the inventory drawer. Existing +screens need no new XML element. The host inserts a colour selector row above the player inventory, +reserves the first right drawer button, and places a second selector vertically inside the drawer. +Each selection configures one resource group. The reusable `InventoryColorButtonBar` builds the +buttons automatically from the groups and their screen anchors: screen reading order (rows from +left to right, stacks from top to bottom), once per group. Groups without a matching anchor follow +in declaration order; item groups without explicit anchors can use their slot indices. The same +order drives the drawer selector and keyboard navigation, without renumbering saved groups or +menu slots. F6 focuses the selector; arrow keys change inventory. Overflow scrolls within the +colour bar instead of covering adjacent controls. + +Place optional machine buttons in a top-level `` after the main screen body. +They align right on the same row as the left-aligned colour bar, against the player inventory's +edges. Layout reserves this row once and computes the selector's remaining width automatically. +Screens without machine controls give the colour bar the full inventory width. +The main inventory panel remains centred when drawers or filters open. Information drawers stay +on the left; inventory side configuration stays on the right. Filter controls occupy a framed, +scrollable region inside the configuration drawer below the addon slots. The arrow bar beneath +the installed filter shows/hides this region; it has no duplicate selected-filter header. +The drawer fits its +visible controls and grows downward to fit an open filter, stopping four GUI pixels above the +screen edge (with extra clearance for JEI's bottom toolbar when visible); overflow scrolls within +the filter region. The colour selector keeps its own compact +height. Info drawers fit their text up to the main panel's bottom or the screen edge, whichever +comes first. + +## Define independent resource groups + +A `ResourceGroup` declares a stable save ID, translated name, RGB colour, `ResourceType`, +supported `IoMode`, member indices, optional screen element IDs to highlight, and `SideConfig`. +Bind its facing supplier and capability view, then create one persistent `AutomationInventory` +containing the groups. Use the same definitions on client and server. + +```java +var input = new ResourceGroup<>("input", Component.translatable("example.input"), + 0x568bd8, ResourceType.ITEMS, IoMode.INPUT, List.of(0, 1), List.of("input_slots"), + new SideConfig(IoMode.INPUT)); +input.bind(this::facing, side -> items.view(slot -> + input.contains(slot) ? input.mode(side) : IoMode.DISABLED)); +automation = new AutomationInventory(this, List.of(input)); +``` + +Register passive item/fluid capabilities using `storage.view(index -> automation.mode(type,index,side))`. +Use `energy.view(() -> automation.mode(ResourceType.ENERGY,0,side))` for energy. Every view retains +all original slot/tank indices; permissions refuse operations instead of renumbering storage. +The group's **active transfer port must restrict access to that group alone**, as above. +Both views preserve storage reservations. Null-side views still honour the declared group role. + +Groups are saved by ID, independently of UI ordering. Call `automation.save(registries)` and +`automation.load(tag, registries)` in the block entity's persistence hooks. Missing groups preserve +their raw saved content. Call `automation.dropAll(recoverResources)` from the block's contents-drop +hook. Addons, filters and pending item transfers drop as items. The required callback must preserve +pending fluid/energy transfers, unpaid energy refunds and custom resource payloads in a dropped +container. It receives a compound with a `resources` list (`type` resource ID plus transport-owned +`data`) and an optional long `refund`. Ownership is handed to the callback before pending data is +cleared. Do not also drop the same addon slots separately. This hook handles transfer escrow; +normal tank/buffer contents remain the host's separate break policy. + +The companion drops a **Recovery Capsule**, a companion item carrying this data. Use it on a +companion machine to restore fluids into accepting tanks and energy into available buffer space. +Partial restoration leaves the remainder in the same capsule; a fully restored capsule is consumed, +including in creative mode. Unknown resource payloads remain preserved. No recovery item is +registered by MMDLib itself. + +## Addon item capabilities + +Register `AddonCapabilities.ADDON` for any item through `RegisterCapabilitiesEvent`. Its +`InventoryAddon` returns an `AddonSpec`: stable type ID, resource type, required operations, +batch size, interval, RF cost per unit and filter support. Acceptance requires **both** the +resource type and supported inventory operations. Disabling every face does not invalidate an addon. + +Each group has four addon slots. A filter-capable addon exposes a smaller child slot. Register +`AddonCapabilities.FILTER` for filter items; `acceptsFilter` can further restrict compatible filters. +Each transfer role may be installed only once per group, regardless of item ID or tier: one input +addon and one output addon can coexist when the inventory supports both. A bidirectional addon +occupies both roles. Separate inventories may each have their own addons. Storage insertion, +direct assignment and client menu prediction enforce the same rule. Loading an older save keeps +the first addon for each role and queues duplicates with their attached filters for recovery. +The parent and child are real fixed menu slots. Ghost entries in editors are not real item slots. +All vanilla interaction paths use the same slot validation, including shift-click and hotbar swap. +Removing/replacing a parent returns its configured filter to the acting player's inventory, or +drops it beside the machine when full. Programmatic removals queue recovery; call `recover(player)` +after the operation, or `recover(null)` to drop queued items. Recovery is persisted across saves. + +Unknown or incompatible installed items remain removable. An installed filter whose capability is +missing blocks its module; it does not become an unfiltered transfer. Empty optional filter slots +are intentionally unfiltered. +Validation is dirty-driven: normal slot edits and loading invalidate the affected groups. Call +`automation.invalidateAddons()` after an external addon-capability/definition change; this also +invalidates editor generations. Use the slot setters for attachment changes rather than mutating +installed stacks without notification. + +## Item-provided filter panels + +`InventoryFilter` supplies a resource type, editor ID, control tree, matching predicate and +configuration validation. The default configuration adapter stores only the `mmdlib_filter` +compound in the stack's custom data and preserves unrelated components. + +```java +public FilterControl controls(ItemStack stack) { + return new FilterControl.Box("panel", false, List.of( + new FilterControl.Toggle("deny", MY_ICON, Component.translatable("example.deny")), + new FilterControl.Samples("entries", 18, ResourceType.ITEMS))); +} +``` + +Use nested `Box` controls for rows/columns, `Toggle` for flat icon buttons, `Text` for bounded text/tag +input, `Number` for bounded numeric values, and `Samples` for a wrapped ghost grid. Width is supplied +by the drawer; tall content scrolls within the available screen space. Counts, nesting, IDs and +text lengths are bounded. A sample comes from the **server's cursor stack**, without consuming it. +Standard item samples match item IDs; fluid samples use a fluid container's contents. A filter +may override sample extraction and validation for stricter matching. Ghost item previews render at +60% opacity. A toggle may provide separate icons and tooltips for its false/true states; the +three-argument constructor retains one shared icon and tooltip. The companion's basic item filter +provides eight ghost cells and white/black list cards for its mode toggle. + +For specialised controls, register a common `FilterWidgetType` with a bounded action codec and +a handler that updates only a candidate configuration. Include `FilterControl.Custom` in the tree. +On the client, `FilterWidgets.register(type, factory)` supplies a fresh instance per open panel. +Its access object exposes measured bounds, configuration copies, pending state and a typed +submitter. The host clips rendering and routes focus, keyboard, pointer dragging, release and +scroll events. `cancel`/`removed` let controls discard drafts and capture when their attachment, +layout or selection changes. Client registrations must stay in client-only classes. + +The server resolves the current installed item and declared control before any edit. Menu session, +attachment generation and configuration revision reject stale/replayed edits and concurrent +overwrites. The candidate is committed only after full filter validation. Authoritative stack sync +updates all viewers. Return `false` from validation to keep the original configuration unchanged. +Custom action JSON is limited to 256 characters; choose codecs with bounded collections and values. + +See `CompanionAddons` and `CompanionWidgetsClient`: the basic/expanded item filters use different +grid sizes, the fluid filter accepts fluid samples, and the tag filter supplies a text field plus +a separately registered custom allow/deny button. None requires machine-specific screen XML. + +## Active transfers + +Call `AutomationTransfers.tick(automation, energy)` once per server tick, after recipe processing. +The scheduler rotates module and face priorities, observes enabled sides and installed predicates, +and charges RF only for actual delivery. It never force-loads a neighbour's chunk. Built-in item, +fluid and energy transports simulate before extracting and retain unexpected insertion remainders +in persistent escrow for later delivery. Inbound escrow belongs to the machine already and finishes +locally without querying the old source, even if that neighbour is removed or external sides are +disabled. Outbound escrow first tries its previous destination, then other enabled output faces; +with no accepting destination it remains saved. It retains its original direction and cost even +if the module is removed, and cannot be reused as a new transfer with different settings. +Refunds that do not fit the power buffer remain persisted and are retried when room is available. +Capability implementations must obey the normal +simulation/count contracts; no API can recover reliably from an external handler throwing after +an undocumented partial mutation. + +The companion uses paid mechanical pullers/pushers and fluid pumps. Its energy relays are lossless +and have no auxiliary RF fee, so importing can bootstrap an empty buffer. Pending refund ownership +is independent of buffer capacity, including when handlers refill the buffer during a transfer. + +Register future content with `ResourceType.register(id, capabilityInterface)` and +`AutomationTransfers.register(type, transport)`. Custom transports implement `move` for a fresh +transfer and `deliver` for an already-extracted payload; `deliver` must not consult or extract from +the source. They own their bounded persistent escrow schema and must preserve unaccepted resources. +Override `recovery` to provide a resource-only payload for the host's break container; the default +preserves the whole pending state. Resource types are not a closed enum; gas +support can add a capability and transport without changing the drawer. + +The optional companion's Logistics Workbench demonstrates seven groups: two item inventories, +four independent fluid tanks and energy. Its output tanks can be filled by the owning machine/test +setup; passive external insertion correctly refuses them. + +Neighbour capability lookups use per-machine NeoForge caches that follow invalidation, replacement +and chunk loading. Resource membership uses indexed lookups, and menus construct sync snapshots +only when their revision or viewer state changes. `AutomationLayout` is the shared source for drawer +frames, real slot coordinates and filter hit bounds. Unsided companion energy queries retain the +buffer's normal insert/extract limits; sided queries additionally obey inventory side settings. diff --git a/scripts/generate_gui_sprites.py b/scripts/generate_gui_sprites.py index ca507300..d049b7f5 100644 --- a/scripts/generate_gui_sprites.py +++ b/scripts/generate_gui_sprites.py @@ -256,6 +256,14 @@ def write_flat_buttons(): write_png(name, img, {"type": "nine_slice", "width": 14, "height": 14, "border": 1}) +def write_filter_icons(): + """White/black list cards for the flat grey mode toggle.""" + for mode, fill in (("whitelist", 0xF1), ("blacklist", 0x24)): + img = canvas(10, 10) + rect(img, 2, 1, 6, 8, grey(fill)) + write_png("icon_filter_" + mode, img) + + def write_control_icons(): """16 px control glyphs with a one-pixel shadow and optional lane/mode badge.""" def glyph(img, rows, x, y, ink): @@ -290,6 +298,58 @@ def glyph(img, rows, x, y, ink): write_png("icon_redstone_" + mode, img) +def write_side_config_sprites(): + """Outlined directional glyphs remain legible over faded neighbour previews.""" + cyan, orange = (133, 210, 255, 255), (255, 185, 101, 255) + ink = (30, 40, 52, 255) + + def outlined(pixels): + result = canvas(len(pixels[0]), len(pixels)) + for y, row in enumerate(pixels): + for x, colour in enumerate(row): + if colour == CLEAR: continue + for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): + nx, ny = x + dx, y + dy + if 0 <= ny < len(result) and 0 <= nx < len(row): result[ny][nx] = ink + for y, row in enumerate(pixels): + for x, colour in enumerate(row): + if colour != CLEAR: result[y][x] = colour + return result + + def arrow(img, x, y, down, colour): + rows = ["..##..", "..##..", "..##..", "..##..", "######", ".####.", "..##.."] + if not down: rows = rows[::-1] + for dy, row in enumerate(rows): + for dx, bit in enumerate(row): + if bit == "#": img[y + dy][x + dx] = colour + + for mode in ("input", "output", "both", "disabled"): + img = canvas(14, 14) + if mode == "both": + arrow(img, 1, 5, True, cyan) + arrow(img, 7, 2, False, orange) + elif mode == "disabled": + for i in range(8): + rect(img, 3 + i, 3 + i, 1, 1, grey(205)) + rect(img, 10 - i, 3 + i, 1, 1, grey(205)) + else: + arrow(img, 4, 2, mode == "input", cyan if mode == "input" else orange) + rect(img, 3, 10, 8, 2, grey(225)) + rect(img, 3, 9, 1, 1, grey(225)) + rect(img, 10, 9, 1, 1, grey(225)) + write_png("side_mode_" + mode, outlined(img)) + + # A central machine with four connected, coloured ports identifies side configuration. + img = canvas(16, 16) + rect(img, 7, 2, 2, 12, grey(220)) + rect(img, 2, 7, 12, 2, grey(220)) + rect(img, 5, 5, 6, 6, grey(240)) + rect(img, 6, 6, 4, 4, (93, 119, 151, 255)) + for x, y, colour in ((6, 1, cyan), (1, 6, cyan), (11, 6, orange), (6, 11, orange)): + rect(img, x, y, 4, 4, colour) + write_png("icon_side_config", outlined(img)) + + def main(): OUT.mkdir(parents=True, exist_ok=True) print(f"writing to {OUT}:") @@ -301,7 +361,9 @@ def main(): write_png("compact_frame", compact_frame(), {"type": "nine_slice", "width": 8, "height": 8, "border": 1}) write_drawer_sprites() write_control_icons() + write_filter_icons() write_flat_buttons() + write_side_config_sprites() write_png("checkbox", checkbox(False)) write_png("checkbox_selected", checkbox(True)) write_png("radio", radio(False)) diff --git a/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java b/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java new file mode 100644 index 00000000..9f11af46 --- /dev/null +++ b/src/companion/java/zone/moddev/mc/companion/CompanionAddons.java @@ -0,0 +1,167 @@ +package zone.moddev.mc.companion; + +import java.util.*; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.*; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.*; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.registries.*; + +import zone.moddev.mc.lib.api.automation.*; +import zone.moddev.mc.lib.api.capabilities.IoMode; + +/** Companion content demonstrates capabilities: the library does not register concrete addons. */ +public final class CompanionAddons { + private static final DeferredRegister ITEMS = + DeferredRegister.create(Registries.ITEM, CompanionMod.ID); + public static final DeferredHolder RECOVERY_CAPSULE = + ITEMS.register("recovery_capsule", RecoveryCapsuleItem::new); + private static final Map, InventoryAddon> ADDONS = + new LinkedHashMap<>(); + private static final Map, InventoryFilter> FILTERS = + new LinkedHashMap<>(); + public static final FilterWidgetType TAG_MODE = + FilterWidgetType.register( + CompanionMod.id("tag_mode"), + com.mojang.serialization.Codec.BOOL, + (config, deny) -> { + config.putBoolean("deny", deny); + return true; + }); + public static final DeferredHolder PULLER = + addon("mechanical_puller", ResourceType.ITEMS, IoMode.INPUT, 8, 20, 4, true); + public static final DeferredHolder PUSHER = + addon("mechanical_pusher", ResourceType.ITEMS, IoMode.OUTPUT, 8, 20, 4, true); + public static final DeferredHolder INPUT_PUMP = + addon("input_pump", ResourceType.FLUIDS, IoMode.INPUT, 250, 10, 1, true); + public static final DeferredHolder OUTPUT_PUMP = + addon("output_pump", ResourceType.FLUIDS, IoMode.OUTPUT, 250, 10, 1, true); + public static final DeferredHolder IMPORT_RELAY = + addon("import_relay", ResourceType.ENERGY, IoMode.INPUT, 1000, 5, 0, false); + public static final DeferredHolder EXPORT_RELAY = + addon("export_relay", ResourceType.ENERGY, IoMode.OUTPUT, 1000, 5, 0, false); + public static final DeferredHolder ITEM_FILTER = + filter("item_filter", ResourceType.ITEMS, 8, false); + public static final DeferredHolder LARGE_FILTER = + filter("large_item_filter", ResourceType.ITEMS, 36, false); + public static final DeferredHolder TAG_FILTER = + filter("tag_filter", ResourceType.ITEMS, 0, true); + public static final DeferredHolder FLUID_FILTER = + filter("fluid_filter", ResourceType.FLUIDS, 6, false); + + private static DeferredHolder addon( + String name, + ResourceType type, + IoMode direction, + int batch, + int interval, + long cost, + boolean filters) { + var item = ITEMS.register(name, () -> new Item(new Item.Properties().stacksTo(1))); + var spec = + new AddonSpec(CompanionMod.id(name), type, direction, batch, interval, cost, filters); + ADDONS.put(item, stack -> spec); + return item; + } + + private static DeferredHolder filter( + String name, ResourceType type, int size, boolean tag) { + var item = ITEMS.register(name, () -> new Item(new Item.Properties().stacksTo(1))); + FILTERS.put( + item, + new InventoryFilter() { + public ResourceType resource(ItemStack stack) { + return type; + } + + public ResourceLocation editorType(ItemStack stack) { + return CompanionMod.id(name); + } + + public FilterControl controls(ItemStack stack) { + return new FilterControl.Box( + "root", + false, + List.of( + tag + ? new FilterControl.Custom("deny", TAG_MODE, 14) + : new FilterControl.Toggle( + "deny", + ResourceLocation.parse("mmdlib:icon_filter_whitelist"), + ResourceLocation.parse("mmdlib:icon_filter_blacklist"), + Component.translatable("screen.mmdlib_companion.filter.whitelist"), + Component.translatable("screen.mmdlib_companion.filter.blacklist")), + tag + ? new FilterControl.Text( + "tag", + 128, + Component.translatable("screen.mmdlib_companion.filter.tag")) + : new FilterControl.Samples("samples", size, type))); + } + + public boolean validate( + ItemStack stack, + net.minecraft.nbt.CompoundTag config, + HolderLookup.Provider registries) { + if (!tag || config.getString("tag").isEmpty()) return true; + var id = ResourceLocation.tryParse(config.getString("tag")); + return id != null + && registries + .lookupOrThrow(Registries.ITEM) + .get(TagKey.create(Registries.ITEM, id)) + .isPresent(); + } + + public boolean matches( + ItemStack stack, Object candidate, HolderLookup.Provider registries) { + var config = configuration(stack); + boolean match = false; + if (tag && candidate instanceof ItemStack item) { + var id = ResourceLocation.tryParse(config.getString("tag")); + match = id != null && item.is(TagKey.create(Registries.ITEM, id)); + } else { + String id = + candidate instanceof ItemStack item + ? BuiltInRegistries.ITEM.getKey(item.getItem()).toString() + : candidate instanceof FluidStack fluid + ? BuiltInRegistries.FLUID.getKey(fluid.getFluid()).toString() + : ""; + var samples = config.getList("samples", net.minecraft.nbt.Tag.TAG_STRING); + for (int i = 0; i < Math.min(size, samples.size()); i++) + if (samples.getString(i).equals(id)) { + match = true; + break; + } + } + return config.getBoolean("deny") ? !match : match; + } + }); + return item; + } + + public static List> items() { + var result = new ArrayList<>(ADDONS.keySet()); + result.addAll(FILTERS.keySet()); + return List.copyOf(result); + } + + public static void register(IEventBus bus) { + ITEMS.register(bus); + } + + public static void capabilities(RegisterCapabilitiesEvent event) { + ADDONS.forEach( + (item, provider) -> + event.registerItem(AddonCapabilities.ADDON, (stack, context) -> provider, item.get())); + FILTERS.forEach( + (item, provider) -> + event.registerItem(AddonCapabilities.FILTER, (stack, context) -> provider, item.get())); + } +} diff --git a/src/companion/java/zone/moddev/mc/companion/CompanionMod.java b/src/companion/java/zone/moddev/mc/companion/CompanionMod.java index 9c798c46..f1e5e16a 100644 --- a/src/companion/java/zone/moddev/mc/companion/CompanionMod.java +++ b/src/companion/java/zone/moddev/mc/companion/CompanionMod.java @@ -37,7 +37,8 @@ @Mod(CompanionMod.ID) public final class CompanionMod { public static final String ID = "mmdlib_companion"; - private static final DeferredRegister BLOCKS = DeferredRegister.create(Registries.BLOCK, ID); + private static final DeferredRegister BLOCKS = + DeferredRegister.create(Registries.BLOCK, ID); private static final DeferredRegister ITEMS = DeferredRegister.create(Registries.ITEM, ID); private static final DeferredRegister> ENTITIES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, ID); @@ -53,23 +54,32 @@ public final class CompanionMod { public static final MachineRecipeType TRANSMUTING = MachineRecipeTypes.register(RECIPE_TYPES, SERIALIZERS, "transmuting"); public static final Machine CRUSHER = new Machine("crusher", ProcessorEntity.Kind.CRUSHER); - public static final Machine TRANSMUTER = new Machine("transmuter", ProcessorEntity.Kind.TRANSMUTER); + public static final Machine TRANSMUTER = + new Machine("transmuter", ProcessorEntity.Kind.TRANSMUTER); public static final Machine DUAL_CRUSHER = new Machine("dual_crusher", ProcessorEntity.Kind.DUAL); public static final Machine CREATIVE_POWER = new Machine("creative_power", null); + public static final Machine LOGISTICS = new Machine("logistics", ProcessorEntity.Kind.LOGISTICS); public static final List MACHINES = - List.of(CRUSHER, TRANSMUTER, DUAL_CRUSHER, CREATIVE_POWER); + List.of(CRUSHER, TRANSMUTER, DUAL_CRUSHER, CREATIVE_POWER, LOGISTICS); static { - TABS.register("machines", () -> CreativeModeTab.builder() - .title(Component.translatable("itemGroup.mmdlib_companion")) - .icon(() -> new ItemStack(CRUSHER.block.get())) - .displayItems((parameters, output) -> - MACHINES.forEach(machine -> output.accept(machine.block.get()))) - .build()); + TABS.register( + "machines", + () -> + CreativeModeTab.builder() + .title(Component.translatable("itemGroup.mmdlib_companion")) + .icon(() -> new ItemStack(CRUSHER.block.get())) + .displayItems( + (parameters, output) -> { + MACHINES.forEach(machine -> output.accept(machine.block.get())); + CompanionAddons.items().forEach(item -> output.accept(item.get())); + }) + .build()); } public CompanionMod(IEventBus bus) { CompanionWidgets.register(); + CompanionAddons.register(bus); BLOCKS.register(bus); ITEMS.register(bus); ENTITIES.register(bus); @@ -77,50 +87,103 @@ public CompanionMod(IEventBus bus) { SERIALIZERS.register(bus); TABS.register(bus); bus.addListener(CompanionMod::capabilities); - MaterialExtensions.GLOBAL.events().registerCallback( - id("tank_container"), MaterialEventTypes.GUI_CLICK, CompanionMod::transferTankContainer); - MaterialExtensions.GLOBAL.events().registerCallback( - id("redstone"), MaterialEventTypes.GUI_CLICK, - click -> { - if (click.menu().blockEntity instanceof ProcessorEntity machine) machine.cycleRedstoneMode(); - }); + MaterialExtensions.GLOBAL + .events() + .registerCallback( + id("tank_container"), + MaterialEventTypes.GUI_CLICK, + CompanionMod::transferTankContainer); + MaterialExtensions.GLOBAL + .events() + .registerCallback( + id("redstone"), + MaterialEventTypes.GUI_CLICK, + click -> { + if (click.menu().blockEntity instanceof ProcessorEntity machine) + machine.cycleRedstoneMode(); + }); for (int lane = 0; lane < 2; lane++) { int index = lane; - MaterialExtensions.GLOBAL.events().registerCallback( - id("toggle_" + lane), MaterialEventTypes.GUI_CLICK, - click -> { - if (click.menu().blockEntity instanceof ProcessorEntity machine) machine.toggle(index); - }); + MaterialExtensions.GLOBAL + .events() + .registerCallback( + id("toggle_" + lane), + MaterialEventTypes.GUI_CLICK, + click -> { + if (click.menu().blockEntity instanceof ProcessorEntity machine) + machine.toggle(index); + }); } } private static void transferTankContainer(ScreenClick click) { - if (!(click.menu().blockEntity instanceof ProcessorEntity machine) || !machine.hasTank()) return; + if (!(click.menu().blockEntity instanceof ProcessorEntity machine) || !machine.hasTank()) + return; var carried = click.menu().getCarried(); if (carried.isEmpty() || FluidUtil.getFluidHandler(carried).isEmpty()) return; var inventory = click.player().getCapability(Capabilities.ItemHandler.ENTITY); if (inventory == null) return; // Work on the server's cursor stack. NeoForge handles partial containers and stacked results. - var result = FluidUtil.tryEmptyContainerAndStow( - carried, machine.fluids, inventory, Integer.MAX_VALUE, click.player(), true); + var result = + FluidUtil.tryEmptyContainerAndStow( + carried, machine.fluids, inventory, Integer.MAX_VALUE, click.player(), true); if (!result.isSuccess()) - result = FluidUtil.tryFillContainerAndStow( - carried, machine.fluids, inventory, Integer.MAX_VALUE, click.player(), true); + result = + FluidUtil.tryFillContainerAndStow( + carried, machine.fluids, inventory, Integer.MAX_VALUE, click.player(), true); if (result.isSuccess()) { click.menu().setCarried(result.getResult()); click.menu().broadcastChanges(); } else { - click.player().displayClientMessage(Component.translatable("screen.mmdlib_companion.tank.refused"), true); + click + .player() + .displayClientMessage( + Component.translatable("screen.mmdlib_companion.tank.refused"), true); } } private static void capabilities(RegisterCapabilitiesEvent event) { - MachineCapabilities.fluids(event, CRUSHER.type.get(), entity -> ((ProcessorEntity) entity).fluids); + CompanionAddons.capabilities(event); for (var machine : MACHINES) { - MachineCapabilities.energy(event, machine.type.get(), entity -> entity.energy, - (entity, side) -> entity instanceof CreativePowerEntity ? IoMode.OUTPUT : IoMode.INPUT); - if (machine != CREATIVE_POWER) - MachineCapabilities.items(event, machine.type.get(), entity -> ((ProcessorEntity) entity).items); + if (machine == CREATIVE_POWER) { + MachineCapabilities.energy( + event, machine.type.get(), entity -> entity.energy, (entity, side) -> IoMode.OUTPUT); + continue; + } + event.registerBlockEntity( + Capabilities.ItemHandler.BLOCK, + machine.type.get(), + (entity, side) -> { + var p = (ProcessorEntity) entity; + return p.items.view( + slot -> + p.automation.mode( + zone.moddev.mc.lib.api.automation.ResourceType.ITEMS, slot, side)); + }); + event.registerBlockEntity( + Capabilities.FluidHandler.BLOCK, + machine.type.get(), + (entity, side) -> { + var p = (ProcessorEntity) entity; + return p.hasTank() + ? p.fluids.view( + tank -> + p.automation.mode( + zone.moddev.mc.lib.api.automation.ResourceType.FLUIDS, tank, side)) + : null; + }); + event.registerBlockEntity( + Capabilities.EnergyStorage.BLOCK, + machine.type.get(), + (entity, side) -> { + var p = (ProcessorEntity) entity; + return side == null + ? p.energy.view(zone.moddev.mc.lib.api.capabilities.IoMode.BOTH) + : p.energy.view( + () -> + p.automation.mode( + zone.moddev.mc.lib.api.automation.ResourceType.ENERGY, 0, side)); + }); } } @@ -136,11 +199,16 @@ public static final class Machine { private Machine(String name, ProcessorEntity.Kind kind) { this.kind = kind; - block = BLOCKS.register(name, () -> new DemoBlock( - BlockBehaviour.Properties.of().strength(3.5F).requiresCorrectToolForDrops(), - this::entityType)); - type = ENTITIES.register(name, () -> - BlockEntityType.Builder.of(this::create, block.get()).build(null)); + block = + BLOCKS.register( + name, + () -> + new DemoBlock( + BlockBehaviour.Properties.of().strength(3.5F).requiresCorrectToolForDrops(), + this::entityType)); + type = + ENTITIES.register( + name, () -> BlockEntityType.Builder.of(this::create, block.get()).build(null)); ITEMS.register(name, () -> new BlockItem(block.get(), new Item.Properties())); } @@ -149,7 +217,8 @@ private BlockEntityType entityType() { } private DemoEntity create(BlockPos pos, BlockState state) { - return kind == null ? new CreativePowerEntity(type.get(), pos, state) + return kind == null + ? new CreativePowerEntity(type.get(), pos, state) : new ProcessorEntity(type.get(), pos, state, kind); } } diff --git a/src/companion/java/zone/moddev/mc/companion/DemoBlock.java b/src/companion/java/zone/moddev/mc/companion/DemoBlock.java index e4e34712..ad12296a 100644 --- a/src/companion/java/zone/moddev/mc/companion/DemoBlock.java +++ b/src/companion/java/zone/moddev/mc/companion/DemoBlock.java @@ -45,14 +45,29 @@ public BlockEntityTicker getTicker( } @Override - protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, - BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - if (level.getBlockEntity(pos) instanceof ProcessorEntity machine && machine.hasTank() + protected ItemInteractionResult useItemOn( + ItemStack stack, + BlockState state, + Level level, + BlockPos pos, + Player player, + InteractionHand hand, + BlockHitResult hit) { + if (stack.is(CompanionAddons.RECOVERY_CAPSULE.get()) + && level.getBlockEntity(pos) instanceof ProcessorEntity machine) { + if (level.isClientSide()) return ItemInteractionResult.SUCCESS; + return RecoveryCapsuleItem.restore(stack, machine) + ? ItemInteractionResult.CONSUME + : ItemInteractionResult.FAIL; + } + if (level.getBlockEntity(pos) instanceof ProcessorEntity machine + && machine.hasTank() && FluidUtil.getFluidHandler(stack).isPresent()) { if (level.isClientSide()) return ItemInteractionResult.SUCCESS; if (FluidUtil.interactWithFluidHandler(player, hand, machine.fluids)) return ItemInteractionResult.CONSUME; - player.displayClientMessage(Component.translatable("screen.mmdlib_companion.tank.refused"), true); + player.displayClientMessage( + Component.translatable("screen.mmdlib_companion.tank.refused"), true); // Do not let a rejected bucket spill its contents beside the machine. return ItemInteractionResult.FAIL; } @@ -60,10 +75,11 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev } @Override - protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, - Player player, BlockHitResult hit) { + protected InteractionResult useWithoutItem( + BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { return ScreenRuntime.open(player, level.getBlockEntity(pos)) - ? InteractionResult.sidedSuccess(level.isClientSide()) : InteractionResult.PASS; + ? InteractionResult.sidedSuccess(level.isClientSide()) + : InteractionResult.PASS; } @Override diff --git a/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java b/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java index 4d18816d..eb41a738 100644 --- a/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java +++ b/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java @@ -4,6 +4,7 @@ import java.util.List; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; @@ -21,6 +22,8 @@ import net.neoforged.neoforge.fluids.FluidStack; +import zone.moddev.mc.lib.api.automation.*; +import zone.moddev.mc.lib.api.capabilities.*; import zone.moddev.mc.lib.api.capabilities.FluidStorage; import zone.moddev.mc.lib.api.capabilities.ItemStorage; import zone.moddev.mc.lib.api.capabilities.Role; @@ -34,17 +37,21 @@ import zone.moddev.mc.lib.machine.neoforge.StorageGroups; /** The dual crusher deliberately runs two lanes over the same input and output groups. */ -public final class ProcessorEntity extends DemoEntity implements ScreenHost, DropsContents, Clearable { +public final class ProcessorEntity extends DemoEntity + implements ScreenHost, DropsContents, Clearable { public enum Kind { CRUSHER, TRANSMUTER, - DUAL + DUAL, + LOGISTICS } public final ItemStorage items; + public final AutomationInventory automation; public final FluidStorage fluids = new FluidStorage().onChange(this::setChanged); public final List lanes; - public final FeatureHost features = new FeatureHost(this::setChanged, () -> FeatureSync.send(this)); + public final FeatureHost features = + new FeatureHost(this::setChanged, () -> FeatureSync.send(this)); private final Kind kind; // 0 ignores redstone, 1 requires a signal, 2 requires no signal. Manual pause is separate. private int redstoneMode; @@ -78,16 +85,22 @@ public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, super(type, pos, state); this.kind = kind; if (kind == Kind.CRUSHER) - fluids.addTank(4_000, Role.STORAGE, + fluids.addTank( + 4_000, + Role.STORAGE, fluid -> fluid.getFluid() == Fluids.WATER || fluid.getFluid() == Fluids.LAVA); + if (kind == Kind.LOGISTICS) + for (int i = 0; i < 4; i++) fluids.addTank(4000, i < 2 ? Role.INPUT : Role.OUTPUT); var inventory = MachineInventory.builder().group("energy", StorageGroups.energy(energy)); if (kind == Kind.TRANSMUTER) { // One key, and one item: the recipe consumes the whole slot before inserting gold. items = new ItemStorage(1).limit(1, 0).onChange(this::setChanged); inventory.group("slot", StorageGroups.items(items, 0)); } else { - items = new ItemStorage(2).role(Role.INPUT, 0).role(Role.OUTPUT, 1).onChange(this::setChanged); - inventory.group("input", StorageGroups.items(items, 0)) + items = + new ItemStorage(2).role(Role.INPUT, 0).role(Role.OUTPUT, 1).onChange(this::setChanged); + inventory + .group("input", StorageGroups.items(items, 0)) .group("output", StorageGroups.items(items, 1)); } var shared = inventory.build(); @@ -99,12 +112,85 @@ public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, features.add(processor); } lanes = List.copyOf(processors); + var groups = new ArrayList>(); + groups.add( + itemGroup("input", 0, 0x568bd8, kind == Kind.TRANSMUTER ? IoMode.BOTH : IoMode.INPUT)); + if (kind != Kind.TRANSMUTER) groups.add(itemGroup("output", 1, 0xe48f45, IoMode.OUTPUT)); + int[] colours = {0x64bfc1, 0xa678cb, 0xe0c55d, 0xce668e}; + for (int i = 0; i < fluids.getTanks(); i++) { + final int tank = i; + var group = + new ResourceGroup<>( + "tank_" + i, + Component.translatable("screen.mmdlib_companion.group.tank", i + 1), + colours[i], + ResourceType.FLUIDS, + kind == Kind.CRUSHER ? IoMode.BOTH : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT), + List.of(i), + List.of(i == 0 ? "tank" : "tank_" + i), + new SideConfig( + kind == Kind.CRUSHER ? IoMode.BOTH : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT))); + group.bind( + () -> Direction.NORTH, + side -> fluids.view(index -> index == tank ? group.mode(side) : IoMode.DISABLED)); + groups.add(group); + } + var energyGroup = + new ResourceGroup<>( + "energy", + Component.translatable("screen.mmdlib_companion.group.energy"), + 0xd85b5b, + ResourceType.ENERGY, + kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT, + List.of(0), + List.of("energy"), + new SideConfig(kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT)); + if (kind == Kind.LOGISTICS) energy.setRates(1000, 1000); + energyGroup.bind(() -> Direction.NORTH, side -> energy.view(() -> energyGroup.mode(side))); + groups.add(energyGroup); + automation = new AutomationInventory(this, groups); + } + + private ResourceGroup itemGroup( + String id, int slot, int colour, IoMode mode) { + var group = + new ResourceGroup<>( + id, + Component.translatable("screen.mmdlib_companion.group." + id), + colour, + ResourceType.ITEMS, + mode, + List.of(slot), + List.of(id), + new SideConfig(mode)); + return group.bind( + () -> Direction.NORTH, + side -> items.view(index -> index == slot ? group.mode(side) : IoMode.DISABLED)); + } + + @Override + public AutomationInventory screenAutomation() { + return automation; } @Override public void serverTick() { - if (redstoneAllowsRun() && energy.stored() >= energy.capacity() * reservePercent / 100) - features.tick(level); + if (kind != Kind.LOGISTICS + && redstoneAllowsRun() + && energy.stored() >= energy.capacity() * reservePercent / 100) features.tick(level); + AutomationTransfers.tick(automation, energy); + } + + public FluidStack getTank1() { + return fluids.getFluidInTank(1); + } + + public FluidStack getTank2() { + return fluids.getFluidInTank(2); + } + + public FluidStack getTank3() { + return fluids.getFluidInTank(3); } public int getRedstoneMode() { @@ -117,8 +203,7 @@ public void cycleRedstoneMode() { } private boolean redstoneAllowsRun() { - return redstoneMode == 0 - || (level.hasNeighborSignal(worldPosition) == (redstoneMode == 1)); + return redstoneMode == 0 || (level.hasNeighborSignal(worldPosition) == (redstoneMode == 1)); } public void toggle(int index) { @@ -177,9 +262,13 @@ public List getWarnings() { for (int i = 0; i < lanes.size(); i++) { var status = lanes.get(i).status(); if (status == RecipeProcessor.Status.STALLED || status == RecipeProcessor.Status.BLOCKED) - warnings.add(Component.translatable("screen.mmdlib_companion.lane_" + (i == 0 ? "a" : "b")) - .append(": ").append(Component.translatable( - "status.mmdlib_companion." + status.name().toLowerCase(java.util.Locale.ROOT)))); + warnings.add( + Component.translatable("screen.mmdlib_companion.lane_" + (i == 0 ? "a" : "b")) + .append(": ") + .append( + Component.translatable( + "status.mmdlib_companion." + + status.name().toLowerCase(java.util.Locale.ROOT)))); } return warnings; } @@ -198,17 +287,26 @@ public int[] screenRecipeSlots(ResourceLocation category) { @Override public void dropContents(Level level, BlockPos pos) { items.dropAll(level, pos); + automation.dropAll( + payload -> + net.minecraft.world.Containers.dropItemStack( + level, + pos.getX() + .5, + pos.getY() + .5, + pos.getZ() + .5, + RecoveryCapsuleItem.create(payload))); } @Override public void clearContent() { + automation.clear(); lanes.forEach(RecipeProcessor::reset); for (int slot = 0; slot < items.getSlots(); slot++) { items.setPlan(slot, SlotPlan.NONE); items.setStackInSlot(slot, ItemStack.EMPTY); } energy.setStored(0); - if (hasTank()) fluids.tank(0).setFluid(FluidStack.EMPTY); + for (int i = 0; i < fluids.getTanks(); i++) fluids.tank(i).setFluid(FluidStack.EMPTY); setChanged(); FeatureSync.send(this); } @@ -217,6 +315,7 @@ public void clearContent() { protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); tag.put("items", items.serializeNBT(registries)); + tag.put("automation", automation.save(registries)); tag.putInt("redstone_mode", redstoneMode); tag.putString("machine_name", machineName); tag.putInt("reserve_percent", reservePercent); @@ -239,7 +338,9 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) : "Crusher"; reservePercent = Math.clamp(tag.getInt("reserve_percent"), 0, 100); items.deserializeNBT(registries, tag.getCompound("items")); - if (hasTank() && tag.contains("fluids")) fluids.deserializeNBT(registries, tag.getCompound("fluids")); + automation.load(tag.getCompound("automation"), registries); + if (hasTank() && tag.contains("fluids")) + fluids.deserializeNBT(registries, tag.getCompound("fluids")); features.load(registries.createSerializationContext(NbtOps.INSTANCE), tag.get("features")); } diff --git a/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java b/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java new file mode 100644 index 00000000..333e043e --- /dev/null +++ b/src/companion/java/zone/moddev/mc/companion/RecoveryCapsuleItem.java @@ -0,0 +1,97 @@ +package zone.moddev.mc.companion; + +import java.util.List; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.*; +import net.minecraft.world.item.component.CustomData; + +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; + +import zone.moddev.mc.lib.api.automation.ResourceType; + +/** Concrete companion content; the library hands its host only the pending resource data. */ +public final class RecoveryCapsuleItem extends Item { + private static final String KEY = "mmdlib_recovery"; + + public RecoveryCapsuleItem() { + super(new Item.Properties().stacksTo(1)); + } + + public static ItemStack create(CompoundTag payload) { + var stack = new ItemStack(CompanionAddons.RECOVERY_CAPSULE.get()); + var data = new CompoundTag(); + data.put(KEY, payload.copy()); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(data)); + return stack; + } + + public static CompoundTag contents(ItemStack stack) { + return stack + .getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag() + .getCompound(KEY); + } + + /** + * Partial restoration leaves every unaccepted resource on the same item, including in creative. + */ + public static boolean restore(ItemStack stack, ProcessorEntity target) { + if (!stack.is(CompanionAddons.RECOVERY_CAPSULE.get()) + || target.getLevel() == null + || target.getLevel().isClientSide) return false; + var outer = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + var payload = outer.getCompound(KEY); + var resources = payload.getList("resources", Tag.TAG_COMPOUND); + boolean changed = false; + for (int i = resources.size() - 1; i >= 0; i--) { + var entry = resources.getCompound(i); + var data = entry.getCompound("data"); + String type = entry.getString("type"); + if (type.equals(ResourceType.FLUIDS.id().toString())) { + var fluid = + FluidStack.parseOptional(target.getLevel().registryAccess(), data.getCompound("fluid")); + int moved = target.fluids.fill(fluid, IFluidHandler.FluidAction.EXECUTE); + if (moved > 0) { + changed = true; + fluid.shrink(moved); + if (fluid.isEmpty()) data.remove("fluid"); + else data.put("fluid", fluid.save(target.getLevel().registryAccess())); + } + } else if (type.equals(ResourceType.ENERGY.id().toString())) { + int amount = data.getInt("energy"); + int moved = (int) target.energy.insert(amount, false); + if (moved > 0) { + changed = true; + if (moved == amount) data.remove("energy"); + else data.putInt("energy", amount - moved); + } + } + if (data.isEmpty()) resources.remove(i); + } + long refund = Math.max(0, payload.getLong("refund")); + long moved = target.energy.insert(refund, false); + if (moved > 0) changed = true; + if (!changed) return false; + if (refund == moved) payload.remove("refund"); + else payload.putLong("refund", refund - moved); + if (resources.isEmpty()) payload.remove("resources"); + if (payload.isEmpty()) stack.shrink(1); + else { + outer.put(KEY, payload); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(outer)); + } + target.setChanged(); + return true; + } + + @Override + public void appendHoverText( + ItemStack stack, TooltipContext context, List lines, TooltipFlag flag) { + lines.add(Component.translatable("item.mmdlib_companion.recovery_capsule.help")); + } +} diff --git a/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java b/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java index 0212392c..d347bfab 100644 --- a/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java +++ b/src/companion/java/zone/moddev/mc/companion/client/CompanionWidgetsClient.java @@ -17,6 +17,55 @@ private CompanionWidgetsClient() {} @SubscribeEvent public static void setup(FMLClientSetupEvent event) { + zone.moddev.mc.lib.gui.client.FilterWidgets.register( + zone.moddev.mc.companion.CompanionAddons.TAG_MODE, + access -> + new zone.moddev.mc.lib.gui.client.FilterWidgets.Instance() { + public void render( + net.minecraft.client.gui.GuiGraphics g, + zone.moddev.mc.lib.gui.layout.Rect r, + net.minecraft.nbt.CompoundTag config, + int mx, + int my) { + var button = new zone.moddev.mc.lib.gui.layout.Rect(r.x(), r.y(), 14, 14); + g.blitSprite( + net.minecraft.resources.ResourceLocation.parse( + button.contains(mx, my) + ? "mmdlib:button_flat_highlighted" + : "mmdlib:button_flat"), + button.x(), + button.y(), + button.width(), + button.height()); + g.blitSprite( + net.minecraft.resources.ResourceLocation.parse( + config.getBoolean("deny") + ? "mmdlib:icon_filter_blacklist" + : "mmdlib:icon_filter_whitelist"), + r.x() + 2, + r.y() + 2, + 10, + 10); + } + + public net.minecraft.network.chat.Component tooltip() { + return net.minecraft.network.chat.Component.translatable( + access.configuration().get().getBoolean("deny") + ? "screen.mmdlib_companion.filter.blacklist" + : "screen.mmdlib_companion.filter.whitelist"); + } + + public boolean click(double x, double y, int button) { + if (!access.pending().getAsBoolean()) + access.submit().accept(!access.configuration().get().getBoolean("deny")); + return true; + } + + public boolean key(int key, int scan, int modifiers) { + if (key == 257 || key == 32) return click(0, 0, 0); + return false; + } + }); WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.NAME_EDITOR, NameEditor::new); WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.RESERVE_SLIDER, ReserveSlider::new); WidgetRenderers.GLOBAL.registerInteractive(CompanionWidgets.LANE_SELECTOR, LaneSelector::new); diff --git a/src/companion/resources/assets/mmdlib_companion/blockstates/logistics.json b/src/companion/resources/assets/mmdlib_companion/blockstates/logistics.json new file mode 100644 index 00000000..5d17bc0f --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/blockstates/logistics.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib_companion:block/logistics" + } + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/lang/en_us.json b/src/companion/resources/assets/mmdlib_companion/lang/en_us.json index 49849e3d..13f3285a 100644 --- a/src/companion/resources/assets/mmdlib_companion/lang/en_us.json +++ b/src/companion/resources/assets/mmdlib_companion/lang/en_us.json @@ -48,5 +48,26 @@ "screen.mmdlib_companion.tank.refused": "Cannot transfer fluid. The tank holds water or lava, one type at a time, up to 4 buckets.", "screen.mmdlib_companion.info.crusher": "*Crusher*\n\nCrush ore into raw materials, or stone into gravel and sand.\n\nPlace a power source beside the machine. Pause to release reserved items.\n\nThe left tank holds 4 buckets of water or lava for display. Click the gauge with a fluid container on the cursor to fill or drain it, or right-click the block with a bucket. Empty the tank before changing fluids. Crushing does not use the fluid.", "screen.mmdlib_companion.info.transmuter": "*Iron to gold*\n\nInsert one iron ingot. The same slot holds the gold when the cycle finishes.\n\nSupply power from an adjacent block. Pause to release the input.", - "screen.mmdlib_companion.info.dual_crusher": "*Dual crusher*\n\nTwo lanes share the input and output slots. Each lane reserves its own items and space.\n\nPause either lane independently to release its share." + "screen.mmdlib_companion.info.dual_crusher": "*Dual crusher*\n\nTwo lanes share the input and output slots. Each lane reserves its own items and space.\n\nPause either lane independently to release its share.", + "item.mmdlib_companion.mechanical_puller": "Mechanical Puller", + "item.mmdlib_companion.mechanical_pusher": "Mechanical Pusher", + "item.mmdlib_companion.input_pump": "Input Pump", + "item.mmdlib_companion.output_pump": "Output Pump", + "item.mmdlib_companion.import_relay": "Import Relay", + "item.mmdlib_companion.export_relay": "Export Relay", + "item.mmdlib_companion.item_filter": "Item Filter", + "item.mmdlib_companion.large_item_filter": "Expanded Item Filter", + "item.mmdlib_companion.tag_filter": "Tag Filter", + "item.mmdlib_companion.fluid_filter": "Fluid Filter", + "block.mmdlib_companion.logistics": "Logistics Workbench", + "screen.mmdlib_companion.group.input": "Item input", + "screen.mmdlib_companion.group.output": "Item output", + "screen.mmdlib_companion.group.energy": "Energy", + "screen.mmdlib_companion.group.tank": "Fluid tank %s", + "screen.mmdlib_companion.filter.deny": "Toggle allow-list / deny-list", + "screen.mmdlib_companion.filter.whitelist": "Whitelist: allow matching entries (click for blacklist)", + "screen.mmdlib_companion.filter.blacklist": "Blacklist: block matching entries (click for whitelist)", + "screen.mmdlib_companion.filter.tag": "Item tag (namespace:path); Enter to apply", + "item.mmdlib_companion.recovery_capsule": "Recovery Capsule", + "item.mmdlib_companion.recovery_capsule.help": "Use on a companion machine to restore pending fluids and energy. Any remainder stays in the capsule." } diff --git a/src/companion/resources/assets/mmdlib_companion/models/block/logistics.json b/src/companion/resources/assets/mmdlib_companion/models/block/logistics.json new file mode 100644 index 00000000..f699c048 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/block/logistics.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "minecraft:block/copper_block" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/export_relay.json b/src/companion/resources/assets/mmdlib_companion/models/item/export_relay.json new file mode 100644 index 00000000..a6402781 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/export_relay.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/repeater" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/fluid_filter.json b/src/companion/resources/assets/mmdlib_companion/models/item/fluid_filter.json new file mode 100644 index 00000000..9b422900 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/fluid_filter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/glass_bottle" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/import_relay.json b/src/companion/resources/assets/mmdlib_companion/models/item/import_relay.json new file mode 100644 index 00000000..724d0a33 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/import_relay.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/redstone" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/input_pump.json b/src/companion/resources/assets/mmdlib_companion/models/item/input_pump.json new file mode 100644 index 00000000..eb3c09c9 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/input_pump.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/water_bucket" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/item_filter.json b/src/companion/resources/assets/mmdlib_companion/models/item/item_filter.json new file mode 100644 index 00000000..1fa971f1 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/item_filter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/paper" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/large_item_filter.json b/src/companion/resources/assets/mmdlib_companion/models/item/large_item_filter.json new file mode 100644 index 00000000..952787b8 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/large_item_filter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/map" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/logistics.json b/src/companion/resources/assets/mmdlib_companion/models/item/logistics.json new file mode 100644 index 00000000..8bb53b5b --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/logistics.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib_companion:block/logistics" +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/mechanical_puller.json b/src/companion/resources/assets/mmdlib_companion/models/item/mechanical_puller.json new file mode 100644 index 00000000..499aca18 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/mechanical_puller.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/hopper" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/mechanical_pusher.json b/src/companion/resources/assets/mmdlib_companion/models/item/mechanical_pusher.json new file mode 100644 index 00000000..9697ac8f --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/mechanical_pusher.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:block/piston_side" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/output_pump.json b/src/companion/resources/assets/mmdlib_companion/models/item/output_pump.json new file mode 100644 index 00000000..12186178 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/output_pump.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/bucket" + } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/recovery_capsule.json b/src/companion/resources/assets/mmdlib_companion/models/item/recovery_capsule.json new file mode 100644 index 00000000..f6aee214 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/recovery_capsule.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:item/generated", + "textures": { "layer0": "minecraft:item/ender_pearl" } +} diff --git a/src/companion/resources/assets/mmdlib_companion/models/item/tag_filter.json b/src/companion/resources/assets/mmdlib_companion/models/item/tag_filter.json new file mode 100644 index 00000000..61969bf8 --- /dev/null +++ b/src/companion/resources/assets/mmdlib_companion/models/item/tag_filter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/name_tag" + } +} diff --git a/src/companion/resources/data/mmdlib_companion/loot_table/blocks/logistics.json b/src/companion/resources/data/mmdlib_companion/loot_table/blocks/logistics.json new file mode 100644 index 00000000..87822e48 --- /dev/null +++ b/src/companion/resources/data/mmdlib_companion/loot_table/blocks/logistics.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "mmdlib_companion:logistics" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/companion/resources/mmdlib/screen/crusher.xml b/src/companion/resources/mmdlib/screen/crusher.xml index d535146e..624cf681 100644 --- a/src/companion/resources/mmdlib/screen/crusher.xml +++ b/src/companion/resources/mmdlib/screen/crusher.xml @@ -8,7 +8,7 @@ tooltip="screen.mmdlib_companion.tank.tip" tooltip-fallback="Water or lava: {value} / {max} mB. Click with a fluid container to transfer." on-click="mmdlib_companion:tank_container" on-right-click="mmdlib_companion:tank_container"/> - + - + @@ -28,14 +28,6 @@ tooltip="screen.mmdlib_companion.settings.tip" tooltip-fallback="Edit the machine name, energy reserve and lane state." shows="panel:settings"/> -