Compare commits

...

12 Commits

60 changed files with 1826 additions and 985 deletions

View File

@ -7,17 +7,17 @@ import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.slot.Slot;
import net.minecraft.text.Text;
import net.shadowfacts.phycon.block.terminal.TerminalScreen;
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.spongepowered.asm.mixin.Mixin;
/**
* @author shadowfacts
*/
@Mixin(TerminalScreen.class)
public abstract class MixinTerminalScreen extends HandledScreen<TerminalScreenHandler> implements ISpecialScrollableScreen, IContainerScreen {
@Mixin(AbstractTerminalScreen.class)
public abstract class MixinTerminalScreen extends HandledScreen<AbstractTerminalScreenHandler> implements ISpecialScrollableScreen, IContainerScreen {
private MixinTerminalScreen(TerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) {
private MixinTerminalScreen(AbstractTerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) {
super(screenHandler, playerInventory, text);
}

View File

@ -8,7 +8,7 @@ import me.shedaniel.rei.api.plugins.REIPluginV0
import net.fabricmc.api.ClientModInitializer
import net.minecraft.client.MinecraftClient
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.block.terminal.TerminalScreen
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen
/**
* @author shadowfacts
@ -17,7 +17,7 @@ object PhyConPlugin: ClientModInitializer, REIPluginV0 {
const val MODID = "phycon_rei"
override fun onInitializeClient() {
TerminalScreen.registerClickHandler { mouseX, mouseY, button ->
AbstractTerminalScreen.registerClickHandler { mouseX, mouseY, button ->
REIHelper.getInstance().searchTextField?.also {
if (it.bounds.contains(mouseX, mouseY)) {
this.terminalVC.searchField.resignFirstResponder()
@ -32,8 +32,8 @@ object PhyConPlugin: ClientModInitializer, REIPluginV0 {
override fun getPluginIdentifier() = Identifier(MODID, "rei_plugin")
override fun registerBounds(helper: DisplayHelper) {
BaseBoundsHandler.getInstance().registerExclusionZones(TerminalScreen::class.java) {
val screen = MinecraftClient.getInstance().currentScreen as TerminalScreen
BaseBoundsHandler.getInstance().registerExclusionZones(AbstractTerminalScreen::class.java) {
val screen = MinecraftClient.getInstance().currentScreen as AbstractTerminalScreen<*, *>
val view = screen.terminalVC.settingsView
val rect = view.convert(view.bounds, to = null)
listOf(

View File

@ -6,8 +6,8 @@ import net.minecraft.client.render.item.ItemRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.shadowfacts.phycon.block.terminal.TerminalScreen;
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -26,8 +26,8 @@ public class MixinHandledScreen {
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableDepthTest()V")
)
private void drawSlotUnderlay(MatrixStack matrixStack, Slot slot, CallbackInfo ci) {
if ((Object)this instanceof TerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this;
if ((Object)this instanceof AbstractTerminalScreen) {
AbstractTerminalScreen<?, ?> self = (AbstractTerminalScreen<?, ?>)(Object)this;
self.drawSlotUnderlay(matrixStack, slot);
}
}
@ -37,9 +37,9 @@ public class MixinHandledScreen {
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/ItemRenderer;renderGuiItemOverlay(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V")
)
private void drawSlotAmount(ItemRenderer itemRenderer, TextRenderer textRenderer, ItemStack stack, int x, int y, @Nullable String countLabel, MatrixStack matrixStack, Slot slot) {
if ((Object)this instanceof TerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this;
TerminalScreenHandler handler = self.getScreenHandler();
if ((Object)this instanceof AbstractTerminalScreen) {
AbstractTerminalScreen<?, ?> self = (AbstractTerminalScreen<?, ?>)(Object)this;
AbstractTerminalScreenHandler<?> handler = self.getScreenHandler();
if (slot.id < handler.getNetworkSlotsEnd() && stack.getCount() > 1) {
self.drawNetworkSlotAmount(stack, x, y, slot, matrixStack);
return;

View File

@ -1,6 +1,7 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture
@ -8,6 +9,7 @@ import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.NinePatchView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
import kotlin.math.floor
/**
* An abstract button class. Cannot be constructed directly, used for creating button implementations with their own
@ -44,18 +46,38 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
* unless the background view is not fully opaque.
*/
var background: View? = NinePatchView(NinePatchTexture.BUTTON_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The background to draw when the button is hovered over by the mouse.
* If `null`, the normal [background] will be used.
* @see background
*/
var hoveredBackground: View? = NinePatchView(NinePatchTexture.BUTTON_HOVERED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used.
* @see background
*/
var disabledBackground: View? = NinePatchView(NinePatchTexture.BUTTON_DISABLED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The tooltip text shown when this button is hovered.
*/
var tooltip: Text? = null
override fun wasAdded() {
solver.dsl {
@ -67,22 +89,28 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK)
content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK)
content.bottomAnchor.greaterThanOrEqualTo(bottomAnchor - padding, WEAK)
listOfNotNull(background, hoveredBackground, disabledBackground).forEach {
addSubview(it)
it.leftAnchor equalTo leftAnchor
it.rightAnchor equalTo rightAnchor
it.topAnchor equalTo topAnchor
it.bottomAnchor equalTo bottomAnchor
}
}
listOfNotNull(background, hoveredBackground, disabledBackground).forEach(::addBackground)
super.wasAdded()
}
private fun addBackground(view: View) {
if (superview != null && hasSolver) {
addSubview(view)
solver.dsl {
view.leftAnchor equalTo leftAnchor
view.rightAnchor equalTo rightAnchor
view.topAnchor equalTo topAnchor
view.bottomAnchor equalTo bottomAnchor
}
}
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top)
RenderHelper.translate(floor(frame.left), floor(frame.top))
RenderHelper.fill(matrixStack, bounds, backgroundColor)
@ -96,6 +124,10 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
// don't draw subviews, otherwise all background views + content will get drawn
RenderHelper.popMatrix()
if (tooltip != null && mouse in bounds) {
window!!.drawTooltip(listOf(tooltip!!))
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {

View File

@ -26,9 +26,10 @@ object PhysicalConnectivity: ModInitializer {
PhyItems.init()
PhyScreens.init()
registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
registerGlobalReceiver(C2SConfigureDevice)
for (it in FabricLoader.getInstance().getEntrypoints("phycon", PhyConPlugin::class.java)) {
it.initializePhyCon(PhyConAPIImpl)

View File

@ -8,6 +8,7 @@ import net.fabricmc.fabric.api.renderer.v1.RendererAccess
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial
import net.shadowfacts.phycon.block.inserter.InserterScreen
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreen
import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreen
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.block.terminal.TerminalScreen
import net.shadowfacts.phycon.client.PhyExtendedModelProvider
@ -40,6 +41,7 @@ object PhysicalConnectivityClient: ClientModInitializer {
}
ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen)
ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen)
ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen)
ScreenRegistry.register(PhyScreens.REDSTONE_EMITTER, ::RedstoneEmitterScreen)

View File

@ -16,7 +16,7 @@ import net.minecraft.world.World
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode
@ -39,7 +39,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
private val invProxy = MinerInvProxy(this)
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = TerminalBlockEntity.INSERTION_TIMEOUT
override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT
override val controller = ActivationController(40L, this)
override var providerPriority = 0

View File

@ -0,0 +1,75 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
import net.minecraft.util.ItemScatterer
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet
/**
* @author shadowfacts
*/
abstract class AbstractTerminalBlock<T: AbstractTerminalBlockEntity>: DeviceBlock<T>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
companion object {
val FACING = Properties.FACING
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.allOf(Direction::class.java)
set.remove(state[FACING])
return set
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return defaultState.with(FACING, context.playerLookDirection.opposite)
}
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult {
getBlockEntity(world, pos)!!.onActivate(player)
return ActionResult.SUCCESS
}
override fun onStateReplaced(state: BlockState, world: World, pos: BlockPos, newState: BlockState, moved: Boolean) {
if (!state.isOf(newState.block)) {
val be = getBlockEntity(world, pos)!!
be.dropItems()
super.onStateReplaced(state, world, pos, newState, moved)
}
}
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) {
to.offer(getBlockEntity(world, pos))
}
}

View File

@ -0,0 +1,272 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.Inventory
import net.minecraft.inventory.InventoryChangedListener
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.TranslatableText
import net.minecraft.util.ItemScatterer
import net.minecraft.util.Tickable
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
/**
* @author shadowfacts
*/
abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>): DeviceBlockEntity(type),
InventoryChangedListener,
BlockEntityClientSerializable,
Tickable,
ItemStackPacketHandler,
NetworkStackDispatcher<AbstractTerminalBlockEntity.PendingInsertion> {
companion object {
// the locate/insertion timeouts are only 1 tick because that's long enough to hear from every device on the network
// in a degraded state (when there's latency in the network), not handling interface priorities correctly is acceptable
val LOCATE_REQUEST_TIMEOUT: Long = 1 // ticks
val INSERTION_TIMEOUT: Long = 1
}
protected val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
val internalBuffer = TerminalBufferInventory(18)
protected val pendingRequests = LinkedList<StackLocateRequest>()
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = INSERTION_TIMEOUT
private var observers = 0
val cachedNetItems = ItemStackCollections.intMap()
// todo: multiple players could have the terminal open simultaneously
var netItemObserver: WeakReference<NetItemObserver>? = null
init {
internalBuffer.addListener(this)
}
override fun findDestination(): Interface? {
for (dir in Direction.values()) {
val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir)
if (itf != null) {
return itf
}
}
return null
}
override fun handle(packet: Packet) {
when (packet) {
is ReadInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
is CapacityPacket -> handleCapacity(packet)
}
}
private fun handleReadInventory(packet: ReadInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
updateAndSync()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
updateAndSync()
}
private fun handleStackLocation(packet: StackLocationPacket) {
val request = pendingRequests.firstOrNull {
ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack)
}
if (request != null) {
request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) {
stackLocateRequestCompleted(request)
}
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val mode =
if (packet.bounceCount > 0) {
// if this stack bounced from an inventory, that means we previously tried to send it to the network, so retry
TerminalBufferInventory.Mode.TO_NETWORK
} else {
TerminalBufferInventory.Mode.FROM_NETWORK
}
val remaining = internalBuffer.insert(packet.stack, mode)
// this happens outside the normal update loop because by receiving the item stack packet
// we "know" how much the count in the source inventory has changed
updateAndSync()
return remaining
}
protected fun updateAndSync() {
updateNetItems()
// syncs the internal buffer to the client
sync()
// syncs the open container (if any) to the client
netItemObserver?.get()?.netItemsChanged()
}
private fun updateNetItems() {
cachedNetItems.clear()
for (inventory in inventoryCache.values) {
for (stack in inventory.storedStacks) {
val amount = inventory.getAmount(stack)
cachedNetItems.mergeInt(stack, amount) { a, b -> a + b }
}
}
}
private fun beginInsertions() {
if (world!!.isClient) return
for (slot in 0 until internalBuffer.size()) {
if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue
if (pendingInsertions.any { it.bufferSlot == slot }) continue
val stack = internalBuffer.getStack(slot)
dispatchItemStack(stack) { insertion ->
insertion.bufferSlot = slot
}
}
}
private fun finishPendingRequests() {
if (world!!.isClient) return
if (pendingRequests.isEmpty()) return
val finishable = pendingRequests.filter { it.isFinishable(counter) }
// stackLocateRequestCompleted removes the object from pendingRequests
finishable.forEach(::stackLocateRequestCompleted)
}
override fun tick() {
super.tick()
if (!world!!.isClient) {
finishPendingRequests()
finishTimedOutPendingInsertions()
}
if (counter % 20 == 0L && !world!!.isClient) {
beginInsertions()
}
}
open fun onActivate(player: PlayerEntity) {
}
fun requestItem(stack: ItemStack, amount: Int = stack.count) {
val request = StackLocateRequest(stack, amount, counter)
pendingRequests.add(request)
// locate packets are sent immediately instead of being added to a queue
// otherwise the terminal UI feels sluggish and unresponsive
sendPacket(LocateStackPacket(stack, ipAddress))
}
protected open fun stackLocateRequestCompleted(request: StackLocateRequest) {
pendingRequests.remove(request)
val sortedResults = request.results.toMutableList()
sortedResults.sortWith { a, b ->
// sort results first by provider priority, and then by the count that it can provide
if (a.second.providerPriority == b.second.providerPriority) {
b.first - a.first
} else {
b.second.providerPriority - a.second.providerPriority
}
}
var amountRequested = 0
while (amountRequested < request.amount && sortedResults.isNotEmpty()) {
val (sourceAmount, sourceInterface) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, request.amount - amountRequested)
amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress))
}
}
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun finishInsertion(insertion: PendingInsertion): ItemStack {
val remaining = super.finishInsertion(insertion)
internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateAndSync()
return remaining
}
override fun onInventoryChanged(inv: Inventory) {
if (inv == internalBuffer && world != null && !world!!.isClient) {
markDirty()
sync()
}
}
open fun dropItems() {
ItemScatterer.spawn(world, pos, internalBuffer)
}
override fun toCommonTag(tag: CompoundTag) {
super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag())
}
override fun fromCommonTag(tag: CompoundTag) {
super.fromCommonTag(tag)
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
}
interface NetItemObserver {
fun netItemsChanged()
}
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
var bufferSlot by Delegates.notNull<Int>()
}
open class StackLocateRequest(
val stack: ItemStack,
val amount: Int,
val timestamp: Long,
) {
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
val totalResultAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
fun isFinishable(currentTimestamp: Long): Boolean {
// we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to
// correctly sort by priority
return currentTimestamp - timestamp >= LOCATE_REQUEST_TIMEOUT
}
}
}

View File

@ -0,0 +1,200 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.Element
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoHandledScreen
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.LinkedList
import kotlin.math.ceil
import kotlin.math.min
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreen<BE: AbstractTerminalBlockEntity, T: AbstractTerminalScreenHandler<BE>>(
handler: T,
playerInv: PlayerInventory,
title: Text,
val terminalBackgroundWidth: Int,
val terminalBackgroundHeight: Int,
): CacaoHandledScreen<T>(handler, playerInv, title) {
companion object {
private val clickHandlers = LinkedList<AbstractTerminalScreen<*, *>.(Double, Double, Int) -> Boolean?>()
fun registerClickHandler(handler: AbstractTerminalScreen<*, *>.(Double, Double, Int) -> Boolean?) {
clickHandlers.add(handler)
}
}
abstract val backgroundTexture: Identifier
val terminalVC: AbstractTerminalViewController<*, *, *>
var amountVC: TerminalRequestAmountViewController? = null
var searchQuery = ""
var scrollPosition = 0.0
init {
backgroundWidth = terminalBackgroundWidth
backgroundHeight = terminalBackgroundHeight
terminalVC = createViewController()
addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems()
}
abstract fun createViewController(): AbstractTerminalViewController<*, *, *>
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat()))
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int, slot: Slot, matrixStack: MatrixStack) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
// draw damage bar
// empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
matrixStack.push()
matrixStack.translate(x.toDouble(), y.toDouble(), itemRenderer.zOffset + 200.0)
val scale = 2 / 3f
matrixStack.scale(scale, scale, 1.0f)
val immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().buffer)
val textX = (1 / scale * 18) - textRenderer.getWidth(s).toFloat() - 3
val textY = (1 / scale * 18) - 11
textRenderer.draw(s, textX, textY, 0xffffff, true, matrixStack.peek().model, immediate, false, 0, 0xF000F0)
immediate.draw()
matrixStack.pop()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
drawBackgroundTexture(matrixStack)
}
open fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(backgroundTexture)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
override fun tick() {
super.tick()
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
for (handler in clickHandlers) {
val res = handler(mouseX, mouseY, button)
if (res != null) {
return res
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) {
val stack = slot.stack
if (type == SlotActionType.QUICK_MOVE) {
// shift click, request full stack
requestItem(stack, min(stack.count, stack.maxCount))
} else if (type == SlotActionType.PICKUP) {
if (clickData == 1) {
// right click, request half stack
requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
} else {
showRequestAmountDialog(stack)
}
}
}
}
private val fakeFocusedElement = TextFieldWidget(textRenderer, 0, 0, 0, 0, LiteralText(""))
override fun getFocused(): Element? {
return if (windows.last().firstResponder != null) {
fakeFocusedElement
} else {
null
}
}
}

View File

@ -0,0 +1,254 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.ScreenHandlerType
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.DefaultPlugin
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.copyWithCount
import java.lang.ref.WeakReference
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreenHandler<T: AbstractTerminalBlockEntity>(
handlerType: ScreenHandlerType<*>,
syncId: Int,
val playerInv: PlayerInventory,
val terminal: T,
): ScreenHandler(handlerType, syncId),
AbstractTerminalBlockEntity.NetItemObserver {
private val rowsDisplayed = 6
private val fakeInv = FakeInventory(this)
private var searchQuery: String = ""
private var settings = TerminalSettings()
var totalEntries = 0
private set
var scrollPosition = 0f
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
it.stack.copyWithCount(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
open val xOffset: Int = 0
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
// intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings
}
val xOffset = xOffset
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, xOffset + 66 + x * 18, 18 + y * 18))
}
}
// internal buffer
for (y in 0 until 6) {
for (x in 0 until 3) {
addSlot(Slot(terminal.internalBuffer, y * 3 + x, xOffset + 8 + x * 18, 18 + y * 18))
}
}
// player inv
for (y in 0 until 3) {
for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, xOffset + 66 + x * 18, 140 + y * 18))
}
}
// hotbar
for (x in 0 until 9) {
addSlot(Slot(playerInv, x, xOffset + 66 + x * 18, 198))
}
}
override fun netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1)
val key = Registry.ITEM.getKey(it.key.item)
if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) {
return@filter true
}
}
it.key.name.string.contains(searchQuery, true)
}
totalEntries = filtered.size
val sorted =
when (settings[DefaultPlugin.SORT_MODE]) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
val offsetInItems = currentScrollOffsetInItems()
val end = min(offsetInItems + rowsDisplayed * 9, sorted.size)
itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) }
// itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, settings, scrollPosition, totalEntries))
}
fun totalRows(): Int {
return ceil(totalEntries / 9f).toInt()
}
fun maxScrollOffsetInRows(): Int {
return totalRows() - rowsDisplayed
}
fun currentScrollOffsetInRows(): Int {
return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt())
}
fun currentScrollOffsetInItems(): Int {
return currentScrollOffsetInRows() * 9
}
fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, settings: TerminalSettings, scrollPosition: Float) {
this.searchQuery = query
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
terminal.netItemObserver = null
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
if (isBufferSlot(slotId)) {
// todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) {
// placing cursor stack into buffer
val bufferSlot = slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index
terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK)
}
}
return super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY;
}
val slot = slots[slotId]
if (!slot.hasStack()) {
return ItemStack.EMPTY
}
val result = slot.stack.copy()
if (isBufferSlot(slotId)) {
// last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
if (slot.stack.isEmpty) {
terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED)
}
} else if (isPlayerSlot(slotId)) {
val slotsInsertedInto = tryInsertItem(slot.stack, bufferSlotsStart until playerSlotsStart) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK }
slotsInsertedInto.forEach { terminal.internalBuffer.markSlot(it - bufferSlotsStart, TerminalBufferInventory.Mode.TO_NETWORK) }
if (slot.stack.isEmpty) {
return ItemStack.EMPTY
}
}
return result
}
private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection<Int> {
val slotsInsertedInto = mutableListOf<Int>()
for (index in slots) {
if (stack.isEmpty) break
if (!slotPredicate(index)) continue
val slot = this.slots[index]
val slotStack = slot.stack
if (slotStack.isEmpty) {
slot.stack = stack.copy()
stack.count = 0
slot.markDirty()
slotsInsertedInto.add(index)
} else if (canStacksCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
val maxToMove = slotStack.maxCount - slotStack.count
val toMove = min(maxToMove, stack.count)
slotStack.increment(toMove)
stack.decrement(toMove)
slot.markDirty()
slotsInsertedInto.add(index)
}
}
return slotsInsertedInto
}
val networkSlotsStart = 0
val networkSlotsEnd = 54
val bufferSlotsStart = 54
val bufferSlotsEnd = 72
val playerSlotsStart = 72
val playerSlotsEnd = 72 + 36
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
}

View File

@ -0,0 +1,181 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.MathHelper
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.LayoutGuide
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.client.screen.ScrollTrackView
import net.shadowfacts.phycon.util.TerminalSettings
/**
* @author shadowfacts
*/
abstract class AbstractTerminalViewController<BE: AbstractTerminalBlockEntity, S: AbstractTerminalScreen<BE, H>, H: AbstractTerminalScreenHandler<BE>>(
val screen: S,
val handler: H,
val terminal: BE = handler.terminal,
): ViewController() {
private lateinit var scrollTrack: ScrollTrackView
lateinit var settingsView: View
private set
lateinit var searchField: TextField
private set
lateinit var pane: LayoutGuide
private set
lateinit var buffer: LayoutGuide
private set
lateinit var network: LayoutGuide
private set
lateinit var playerInv: LayoutGuide
private set
lateinit var networkLabel: View
private set
lateinit var playerInvLabel: View
private set
lateinit var bufferLabel: View
private set
override fun loadView() {
view = ScrollHandlingView(this)
}
override fun viewDidLoad() {
super.viewDidLoad()
pane = view.addLayoutGuide()
view.solver.dsl {
pane.centerXAnchor equalTo view.centerXAnchor
pane.centerYAnchor equalTo view.centerYAnchor
pane.widthAnchor equalTo screen.terminalBackgroundWidth
pane.heightAnchor equalTo screen.terminalBackgroundHeight
}
buffer = view.addLayoutGuide()
view.solver.dsl {
buffer.leftAnchor equalTo (pane.leftAnchor + 7 + handler.xOffset)
buffer.topAnchor equalTo (pane.topAnchor + 17)
buffer.widthAnchor equalTo (18 * 3)
buffer.heightAnchor equalTo (18 * 6)
}
network = view.addLayoutGuide()
view.solver.dsl {
network.leftAnchor equalTo (pane.leftAnchor + 65 + handler.xOffset)
network.topAnchor equalTo buffer.topAnchor
network.widthAnchor equalTo (18 * 9)
network.heightAnchor equalTo (18 * 6)
}
playerInv = view.addLayoutGuide()
view.solver.dsl {
playerInv.leftAnchor equalTo network.leftAnchor
playerInv.topAnchor equalTo (pane.topAnchor + 139)
playerInv.widthAnchor equalTo (18 * 9)
playerInv.heightAnchor equalTo 76
}
networkLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_network"))).apply {
textColor = Color.TEXT
}
playerInvLabel = view.addSubview(Label(handler.playerInv.displayName)).apply {
textColor = Color.TEXT
}
bufferLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_buffer"))).apply {
textColor = Color.TEXT
}
searchField = view.addSubview(TerminalSearchField()).apply {
handler = ::searchFieldChanged
drawBackground = false
}
searchField.becomeFirstResponder()
scrollTrack = view.addSubview(ScrollTrackView(::scrollPositionChanged))
val settingsStack = view.addSubview(StackView(Axis.VERTICAL, spacing = 2.0))
settingsView = settingsStack
TerminalSettings.allKeys.forEach { key ->
val button = SettingButton(key)
button.handler = { settingsChanged() }
settingsStack.addArrangedSubview(button)
}
view.solver.dsl {
networkLabel.leftAnchor equalTo network.leftAnchor
networkLabel.topAnchor equalTo (pane.topAnchor + 6)
bufferLabel.leftAnchor equalTo buffer.leftAnchor
bufferLabel.topAnchor equalTo networkLabel.topAnchor
playerInvLabel.leftAnchor equalTo playerInv.leftAnchor
playerInvLabel.topAnchor equalTo (pane.topAnchor + 128)
searchField.leftAnchor equalTo (pane.leftAnchor + 138 + handler.xOffset)
searchField.topAnchor equalTo (pane.topAnchor + 5)
searchField.widthAnchor equalTo 80
searchField.heightAnchor equalTo 9
scrollTrack.leftAnchor equalTo (pane.leftAnchor + 232 + handler.xOffset)
scrollTrack.topAnchor equalTo (network.topAnchor + 1)
scrollTrack.bottomAnchor equalTo (network.bottomAnchor - 1)
scrollTrack.widthAnchor equalTo 12
settingsStack.leftAnchor equalTo (pane.rightAnchor + 4)
settingsStack.topAnchor equalTo pane.topAnchor
}
}
private fun searchFieldChanged(field: TextField) {
screen.searchQuery = field.text
screen.requestUpdatedItems()
}
private fun scrollPositionChanged(track: ScrollTrackView) {
val oldOffset = handler.currentScrollOffsetInRows()
handler.scrollPosition = track.scrollPosition.toFloat()
screen.scrollPosition = track.scrollPosition
if (handler.currentScrollOffsetInRows() != oldOffset) {
screen.requestUpdatedItems()
}
}
private fun settingsChanged() {
screen.requestUpdatedItems()
}
class TerminalSearchField: TextField("") {
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
// no-op
}
}
class ScrollHandlingView(val vc: AbstractTerminalViewController<*, *, *>): View() {
override fun mouseScrolled(point: Point, amount: Double): Boolean {
var newOffsetInRows = vc.handler.currentScrollOffsetInRows() - amount.toInt()
newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, vc.handler.maxScrollOffsetInRows())
if (newOffsetInRows != vc.handler.currentScrollOffsetInRows()) {
val newScrollPosition = newOffsetInRows / vc.handler.maxScrollOffsetInRows().toDouble()
vc.screen.scrollPosition = newScrollPosition
vc.scrollTrack.scrollPosition = newScrollPosition
vc.screen.requestUpdatedItems()
}
return true
}
}
}

View File

@ -0,0 +1,18 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.util.Identifier
import net.minecraft.world.BlockView
import net.shadowfacts.phycon.PhysicalConnectivity
/**
* @author shadowfacts
*/
class CraftingTerminalBlock: AbstractTerminalBlock<CraftingTerminalBlockEntity>() {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "crafting_terminal")
}
override fun createBlockEntity(world: BlockView) = CraftingTerminalBlockEntity()
}

View File

@ -0,0 +1,141 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import it.unimi.dsi.fastutil.objects.Object2IntMap
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.CraftingInventory
import net.minecraft.inventory.SimpleInventory
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.Text
import net.minecraft.text.TranslatableText
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.ItemStackPacket
import net.shadowfacts.phycon.packet.LocateStackPacket
import net.shadowfacts.phycon.packet.RequestInventoryPacket
import net.shadowfacts.phycon.util.fromTag
import net.shadowfacts.phycon.util.toTag
import java.util.LinkedList
import kotlin.math.min
/**
* @author shadowfacts
*/
class CraftingTerminalBlockEntity: AbstractTerminalBlockEntity(PhyBlockEntities.CRAFTING_TERMINAL) {
val craftingInv = SimpleInventory(9)
private val completedCraftingStackRequests = LinkedList<CraftingStackLocateRequest>()
override fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) {
updateAndSync()
inventoryCache.clear()
sendPacket(RequestInventoryPacket(ipAddress))
val factory = object: ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? {
return CraftingTerminalScreenHandler(syncId, playerInv, this@CraftingTerminalBlockEntity)
}
override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal")
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos)
}
}
player.openHandledScreen(factory)
}
}
fun requestItemsForCrafting(maxAmount: Int) {
val amounts = ItemStackCollections.map<IntArray>()
for (i in 0 until craftingInv.size()) {
val craftingInvStack = craftingInv.getStack(i)
if (craftingInvStack.isEmpty) continue
if (craftingInvStack.count >= craftingInvStack.maxCount) continue
if (craftingInvStack !in amounts) amounts[craftingInvStack] = IntArray(9) { 0 }
amounts[craftingInvStack]!![i] = min(maxAmount, craftingInvStack.maxCount - craftingInvStack.count)
}
for ((stack, amountPerSlot) in amounts) {
val total = amountPerSlot.sum()
val request = CraftingStackLocateRequest(stack, total, counter, amountPerSlot)
pendingRequests.add(request)
sendPacket(LocateStackPacket(stack, ipAddress))
}
}
override fun stackLocateRequestCompleted(request: StackLocateRequest) {
if (request is CraftingStackLocateRequest) {
completedCraftingStackRequests.add(request)
}
super.stackLocateRequestCompleted(request)
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val craftingReq = completedCraftingStackRequests.find { ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) }
if (craftingReq != null) {
var remaining = packet.stack.copy()
for (i in 0 until craftingInv.size()) {
val currentStack = craftingInv.getStack(i)
if (currentStack.count >= currentStack.maxCount) continue
if (!ItemStackUtil.areEqualIgnoreAmounts(currentStack, remaining)) continue
val toInsert = minOf(remaining.count, currentStack.maxCount - currentStack.count, craftingReq.amountPerSlot[i])
currentStack.count += toInsert
remaining.count -= toInsert
craftingReq.amountPerSlot[i] -= toInsert
craftingReq.received += toInsert
if (remaining.isEmpty) {
break
}
}
if (craftingReq.amountPerSlot.sum() == 0 || craftingReq.received >= craftingReq.totalResultAmount) {
completedCraftingStackRequests.remove(craftingReq)
}
if (!remaining.isEmpty) {
remaining = internalBuffer.insert(remaining, TerminalBufferInventory.Mode.FROM_NETWORK)
}
updateAndSync()
return remaining
} else {
return super.doHandleItemStack(packet)
}
}
override fun toCommonTag(tag: CompoundTag) {
super.toCommonTag(tag)
tag.put("CraftingInv", craftingInv.toTag())
}
override fun fromCommonTag(tag: CompoundTag) {
super.fromCommonTag(tag)
craftingInv.fromTag(tag.getList("CraftingInv", 10))
}
class CraftingStackLocateRequest(
stack: ItemStack,
amount: Int,
timestamp: Long,
val amountPerSlot: IntArray,
): StackLocateRequest(stack, amount, timestamp) {
var received: Int = 0
}
}

View File

@ -0,0 +1,48 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
/**
* @author shadowfacts
*/
class CraftingTerminalScreen(
handler: CraftingTerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
): AbstractTerminalScreen<CraftingTerminalBlockEntity, CraftingTerminalScreenHandler>(
handler,
playerInv,
title,
259,
252,
) {
companion object {
private val BACKGROUND_1 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_1.png")
private val BACKGROUND_2 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_2.png")
}
override val backgroundTexture = BACKGROUND_1
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return CraftingTerminalViewController(this, handler)
}
override fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(BACKGROUND_1)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, 256, 252)
client!!.textureManager.bindTexture(BACKGROUND_2)
drawTexture(matrixStack, x + 256, y, 0, 0, 3, 252)
}
}

View File

@ -0,0 +1,130 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.ItemStackCollections
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.CraftingInventory
import net.minecraft.inventory.CraftingResultInventory
import net.minecraft.inventory.Inventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket
import net.minecraft.recipe.RecipeFinder
import net.minecraft.recipe.RecipeType
import net.minecraft.screen.slot.CraftingResultSlot
import net.minecraft.screen.slot.Slot
import net.minecraft.server.network.ServerPlayerEntity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
/**
* @author shadowfacts
*/
class CraftingTerminalScreenHandler(
syncId: Int,
playerInv: PlayerInventory,
terminal: CraftingTerminalBlockEntity,
): AbstractTerminalScreenHandler<CraftingTerminalBlockEntity>(PhyScreens.CRAFTING_TERMINAL, syncId, playerInv, terminal) {
val craftingInv = CraftingInv(this)
val result = CraftingResultInventory()
val resultSlot: CraftingResultSlot
override val xOffset: Int
get() = 5
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf):
this(
syncId,
playerInv,
PhyBlocks.CRAFTING_TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
)
init {
for (y in 0 until 3) {
for (x in 0 until 3) {
this.addSlot(Slot(craftingInv, x + y * 3, 13 + x * 18, 140 + y * 18))
}
}
resultSlot = CraftingResultSlot(playerInv.player, craftingInv, result, 0, 31, 224)
addSlot(resultSlot)
updateCraftingResult()
}
override fun onContentChanged(inventory: Inventory?) {
updateCraftingResult()
}
private fun updateCraftingResult() {
val world = playerInv.player.world
if (!world.isClient) {
val player = playerInv.player as ServerPlayerEntity
val recipe = world.server!!.recipeManager.getFirstMatch(RecipeType.CRAFTING, craftingInv, world)
val resultStack =
if (recipe.isPresent && result.shouldCraftRecipe(world, player, recipe.get())) {
recipe.get().craft(craftingInv)
} else {
ItemStack.EMPTY
}
result.setStack(0, resultStack)
player.networkHandler.sendPacket(ScreenHandlerSlotUpdateS2CPacket(syncId, resultSlot.id, resultStack))
}
}
fun clearCraftingGrid() {
assert(!playerInv.player.world.isClient)
for (i in 0 until terminal.craftingInv.size()) {
val craftingInvStack = terminal.craftingInv.getStack(i)
if (craftingInvStack.isEmpty) continue
val remainder = terminal.internalBuffer.insert(craftingInvStack, TerminalBufferInventory.Mode.TO_NETWORK)
terminal.craftingInv.setStack(i, remainder)
}
updateCraftingResult()
sendContentUpdates()
}
fun requestMoreCraftingIngredients(maxAmount: Int) {
assert(!playerInv.player.world.isClient)
terminal.requestItemsForCrafting(maxAmount)
}
// RecipeType.CRAFTING wants a CraftingInventory, but we can't store a CraftingInventory on the BE without a screen handler, so...
class CraftingInv(val handler: CraftingTerminalScreenHandler): CraftingInventory(handler, 3, 3) {
private val backing = handler.terminal.craftingInv
override fun isEmpty(): Boolean {
return backing.isEmpty
}
override fun getStack(i: Int): ItemStack {
return backing.getStack(i)
}
override fun removeStack(i: Int): ItemStack {
return backing.removeStack(i)
}
override fun removeStack(i: Int, j: Int): ItemStack {
val res = backing.removeStack(i, j)
if (!res.isEmpty) {
handler.onContentChanged(this)
}
return res
}
override fun setStack(i: Int, itemStack: ItemStack?) {
backing.setStack(i, itemStack)
handler.onContentChanged(this)
}
override fun clear() {
backing.clear()
}
override fun provideRecipeInputs(finder: RecipeFinder) {
TODO()
}
}
}

View File

@ -0,0 +1,100 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.client.MinecraftClient
import net.minecraft.client.util.InputUtil
import net.minecraft.text.TranslatableText
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.LayoutGuide
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.networking.C2STerminalCraftingButton
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class CraftingTerminalViewController(
screen: CraftingTerminalScreen,
handler: CraftingTerminalScreenHandler,
): AbstractTerminalViewController<CraftingTerminalBlockEntity, CraftingTerminalScreen, CraftingTerminalScreenHandler>(
screen,
handler,
) {
companion object {
val SMALL_BUTTON = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 0, 48)
val SMALL_BUTTON_HOVERED = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 16, 48)
val CLEAR_ICON = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 32, 48)
val PLUS_ICON = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 48, 48)
}
lateinit var craftingInv: LayoutGuide
override fun viewDidLoad() {
super.viewDidLoad()
craftingInv = view.addLayoutGuide()
view.solver.dsl {
craftingInv.leftAnchor equalTo buffer.leftAnchor
craftingInv.topAnchor equalTo playerInv.topAnchor
craftingInv.widthAnchor equalTo buffer.widthAnchor
craftingInv.heightAnchor equalTo 54
}
val craftingLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_crafting"))).apply {
textColor = Color.TEXT
}
view.solver.dsl {
craftingLabel.leftAnchor equalTo craftingInv.leftAnchor
craftingLabel.topAnchor equalTo playerInvLabel.topAnchor
}
val clearIcon = TextureView(CLEAR_ICON).apply {
intrinsicContentSize = Size(3.0,3.0)
}
val clearButton = view.addSubview(Button(clearIcon, padding = 2.0, handler = ::clearPressed)).apply {
background = TextureView(SMALL_BUTTON)
hoveredBackground = TextureView(SMALL_BUTTON_HOVERED)
tooltip = TranslatableText("gui.phycon.terminal.clear_crafting")
}
view.solver.dsl {
clearButton.topAnchor equalTo craftingInv.topAnchor
clearButton.leftAnchor equalTo (pane.leftAnchor + 4)
}
val plusIcon = TextureView(PLUS_ICON).apply {
intrinsicContentSize = Size(3.0, 3.0)
}
val plusButton = view.addSubview(Button(plusIcon, padding = 2.0, handler = ::plusPressed)).apply {
background= TextureView(SMALL_BUTTON)
hoveredBackground = TextureView(SMALL_BUTTON_HOVERED)
tooltip = TranslatableText("gui.phycon.terminal.more_crafting")
}
view.solver.dsl {
plusButton.topAnchor equalTo (clearButton.bottomAnchor + 2)
plusButton.leftAnchor equalTo clearButton.leftAnchor
}
}
private fun clearPressed(button: Button) {
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2STerminalCraftingButton(terminal, C2STerminalCraftingButton.Action.CLEAR_GRID))
}
private fun plusPressed(button: Button) {
val client = MinecraftClient.getInstance()
val action =
if (InputUtil.isKeyPressed(client.window.handle, GLFW.GLFW_KEY_LEFT_SHIFT) || InputUtil.isKeyPressed(client.window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT)) {
C2STerminalCraftingButton.Action.REQUEST_MAX_MORE
} else {
C2STerminalCraftingButton.Action.REQUEST_ONE_MORE
}
client.player!!.networkHandler.sendPacket(C2STerminalCraftingButton(terminal, action))
}
}

View File

@ -1,6 +1,5 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.EnumHelper
@ -31,14 +30,15 @@ class SettingButton<E>(
get() = content as TextureView
init {
updateTexture()
update()
}
private fun updateTexture() {
private fun update() {
textureView.texture = textureCache.getOrPut(key.value) {
val uv = key.value.uv
Texture(key.value.iconTexture, uv[0], uv[1])
}
tooltip = key.value.tooltip
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
@ -53,20 +53,10 @@ class SettingButton<E>(
PhysicalConnectivityClient.terminalSettings[key] = newValue
updateTexture()
update()
}
return super.mouseClicked(point, mouseButton)
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
super.draw(matrixStack, mouse, delta)
if (mouse in bounds) {
key.value.tooltip?.also {
window!!.drawTooltip(listOf(it))
}
}
}
}

View File

@ -1,80 +1,18 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.server.world.ServerWorld
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
import net.minecraft.util.ItemScatterer
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet
/**
* @author shadowfacts
*/
class TerminalBlock: DeviceBlock<TerminalBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
class TerminalBlock: AbstractTerminalBlock<TerminalBlockEntity>() {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal")
val FACING = Properties.FACING
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.allOf(Direction::class.java)
set.remove(state[FACING])
return set
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return defaultState.with(FACING, context.playerLookDirection.opposite)
}
override fun createBlockEntity(world: BlockView) = TerminalBlockEntity()
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult {
getBlockEntity(world, pos)!!.onActivate(player)
return ActionResult.SUCCESS
}
override fun onStateReplaced(state: BlockState, world: World, pos: BlockPos, newState: BlockState, moved: Boolean) {
if (!state.isOf(newState.block)) {
val be = getBlockEntity(world, pos)!!
ItemScatterer.spawn(world, pos, be.internalBuffer)
super.onStateReplaced(state, world, pos, newState, moved)
}
}
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) {
to.offer(getBlockEntity(world, pos))
}
}

View File

@ -1,175 +1,21 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.Inventory
import net.minecraft.inventory.InventoryChangedListener
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.TranslatableText
import net.minecraft.util.Tickable
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
import net.shadowfacts.phycon.packet.RequestInventoryPacket
/**
* @author shadowfacts
*/
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
InventoryChangedListener,
BlockEntityClientSerializable,
Tickable,
ItemStackPacketHandler,
NetworkStackDispatcher<TerminalBlockEntity.PendingInsertion> {
class TerminalBlockEntity: AbstractTerminalBlockEntity(PhyBlockEntities.TERMINAL) {
companion object {
// the locate/insertion timeouts are only 1 tick because that's long enough to hear from every device on the network
// in a degraded state (when there's latency in the network), not handling interface priorities correctly is acceptable
val LOCATE_REQUEST_TIMEOUT: Long = 1 // ticks
val INSERTION_TIMEOUT: Long = 1
}
private val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
val internalBuffer = TerminalBufferInventory(18)
private val pendingRequests = LinkedList<StackLocateRequest>()
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = INSERTION_TIMEOUT
private var observers = 0
val cachedNetItems = ItemStackCollections.intMap()
// todo: multiple players could have the terminal open simultaneously
var netItemObserver: WeakReference<NetItemObserver>? = null
init {
internalBuffer.addListener(this)
}
override fun findDestination(): Interface? {
for (dir in Direction.values()) {
val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir)
if (itf != null) {
return itf
}
}
return null
}
override fun handle(packet: Packet) {
when (packet) {
is ReadInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
is CapacityPacket -> handleCapacity(packet)
}
}
private fun handleReadInventory(packet: ReadInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
updateAndSync()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
updateAndSync()
}
private fun handleStackLocation(packet: StackLocationPacket) {
val request = pendingRequests.firstOrNull {
ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack)
}
if (request != null) {
request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) {
stackLocateRequestCompleted(request)
}
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val remaining = internalBuffer.insertFromNetwork(packet.stack)
// this happens outside the normal update loop because by receiving the item stack packet
// we "know" how much the count in the source inventory has changed
updateAndSync()
return remaining
}
private fun updateAndSync() {
updateNetItems()
// syncs the internal buffer to the client
sync()
// syncs the open container (if any) to the client
netItemObserver?.get()?.netItemsChanged()
}
private fun updateNetItems() {
cachedNetItems.clear()
for (inventory in inventoryCache.values) {
for (stack in inventory.storedStacks) {
val amount = inventory.getAmount(stack)
cachedNetItems.mergeInt(stack, amount) { a, b -> a + b }
}
}
}
private fun beginInsertions() {
if (world!!.isClient) return
for (slot in 0 until internalBuffer.size()) {
if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue
if (pendingInsertions.any { it.bufferSlot == slot }) continue
val stack = internalBuffer.getStack(slot)
dispatchItemStack(stack) { insertion ->
insertion.bufferSlot = slot
}
}
}
private fun finishPendingRequests() {
if (world!!.isClient) return
if (pendingRequests.isEmpty()) return
val finishable = pendingRequests.filter { it.isFinishable(counter) }
// stackLocateRequestCompleted removes the object from pendingRequests
finishable.forEach(::stackLocateRequestCompleted)
}
override fun tick() {
super.tick()
if (!world!!.isClient) {
finishPendingRequests()
finishTimedOutPendingInsertions()
}
if (counter % 20 == 0L && !world!!.isClient) {
beginInsertions()
}
}
fun onActivate(player: PlayerEntity) {
override fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) {
updateAndSync()
@ -190,85 +36,4 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
}
}
fun requestItem(stack: ItemStack, amount: Int = stack.count) {
val request = StackLocateRequest(stack, amount, counter)
pendingRequests.add(request)
// locate packets are sent immediately instead of being added to a queue
// otherwise the terminal UI feels sluggish and unresponsive
sendPacket(LocateStackPacket(stack, ipAddress))
}
private fun stackLocateRequestCompleted(request: StackLocateRequest) {
pendingRequests.remove(request)
val sortedResults = request.results.toMutableList()
sortedResults.sortWith { a, b ->
// sort results first by provider priority, and then by the count that it can provide
if (a.second.providerPriority == b.second.providerPriority) {
b.first - a.first
} else {
b.second.providerPriority - a.second.providerPriority
}
}
var amountRequested = 0
while (amountRequested < request.amount && sortedResults.isNotEmpty()) {
val (sourceAmount, sourceInterface) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, request.amount - amountRequested)
amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress))
}
}
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun finishInsertion(insertion: PendingInsertion): ItemStack {
val remaining = super.finishInsertion(insertion)
internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateAndSync()
return remaining
}
override fun onInventoryChanged(inv: Inventory) {
if (inv == internalBuffer && world != null && !world!!.isClient) {
markDirty()
sync()
}
}
override fun toCommonTag(tag: CompoundTag) {
super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag())
}
override fun fromCommonTag(tag: CompoundTag) {
super.fromCommonTag(tag)
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
}
interface NetItemObserver {
fun netItemsChanged()
}
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
var bufferSlot by Delegates.notNull<Int>()
}
}
data class StackLocateRequest(
val stack: ItemStack,
val amount: Int,
val timestamp: Long,
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
) {
val totalResultAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
fun isFinishable(currentTimestamp: Long): Boolean {
// we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to
// correctly sort by priority
return currentTimestamp - timestamp >= TerminalBlockEntity.LOCATE_REQUEST_TIMEOUT
}
}

View File

@ -34,12 +34,12 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) {
tag.getIntArray("Modes").forEachIndexed { i, it -> modes[i] = Mode.values()[it] }
}
fun insertFromNetwork(stack: ItemStack): ItemStack {
fun insert(stack: ItemStack, mode: Mode): ItemStack {
var remaining = stack.copy()
for (slot in 0 until size()) {
if (modes[slot] == Mode.TO_NETWORK) continue
if (modes[slot] != mode && modes[slot] != Mode.UNASSIGNED) continue
remaining = insertFromNetwork(stack, slot)
remaining = tryInsert(stack, slot, mode)
if (remaining.isEmpty) {
break
@ -48,19 +48,17 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) {
return remaining
}
private fun insertFromNetwork(stack: ItemStack, slot: Int): ItemStack {
val mode = modes[slot]
if (mode == Mode.TO_NETWORK) return stack
private fun tryInsert(stack: ItemStack, slot: Int, mode: Mode): ItemStack {
val current = getStack(slot)
if (current.isEmpty) {
setStack(slot, stack)
modes[slot] = Mode.FROM_NETWORK
markSlot(slot, mode)
return ItemStack.EMPTY
} else if (ItemStackUtil.areEqualIgnoreAmounts(stack, current)) {
val toTransfer = min(current.maxCount - current.count, stack.count)
current.count += toTransfer
stack.count -= toTransfer
modes[slot] = Mode.FROM_NETWORK
markSlot(slot, mode)
return stack
} else {
return stack

View File

@ -23,7 +23,7 @@ class TerminalFakeSlot(fakeInv: FakeInventory, slot: Int, x: Int, y: Int): Slot(
}
class FakeInventory(val screenHandler: TerminalScreenHandler): Inventory {
class FakeInventory(val screenHandler: AbstractTerminalScreenHandler<*>): Inventory {
override fun getStack(slot: Int): ItemStack {
if (slot >= screenHandler.itemsForDisplay.size) return ItemStack.EMPTY
return screenHandler.itemsForDisplay[slot]

View File

@ -19,7 +19,7 @@ import kotlin.math.floor
* @author shadowfacts
*/
class TerminalRequestAmountViewController(
val screen: TerminalScreen,
val screen: AbstractTerminalScreen<*, *>,
val stack: ItemStack,
): ViewController() {

View File

@ -1,194 +1,33 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.Element
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoHandledScreen
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.LinkedList
import kotlin.math.ceil
import kotlin.math.min
/**
* @author shadowfacts
*/
class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): CacaoHandledScreen<TerminalScreenHandler>(handler, playerInv, title) {
class TerminalScreen(
handler: TerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
): AbstractTerminalScreen<TerminalBlockEntity, TerminalScreenHandler>(
handler,
playerInv,
title,
252,
222
) {
companion object {
private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png")
private val clickHandlers = LinkedList<TerminalScreen.(Double, Double, Int) -> Boolean?>()
fun registerClickHandler(handler: TerminalScreen.(Double, Double, Int) -> Boolean?) {
clickHandlers.add(handler)
}
}
val backgroundWidth: Int
get() = backgroundWidth
val backgroundHeight: Int
get() = backgroundHeight
override val backgroundTexture = BACKGROUND
val terminalVC = TerminalViewController(this, handler, handler.terminal)
var amountVC: TerminalRequestAmountViewController? = null
var searchQuery = ""
var scrollPosition = 0.0
init {
backgroundWidth = 252
backgroundHeight = 222
addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems()
}
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat()))
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int, slot: Slot, matrixStack: MatrixStack) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
// draw damage bar
// empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
matrixStack.push()
matrixStack.translate(x.toDouble(), y.toDouble(), itemRenderer.zOffset + 200.0)
val scale = 2 / 3f
matrixStack.scale(scale, scale, 1.0f)
val immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().buffer)
val textX = (1 / scale * 18) - textRenderer.getWidth(s).toFloat() - 3
val textY = (1 / scale * 18) - 11
textRenderer.draw(s, textX, textY, 0xffffff, true, matrixStack.peek().model, immediate, false, 0, 0xF000F0)
immediate.draw()
matrixStack.pop()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
override fun tick() {
super.tick()
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
for (handler in clickHandlers) {
val res = handler(mouseX, mouseY, button)
if (res != null) {
return res
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) {
val stack = slot.stack
if (type == SlotActionType.QUICK_MOVE) {
// shift click, request full stack
requestItem(stack, min(stack.count, stack.maxCount))
} else if (type == SlotActionType.PICKUP) {
if (clickData == 1) {
// right click, request half stack
requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
} else {
showRequestAmountDialog(stack)
}
}
}
}
private val fakeFocusedElement = TextFieldWidget(textRenderer, 0, 0, 0, 0, LiteralText(""))
override fun getFocused(): Element? {
return if (windows.last().firstResponder != null) {
fakeFocusedElement
} else {
null
}
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return TerminalViewController(this, handler)
}
}

View File

@ -1,62 +1,18 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.DefaultPlugin
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.copyWithCount
import java.lang.ref.WeakReference
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
class TerminalScreenHandler(
syncId: Int,
val playerInv: PlayerInventory,
val terminal: TerminalBlockEntity,
): ScreenHandler(PhyScreens.TERMINAL, syncId),
TerminalBlockEntity.NetItemObserver {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal")
}
private val rowsDisplayed = 6
private val fakeInv = FakeInventory(this)
private var searchQuery: String = ""
private var settings = TerminalSettings()
var totalEntries = 0
private set
var scrollPosition = 0f
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
it.stack.copyWithCount(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
playerInv: PlayerInventory,
terminal: TerminalBlockEntity,
): AbstractTerminalScreenHandler<TerminalBlockEntity>(PhyScreens.TERMINAL, syncId, playerInv, terminal) {
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf):
this(
@ -65,195 +21,4 @@ class TerminalScreenHandler(
PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
)
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
// intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings
}
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, 66 + x * 18, 18 + y * 18))
}
}
// internal buffer
for (y in 0 until 6) {
for (x in 0 until 3) {
addSlot(Slot(terminal.internalBuffer, y * 3 + x, 8 + x * 18, 18 + y * 18))
}
}
// player inv
for (y in 0 until 3) {
for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, 66 + x * 18, 140 + y * 18))
}
}
// hotbar
for (x in 0 until 9) {
addSlot(Slot(playerInv, x, 66 + x * 18, 198))
}
}
override fun netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1)
val key = Registry.ITEM.getKey(it.key.item)
if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) {
return@filter true
}
}
it.key.name.string.contains(searchQuery, true)
}
totalEntries = filtered.size
val sorted =
when (settings[DefaultPlugin.SORT_MODE]) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
val offsetInItems = currentScrollOffsetInItems()
val end = min(offsetInItems + rowsDisplayed * 9, sorted.size)
itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) }
// itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, settings, scrollPosition, totalEntries))
}
fun totalRows(): Int {
return ceil(totalEntries / 9f).toInt()
}
fun maxScrollOffsetInRows(): Int {
return totalRows() - rowsDisplayed
}
fun currentScrollOffsetInRows(): Int {
return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt())
}
fun currentScrollOffsetInItems(): Int {
return currentScrollOffsetInRows() * 9
}
fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, settings: TerminalSettings, scrollPosition: Float) {
this.searchQuery = query
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
terminal.netItemObserver = null
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
if (isBufferSlot(slotId)) {
// todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) {
// placing cursor stack into buffer
val bufferSlot = slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index
terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK)
}
}
return super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY;
}
val slot = slots[slotId]
if (!slot.hasStack()) {
return ItemStack.EMPTY
}
val result = slot.stack.copy()
if (isBufferSlot(slotId)) {
// last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
if (slot.stack.isEmpty) {
terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED)
}
} else if (isPlayerSlot(slotId)) {
val slotsInsertedInto = tryInsertItem(slot.stack, bufferSlotsStart until playerSlotsStart) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK }
slotsInsertedInto.forEach { terminal.internalBuffer.markSlot(it - bufferSlotsStart, TerminalBufferInventory.Mode.TO_NETWORK) }
if (slot.stack.isEmpty) {
return ItemStack.EMPTY
}
}
return result
}
private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection<Int> {
val slotsInsertedInto = mutableListOf<Int>()
for (index in slots) {
if (stack.isEmpty) break
if (!slotPredicate(index)) continue
val slot = this.slots[index]
val slotStack = slot.stack
if (slotStack.isEmpty) {
slot.stack = stack.copy()
stack.count = 0
slot.markDirty()
slotsInsertedInto.add(index)
} else if (canStacksCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
val maxToMove = slotStack.maxCount - slotStack.count
val toMove = min(maxToMove, stack.count)
slotStack.increment(toMove)
stack.decrement(toMove)
slot.markDirty()
slotsInsertedInto.add(index)
}
}
return slotsInsertedInto
}
val networkSlotsStart = 0
val networkSlotsEnd = 54
val bufferSlotsStart = 54
val bufferSlotsEnd = 72
val playerSlotsStart = 72
val playerSlotsEnd = 72 + 36
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
}

View File

@ -1,164 +1,13 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.MathHelper
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
/**
* @author shadowfacts
*/
class TerminalViewController(
val screen: TerminalScreen,
val handler: TerminalScreenHandler,
val terminal: TerminalBlockEntity,
): ViewController() {
private lateinit var scrollTrack: ScrollTrackView
lateinit var settingsView: View
private set
lateinit var searchField: TextField
private set
override fun loadView() {
view = ScrollHandlingView(this)
}
override fun viewDidLoad() {
super.viewDidLoad()
val pane = view.addLayoutGuide()
view.solver.dsl {
pane.centerXAnchor equalTo view.centerXAnchor
pane.centerYAnchor equalTo view.centerYAnchor
pane.widthAnchor equalTo screen.backgroundWidth
pane.heightAnchor equalTo screen.backgroundHeight
}
val buffer = view.addLayoutGuide()
view.solver.dsl {
buffer.leftAnchor equalTo (pane.leftAnchor + 7)
buffer.topAnchor equalTo (pane.topAnchor + 17)
buffer.widthAnchor equalTo (18 * 3)
buffer.heightAnchor equalTo (18 * 6)
}
val network = view.addLayoutGuide()
view.solver.dsl {
network.leftAnchor equalTo (pane.leftAnchor + 65)
network.topAnchor equalTo buffer.topAnchor
network.widthAnchor equalTo (18 * 9)
network.heightAnchor equalTo (18 * 6)
}
val playerInv = view.addLayoutGuide()
view.solver.dsl {
playerInv.leftAnchor equalTo network.leftAnchor
playerInv.topAnchor equalTo (pane.topAnchor + 139)
playerInv.widthAnchor equalTo (18 * 9)
playerInv.heightAnchor equalTo 76
}
val titleLabel = view.addSubview(Label(screen.title)).apply {
textColor = Color.TEXT
}
val playerInvLabel = view.addSubview(Label(handler.playerInv.displayName)).apply {
textColor = Color.TEXT
}
val bufferLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_buffer"))).apply {
textColor = Color.TEXT
}
searchField = view.addSubview(TerminalSearchField()).apply {
handler = ::searchFieldChanged
drawBackground = false
}
searchField.becomeFirstResponder()
scrollTrack = view.addSubview(ScrollTrackView(::scrollPositionChanged))
val settingsStack = view.addSubview(StackView(Axis.VERTICAL, spacing = 2.0))
settingsView = settingsStack
TerminalSettings.allKeys.forEach { key ->
val button = SettingButton(key)
button.handler = { settingsChanged() }
settingsStack.addArrangedSubview(button)
}
view.solver.dsl {
titleLabel.leftAnchor equalTo network.leftAnchor
titleLabel.topAnchor equalTo (pane.topAnchor + 6)
bufferLabel.leftAnchor equalTo buffer.leftAnchor
bufferLabel.topAnchor equalTo titleLabel.topAnchor
playerInvLabel.leftAnchor equalTo playerInv.leftAnchor
playerInvLabel.topAnchor equalTo (pane.bottomAnchor - 94)
searchField.leftAnchor equalTo (pane.leftAnchor + 138)
searchField.topAnchor equalTo (pane.topAnchor + 5)
searchField.widthAnchor equalTo 80
searchField.heightAnchor equalTo 9
scrollTrack.leftAnchor equalTo (pane.leftAnchor + 232)
scrollTrack.topAnchor equalTo (network.topAnchor + 1)
scrollTrack.bottomAnchor equalTo (network.bottomAnchor - 1)
scrollTrack.widthAnchor equalTo 12
settingsStack.leftAnchor equalTo (pane.rightAnchor + 4)
settingsStack.topAnchor equalTo pane.topAnchor
}
}
private fun searchFieldChanged(field: TextField) {
screen.searchQuery = field.text
screen.requestUpdatedItems()
}
private fun scrollPositionChanged(track: ScrollTrackView) {
val oldOffset = handler.currentScrollOffsetInRows()
handler.scrollPosition = track.scrollPosition.toFloat()
screen.scrollPosition = track.scrollPosition
if (handler.currentScrollOffsetInRows() != oldOffset) {
screen.requestUpdatedItems()
}
}
private fun settingsChanged() {
screen.requestUpdatedItems()
}
class TerminalSearchField: TextField("") {
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
// no-op
}
}
class ScrollHandlingView(val vc: TerminalViewController): View() {
override fun mouseScrolled(point: Point, amount: Double): Boolean {
var newOffsetInRows = vc.handler.currentScrollOffsetInRows() - amount.toInt()
newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, vc.handler.maxScrollOffsetInRows())
if (newOffsetInRows != vc.handler.currentScrollOffsetInRows()) {
val newScrollPosition = newOffsetInRows / vc.handler.maxScrollOffsetInRows().toDouble()
vc.screen.scrollPosition = newScrollPosition
vc.scrollTrack.scrollPosition = newScrollPosition
vc.screen.requestUpdatedItems()
}
return true
}
}
screen: TerminalScreen,
handler: TerminalScreenHandler,
): AbstractTerminalViewController<TerminalBlockEntity, TerminalScreen, TerminalScreenHandler>(
screen,
handler,
) {
}

View File

@ -6,7 +6,7 @@ import net.minecraft.client.render.model.UnbakedModel
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.client.model.TerminalModel
import net.shadowfacts.phycon.client.model.ScreenDeviceModel
/**
* @author shadowfacts
@ -15,11 +15,13 @@ class PhyExtendedModelProvider(resourceManager: ResourceManager): ModelResourceP
companion object {
val TERMINAL = Identifier(PhysicalConnectivity.MODID, "block/terminal")
val CRAFTING_TERMINAL = Identifier(PhysicalConnectivity.MODID, "block/crafting_terminal")
}
override fun loadModelResource(resourceId: Identifier, context: ModelProviderContext): UnbakedModel? {
return when (resourceId) {
TERMINAL -> TerminalModel
TERMINAL -> ScreenDeviceModel(TERMINAL)
CRAFTING_TERMINAL -> ScreenDeviceModel(CRAFTING_TERMINAL)
else -> null
}
}

View File

@ -19,7 +19,7 @@ import net.minecraft.util.math.Direction
import net.minecraft.world.BlockRenderView
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlock
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlock
import java.util.Random
import java.util.function.Function
import java.util.function.Supplier
@ -27,13 +27,18 @@ import java.util.function.Supplier
/**
* @author shadowfacts
*/
object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
class ScreenDeviceModel(
screenTexture: Identifier,
): UnbakedModel, BakedModel, FabricBakedModel {
private val TERMINAL = SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, Identifier(PhysicalConnectivity.MODID, "block/terminal"))
private val CASING = SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, Identifier(PhysicalConnectivity.MODID, "block/casing"))
companion object {
private val CASING = SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, Identifier(PhysicalConnectivity.MODID, "block/casing"))
}
private val screenTexture = SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, screenTexture)
private lateinit var meshes: Array<Mesh>
private lateinit var terminalSprite: Sprite
private lateinit var screenSprite: Sprite
override fun getModelDependencies(): Collection<Identifier> {
return listOf()
@ -43,7 +48,7 @@ object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
unbakedModelGetter: Function<Identifier, UnbakedModel>,
unresolvedTextureDependencies: MutableSet<Pair<String, String>>
): Collection<SpriteIdentifier> {
return listOf(TERMINAL, CASING)
return listOf(screenTexture, CASING)
}
override fun bake(
@ -53,7 +58,7 @@ object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
modelId: Identifier
): BakedModel {
terminalSprite = textureGetter.apply(TERMINAL)
screenSprite = textureGetter.apply(screenTexture)
val casingSprite = textureGetter.apply(CASING)
val renderer = RendererAccess.INSTANCE.renderer!!
@ -66,26 +71,63 @@ object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
for (dir in Direction.values()) {
if (dir == facing) {
emitter.square(facing, 0f, 0f, 1f, 1f, QuadEmitter.CULL_FACE_EPSILON * 10)
emitter.spriteBake(0, terminalSprite, MutableQuadView.BAKE_LOCK_UV)
// screen border
emitter.square(facing, 0f, 0f, 3/16f, 3/16f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 13/16f, 0f, 1f, 3/16f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 13/16f, 13/16f, 1f, 1f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 0f, 13/16f, 3/16f, 1f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 3/16f, 0f, 13/16f, 2/16f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 3/16f, 14/16f, 13/16f, 1f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 0f, 3/16f, 2/16f, 13/16f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.square(facing, 14/16f, 3/16f, 1f, 13/16f, 0f)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
// screen
emitter.material(PhysicalConnectivityClient.screenMaterial)
emitter.square(facing, 3/16f, 2/16f, 13/16f, 3/16f, 0f)
emitter.spriteBake(0, terminalSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.material(PhysicalConnectivityClient.screenMaterial)
emitter.square(facing, 2/16f, 3/16f, 14/16f, 13/16f, 0f)
emitter.spriteBake(0, terminalSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
emitter.material(PhysicalConnectivityClient.screenMaterial)
emitter.square(facing, 3/16f, 13/16f, 13/16f, 14/16f, 0f)
emitter.spriteBake(0, terminalSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteBake(0, screenSprite, MutableQuadView.BAKE_LOCK_UV)
emitter.spriteColor(0, -1, -1, -1, -1)
emitter.emit()
} else {
@ -112,7 +154,7 @@ object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
randomSupplier: Supplier<Random>,
context: RenderContext
) {
val mesh = meshes[state[TerminalBlock.FACING].ordinal]
val mesh = meshes[state[AbstractTerminalBlock.FACING].ordinal]
context.meshConsumer().accept(mesh)
}
@ -129,7 +171,7 @@ object TerminalModel: UnbakedModel, BakedModel, FabricBakedModel {
override fun isBuiltin() = false
override fun getSprite() = terminalSprite
override fun getSprite() = screenSprite
override fun getTransformation() = null

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen
package net.shadowfacts.phycon.client.screen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.Identifier

View File

@ -1,7 +1,6 @@
package net.shadowfacts.phycon.screen
package net.shadowfacts.phycon.client.screen
import net.minecraft.client.gui.widget.AbstractPressableButtonWidget
import net.minecraft.text.Text
import net.shadowfacts.phycon.util.FriendlyNameable
import net.shadowfacts.phycon.util.RotatableEnum
import net.shadowfacts.phycon.util.next

View File

@ -1,10 +1,9 @@
package net.shadowfacts.phycon.block.terminal
package net.shadowfacts.phycon.client.screen
import net.minecraft.util.Identifier
import net.minecraft.util.math.MathHelper
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.TextureView

View File

@ -1,19 +1,11 @@
package net.shadowfacts.phycon.screen
package net.shadowfacts.phycon.client.screen
import net.minecraft.client.MinecraftClient
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.*
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.TabViewController
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.text.TranslatableText
import net.minecraft.util.Identifier

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText
@ -6,11 +6,10 @@ import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.screen.ClipboardButton
import net.shadowfacts.phycon.client.screen.ClipboardButton
/**
* @author shadowfacts

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText

View File

@ -1,13 +1,10 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.block.entity.BlockEntity
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.button.ToggleButton
import net.shadowfacts.cacao.view.textfield.NumberField
import net.shadowfacts.cacao.viewcontroller.ViewController

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.block.entity.BlockEntity
import net.minecraft.client.MinecraftClient

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.screen.console
package net.shadowfacts.phycon.client.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText

View File

@ -19,6 +19,8 @@ import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock
import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlockEntity
import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock
import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalBlock
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
@ -29,6 +31,7 @@ object PhyBlockEntities {
val INTERFACE = create(::InterfaceBlockEntity, PhyBlocks.INTERFACE)
val TERMINAL = create(::TerminalBlockEntity, PhyBlocks.TERMINAL)
val CRAFTING_TERMINAL = create(::CraftingTerminalBlockEntity, PhyBlocks.CRAFTING_TERMINAL)
val SWITCH = create(::SwitchBlockEntity, PhyBlocks.SWITCH)
val EXTRACTOR = create(::ExtractorBlockEntity, PhyBlocks.EXTRACTOR)
val INSERTER = create(::InserterBlockEntity, PhyBlocks.INSERTER)
@ -43,6 +46,7 @@ object PhyBlockEntities {
fun init() {
register(InterfaceBlock.ID, INTERFACE)
register(TerminalBlock.ID, TERMINAL)
register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL)
register(SwitchBlock.ID, SWITCH)
register(ExtractorBlock.ID, EXTRACTOR)
register(InserterBlock.ID, INSERTER)

View File

@ -13,6 +13,7 @@ import net.shadowfacts.phycon.block.netinterface.InterfaceBlock
import net.shadowfacts.phycon.block.netswitch.SwitchBlock
import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock
import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock
import net.shadowfacts.phycon.block.terminal.TerminalBlock
/**
@ -24,6 +25,7 @@ object PhyBlocks {
val INTERFACE = InterfaceBlock()
val TERMINAL = TerminalBlock()
val CRAFTING_TERMINAL = CraftingTerminalBlock()
val SWITCH = SwitchBlock()
val EXTRACTOR = ExtractorBlock()
val INSERTER = InserterBlock()
@ -38,6 +40,7 @@ object PhyBlocks {
register(InterfaceBlock.ID, INTERFACE)
register(TerminalBlock.ID, TERMINAL)
register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL)
register(SwitchBlock.ID, SWITCH)
register(ExtractorBlock.ID, EXTRACTOR)
register(InserterBlock.ID, INSERTER)

View File

@ -7,7 +7,6 @@ import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.item.ConsoleItem
import net.shadowfacts.phycon.item.ScrewdriverItem
import net.shadowfacts.phycon.block.cable.CableBlock
import net.shadowfacts.phycon.block.extractor.ExtractorBlock
import net.shadowfacts.phycon.block.inserter.InserterBlock
import net.shadowfacts.phycon.block.miner.MinerBlock
@ -15,6 +14,7 @@ import net.shadowfacts.phycon.block.netinterface.InterfaceBlock
import net.shadowfacts.phycon.block.netswitch.SwitchBlock
import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock
import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock
import net.shadowfacts.phycon.block.terminal.TerminalBlock
import net.shadowfacts.phycon.item.DeviceBlockItem
import net.shadowfacts.phycon.item.FaceDeviceBlockItem
@ -30,6 +30,7 @@ object PhyItems {
val INTERFACE = FaceDeviceBlockItem(PhyBlocks.INTERFACE, Item.Settings())
val TERMINAL = DeviceBlockItem(PhyBlocks.TERMINAL, Item.Settings())
val CRAFTING_TERMINAL = DeviceBlockItem(PhyBlocks.CRAFTING_TERMINAL, Item.Settings())
val SWITCH = BlockItem(PhyBlocks.SWITCH, Item.Settings())
val EXTRACTOR = FaceDeviceBlockItem(PhyBlocks.EXTRACTOR, Item.Settings())
val INSERTER = FaceDeviceBlockItem(PhyBlocks.INSERTER, Item.Settings())
@ -53,6 +54,7 @@ object PhyItems {
register(InterfaceBlock.ID, INTERFACE)
register(TerminalBlock.ID, TERMINAL)
register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL)
register(SwitchBlock.ID, SWITCH)
register(ExtractorBlock.ID, EXTRACTOR)
register(InserterBlock.ID, INSERTER)

View File

@ -4,19 +4,25 @@ import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry
import net.minecraft.screen.ScreenHandlerType
import net.shadowfacts.phycon.block.inserter.InserterScreenHandler
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreenHandler
import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock
import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreenHandler
import net.shadowfacts.phycon.block.terminal.TerminalBlock
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
object PhyScreens {
lateinit var TERMINAL: ScreenHandlerType<TerminalScreenHandler>
private set
lateinit var CRAFTING_TERMINAL: ScreenHandlerType<CraftingTerminalScreenHandler>
private set
lateinit var INSERTER: ScreenHandlerType<InserterScreenHandler>
private set
lateinit var REDSTONE_EMITTER: ScreenHandlerType<RedstoneEmitterScreenHandler>
private set
fun init() {
TERMINAL = ScreenHandlerRegistry.registerExtended(TerminalScreenHandler.ID, ::TerminalScreenHandler)
TERMINAL = ScreenHandlerRegistry.registerExtended(TerminalBlock.ID, ::TerminalScreenHandler)
CRAFTING_TERMINAL = ScreenHandlerRegistry.registerExtended(CraftingTerminalBlock.ID, ::CraftingTerminalScreenHandler)
INSERTER = ScreenHandlerRegistry.registerExtended(InserterScreenHandler.ID, ::InserterScreenHandler)
REDSTONE_EMITTER = ScreenHandlerRegistry.registerExtended(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler)
}

View File

@ -8,7 +8,7 @@ import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.DeviceBlock
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.screen.console.DeviceConsoleScreen
import net.shadowfacts.phycon.client.screen.console.DeviceConsoleScreen
/**
* @author shadowfacts

View File

@ -0,0 +1,56 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.fabricmc.fabric.api.networking.v1.PacketSender
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.minecraft.server.MinecraftServer
import net.minecraft.server.network.ServerPlayNetworkHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.minecraft.util.registry.RegistryKey
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreenHandler
/**
* @author shadowfacts
*/
object C2STerminalCraftingButton: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_crafting_button")
enum class Action {
CLEAR_GRID,
REQUEST_ONE_MORE,
REQUEST_MAX_MORE,
}
operator fun invoke(terminal: CraftingTerminalBlockEntity, action: Action): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
buf.writeByte(action.ordinal)
return createPacket(buf)
}
override fun receive(server: MinecraftServer, player: ServerPlayerEntity, handler: ServerPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val action = Action.values()[buf.readByte().toInt()]
server.execute {
val key = RegistryKey.of(Registry.DIMENSION, dimID)
val screenHandler = player.currentScreenHandler as? CraftingTerminalScreenHandler ?: return@execute
if (screenHandler.terminal.pos != pos || screenHandler.terminal.world!!.registryKey != key) return@execute
when (action) {
Action.CLEAR_GRID -> screenHandler.clearCraftingGrid()
Action.REQUEST_ONE_MORE -> screenHandler.requestMoreCraftingIngredients(1)
Action.REQUEST_MAX_MORE -> screenHandler.requestMoreCraftingIngredients(64)
}
}
}
}

View File

@ -13,8 +13,7 @@ import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.minecraft.util.registry.RegistryKey
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.util.copyWithCount
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.util.readItemStackWithoutCount
import net.shadowfacts.phycon.util.writeItemStackWithoutCount
@ -25,7 +24,7 @@ object C2STerminalRequestItem: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_request_item")
operator fun invoke(terminal: TerminalBlockEntity, stack: ItemStack, amount: Int): Packet<*> {
operator fun invoke(terminal: AbstractTerminalBlockEntity, stack: ItemStack, amount: Int): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
@ -48,7 +47,7 @@ object C2STerminalRequestItem: ServerReceiver {
server.execute {
val key = RegistryKey.of(Registry.DIMENSION, dimID)
val world = server.getWorld(key) ?: return@execute
val terminal = world.getBlockEntity(pos) as? TerminalBlockEntity ?: return@execute
val terminal = world.getBlockEntity(pos) as? AbstractTerminalBlockEntity ?: return@execute
terminal.requestItem(stack, amount)
}
}

View File

@ -11,9 +11,8 @@ import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler
import net.shadowfacts.phycon.util.TerminalSettings
/**
@ -23,7 +22,7 @@ object C2STerminalUpdateDisplayedItems: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_update_displayed")
operator fun invoke(terminal: TerminalBlockEntity, query: String, scrollPosition: Float): Packet<*> {
operator fun invoke(terminal: AbstractTerminalBlockEntity, query: String, scrollPosition: Float): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
@ -47,7 +46,7 @@ object C2STerminalUpdateDisplayedItems: ServerReceiver {
server.execute {
if (player.world.registryKey.value != dimID) return@execute
val screenHandler = player.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler !is AbstractTerminalScreenHandler<*>) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.sendUpdatedItemsToClient(player, query, settings, scrollPosition)
}

View File

@ -8,9 +8,8 @@ import net.minecraft.client.network.ClientPlayNetworkHandler
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.readItemStackWithoutCount
import net.shadowfacts.phycon.util.writeItemStackWithoutCount
@ -21,7 +20,7 @@ import net.shadowfacts.phycon.util.writeItemStackWithoutCount
object S2CTerminalUpdateDisplayedItems: ClientReceiver {
override val CHANNEL = C2STerminalUpdateDisplayedItems.CHANNEL
operator fun invoke(terminal: TerminalBlockEntity, entries: List<TerminalScreenHandler.Entry>, query: String, settings: TerminalSettings, scrollPosition: Float, totalEntries: Int): Packet<*> {
operator fun invoke(terminal: AbstractTerminalBlockEntity, entries: List<AbstractTerminalScreenHandler.Entry>, query: String, settings: TerminalSettings, scrollPosition: Float, totalEntries: Int): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
@ -45,9 +44,9 @@ object S2CTerminalUpdateDisplayedItems: ClientReceiver {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val entryCount = buf.readVarInt()
val entries = ArrayList<TerminalScreenHandler.Entry>(entryCount)
val entries = ArrayList<AbstractTerminalScreenHandler.Entry>(entryCount)
for (i in 0 until entryCount) {
entries.add(TerminalScreenHandler.Entry(buf.readItemStackWithoutCount(), buf.readVarInt()))
entries.add(AbstractTerminalScreenHandler.Entry(buf.readItemStackWithoutCount(), buf.readVarInt()))
}
val query = buf.readString()
PhysicalConnectivityClient.terminalSettings.fromTag(buf.readCompoundTag()!!)
@ -57,7 +56,7 @@ object S2CTerminalUpdateDisplayedItems: ClientReceiver {
client.execute {
if (client.player!!.world.registryKey.value != dimID) return@execute
val screenHandler = client.player!!.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler !is AbstractTerminalScreenHandler<*>) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.receivedUpdatedItemsFromServer(entries, query, scrollPosition, totalEntries)
}

View File

@ -0,0 +1,27 @@
{
"variants": {
"facing=down": {
"model": "phycon:block/crafting_terminal",
"x": 90
},
"facing=up": {
"model": "phycon:block/crafting_terminal",
"x": 270
},
"facing=north": {
"model": "phycon:block/crafting_terminal"
},
"facing=south": {
"model": "phycon:block/crafting_terminal",
"y": 180
},
"facing=west": {
"model": "phycon:block/crafting_terminal",
"y": 270
},
"facing=east": {
"model": "phycon:block/crafting_terminal",
"y": 90
}
}
}

View File

@ -2,6 +2,7 @@
"block.phycon.switch": "Network Switch",
"block.phycon.network_interface": "Inventory Interface",
"block.phycon.terminal": "Terminal",
"block.phycon.crafting_terminal": "Crafting Terminal",
"block.phycon.cable_white": "White Cable",
"block.phycon.cable_orange": "Orange Cable",
"block.phycon.cable_magenta": "Magenta Cable",
@ -34,6 +35,10 @@
"item.phycon.redstone_processor": "Redstone Processor",
"gui.phycon.terminal_buffer": "Buffer",
"gui.phycon.terminal_network": "Network",
"gui.phycon.terminal_crafting": "Crafting",
"gui.phycon.terminal.clear_crafting": "Clear crafting table",
"gui.phycon.terminal.more_crafting": "Request more ingredients",
"gui.phycon.console.details": "Device Details",
"gui.phycon.console.details.ip": "IP Address: %s",
"gui.phycon.console.details.mac": "MAC Address: %s",
@ -75,6 +80,8 @@
"advancements.phycon.interface.description": "Place a Network Interface on a Cable to connect to a Chest",
"advancements.phycon.terminal.title": "Spooky Action",
"advancements.phycon.terminal.description": "Use a Terminal to interact with a Chest",
"advancements.phycon.crafting_terminal.title": "Multi-Tool",
"advancements.phycon.crafting_terminal.description": "Construct a Crafting Terminal",
"advancements.phycon.switch.title": "Interchange",
"advancements.phycon.switch.description": "Connect multiple devices with a Network Switch",
"advancements.phycon.console.title": "Console",

View File

@ -0,0 +1,14 @@
{
"parent": "block/cube",
"textures": {
"side": "phycon:block/casing",
"front": "phycon:block/crafting_terminal",
"particle": "#side",
"down": "#side",
"up": "#side",
"north": "#front",
"south": "#side",
"west": "#side",
"east": "#side"
}
}

View File

@ -0,0 +1,13 @@
{
"parent": "block/cube",
"textures": {
"side": "phycon:block/casing",
"front": "phycon:block/crafting_terminal",
"down": "#side",
"up": "#side",
"north": "#side",
"south": "#side",
"west": "#side",
"east": "#front"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,35 @@
{
"parent": "phycon:terminal",
"display": {
"icon": {
"item": "phycon:crafting_terminal"
},
"title": {
"translate": "advancements.phycon.crafting_terminal.title"
},
"description": {
"translate": "advancements.phycon.crafting_terminal.description"
},
"frame": "task",
"show_toast": true,
"announce_to_chat": true,
"hidden": false
},
"criteria": {
"crafting_terminal": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"item": "phycon:crafting_terminal"
}
]
}
}
},
"requirements": [
[
"crafting_terminal"
]
]
}

View File

@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"rewards": {
"recipes": [
"phycon:crafting_terminal"
]
},
"criteria": {
"has_terminal": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"item": "phycon:terminal"
}
]
}
},
"has_the_recipe": {
"trigger": "minecraft:recipe_unlocked",
"conditions": {
"recipe": "phycon:crafting_terminal"
}
}
},
"requirements": [
[
"has_terminal",
"has_the_recipe"
]
]
}

View File

@ -0,0 +1,17 @@
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
{
"item": "phycon:terminal"
},
{
"item": "phycon:item_processor"
},
{
"item": "minecraft:crafting_table"
}
],
"result": {
"item": "phycon:crafting_terminal"
}
}