Compare commits

...

10 Commits

47 changed files with 1812 additions and 957 deletions

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import net.minecraft.client.render.item.ItemRenderer;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot; import net.minecraft.screen.slot.Slot;
import net.shadowfacts.phycon.block.terminal.TerminalScreen; import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler; import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; 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") at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableDepthTest()V")
) )
private void drawSlotUnderlay(MatrixStack matrixStack, Slot slot, CallbackInfo ci) { private void drawSlotUnderlay(MatrixStack matrixStack, Slot slot, CallbackInfo ci) {
if ((Object)this instanceof TerminalScreen) { if ((Object)this instanceof AbstractTerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this; AbstractTerminalScreen<?, ?> self = (AbstractTerminalScreen<?, ?>)(Object)this;
self.drawSlotUnderlay(matrixStack, slot); 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") 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) { 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) { if ((Object)this instanceof AbstractTerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this; AbstractTerminalScreen<?, ?> self = (AbstractTerminalScreen<?, ?>)(Object)this;
TerminalScreenHandler handler = self.getScreenHandler(); AbstractTerminalScreenHandler<?> handler = self.getScreenHandler();
if (slot.id < handler.getNetworkSlotsEnd() && stack.getCount() > 1) { if (slot.id < handler.getNetworkSlotsEnd() && stack.getCount() > 1) {
self.drawNetworkSlotAmount(stack, x, y, slot, matrixStack); self.drawNetworkSlotAmount(stack, x, y, slot, matrixStack);
return; return;

View File

@ -1,6 +1,7 @@
package net.shadowfacts.cacao.view.button package net.shadowfacts.cacao.view.button
import net.minecraft.client.util.math.MatrixStack import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture 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.NinePatchView
import net.shadowfacts.cacao.view.View import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl 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 * 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. * unless the background view is not fully opaque.
*/ */
var background: View? = NinePatchView(NinePatchTexture.BUTTON_BG) 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. * The background to draw when the button is hovered over by the mouse.
* If `null`, the normal [background] will be used. * If `null`, the normal [background] will be used.
* @see background * @see background
*/ */
var hoveredBackground: View? = NinePatchView(NinePatchTexture.BUTTON_HOVERED_BG) 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]. * The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used. * If `null`, the normal [background] will be used.
* @see background * @see background
*/ */
var disabledBackground: View? = NinePatchView(NinePatchTexture.BUTTON_DISABLED_BG) 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() { override fun wasAdded() {
solver.dsl { solver.dsl {
@ -67,22 +89,28 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK) content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK)
content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK) content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK)
content.bottomAnchor.greaterThanOrEqualTo(bottomAnchor - 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() 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) { override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix() RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top) RenderHelper.translate(floor(frame.left), floor(frame.top))
RenderHelper.fill(matrixStack, bounds, backgroundColor) 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 // don't draw subviews, otherwise all background views + content will get drawn
RenderHelper.popMatrix() RenderHelper.popMatrix()
if (tooltip != null && mouse in bounds) {
window!!.drawTooltip(listOf(tooltip!!))
}
} }
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {

View File

@ -26,9 +26,10 @@ object PhysicalConnectivity: ModInitializer {
PhyItems.init() PhyItems.init()
PhyScreens.init() PhyScreens.init()
registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem) registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems) registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
registerGlobalReceiver(C2SConfigureDevice)
for (it in FabricLoader.getInstance().getEntrypoints("phycon", PhyConPlugin::class.java)) { for (it in FabricLoader.getInstance().getEntrypoints("phycon", PhyConPlugin::class.java)) {
it.initializePhyCon(PhyConAPIImpl) 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.fabricmc.fabric.api.renderer.v1.material.RenderMaterial
import net.shadowfacts.phycon.block.inserter.InserterScreen import net.shadowfacts.phycon.block.inserter.InserterScreen
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreen 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.init.PhyScreens
import net.shadowfacts.phycon.block.terminal.TerminalScreen import net.shadowfacts.phycon.block.terminal.TerminalScreen
import net.shadowfacts.phycon.client.PhyExtendedModelProvider import net.shadowfacts.phycon.client.PhyExtendedModelProvider
@ -40,6 +41,7 @@ object PhysicalConnectivityClient: ClientModInitializer {
} }
ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen) ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen)
ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen)
ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen) ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen)
ScreenRegistry.register(PhyScreens.REDSTONE_EMITTER, ::RedstoneEmitterScreen) 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.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity 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.component.*
import net.shadowfacts.phycon.packet.* import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode import net.shadowfacts.phycon.util.ActivationMode
@ -39,7 +39,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
private val invProxy = MinerInvProxy(this) private val invProxy = MinerInvProxy(this)
override val pendingInsertions = mutableListOf<PendingInsertion>() override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = TerminalBlockEntity.INSERTION_TIMEOUT override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT
override val controller = ActivationController(40L, this) override val controller = ActivationController(40L, this)
override var providerPriority = 0 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 package net.shadowfacts.phycon.block.terminal
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.EnumHelper import net.shadowfacts.cacao.util.EnumHelper
@ -31,14 +30,15 @@ class SettingButton<E>(
get() = content as TextureView get() = content as TextureView
init { init {
updateTexture() update()
} }
private fun updateTexture() { private fun update() {
textureView.texture = textureCache.getOrPut(key.value) { textureView.texture = textureCache.getOrPut(key.value) {
val uv = key.value.uv val uv = key.value.uv
Texture(key.value.iconTexture, uv[0], uv[1]) Texture(key.value.iconTexture, uv[0], uv[1])
} }
tooltip = key.value.tooltip
} }
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
@ -53,20 +53,10 @@ class SettingButton<E>(
PhysicalConnectivityClient.terminalSettings[key] = newValue PhysicalConnectivityClient.terminalSettings[key] = newValue
updateTexture() update()
} }
return super.mouseClicked(point, mouseButton) 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 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.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.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class TerminalBlock: DeviceBlock<TerminalBlockEntity>( class TerminalBlock: AbstractTerminalBlock<TerminalBlockEntity>() {
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal") 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 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 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.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory 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.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.TranslatableText 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.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity import net.shadowfacts.phycon.packet.RequestInventoryPacket
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 * @author shadowfacts
*/ */
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), class TerminalBlockEntity: AbstractTerminalBlockEntity(PhyBlockEntities.TERMINAL) {
InventoryChangedListener,
BlockEntityClientSerializable,
Tickable,
ItemStackPacketHandler,
NetworkStackDispatcher<TerminalBlockEntity.PendingInsertion> {
companion object { override fun onActivate(player: PlayerEntity) {
// 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) {
if (!world!!.isClient) { if (!world!!.isClient) {
updateAndSync() 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] } 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() var remaining = stack.copy()
for (slot in 0 until size()) { 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) { if (remaining.isEmpty) {
break break
@ -48,19 +48,17 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) {
return remaining return remaining
} }
private fun insertFromNetwork(stack: ItemStack, slot: Int): ItemStack { private fun tryInsert(stack: ItemStack, slot: Int, mode: Mode): ItemStack {
val mode = modes[slot]
if (mode == Mode.TO_NETWORK) return stack
val current = getStack(slot) val current = getStack(slot)
if (current.isEmpty) { if (current.isEmpty) {
setStack(slot, stack) setStack(slot, stack)
modes[slot] = Mode.FROM_NETWORK markSlot(slot, mode)
return ItemStack.EMPTY return ItemStack.EMPTY
} else if (ItemStackUtil.areEqualIgnoreAmounts(stack, current)) { } else if (ItemStackUtil.areEqualIgnoreAmounts(stack, current)) {
val toTransfer = min(current.maxCount - current.count, stack.count) val toTransfer = min(current.maxCount - current.count, stack.count)
current.count += toTransfer current.count += toTransfer
stack.count -= toTransfer stack.count -= toTransfer
modes[slot] = Mode.FROM_NETWORK markSlot(slot, mode)
return stack return stack
} else { } else {
return stack 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 { override fun getStack(slot: Int): ItemStack {
if (slot >= screenHandler.itemsForDisplay.size) return ItemStack.EMPTY if (slot >= screenHandler.itemsForDisplay.size) return ItemStack.EMPTY
return screenHandler.itemsForDisplay[slot] return screenHandler.itemsForDisplay[slot]

View File

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

View File

@ -1,194 +1,33 @@
package net.shadowfacts.phycon.block.terminal 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.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.text.Text
import net.minecraft.util.Identifier 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.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 * @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 { companion object {
private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") 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 override val backgroundTexture = BACKGROUND
get() = backgroundWidth
val backgroundHeight: Int
get() = backgroundHeight
val terminalVC = TerminalViewController(this, handler, handler.terminal) override fun createViewController(): AbstractTerminalViewController<*, *, *> {
var amountVC: TerminalRequestAmountViewController? = null return TerminalViewController(this, handler)
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
}
} }
} }

View File

@ -1,62 +1,18 @@
package net.shadowfacts.phycon.block.terminal 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.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf 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.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens 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 * @author shadowfacts
*/ */
class TerminalScreenHandler( class TerminalScreenHandler(
syncId: Int, syncId: Int,
val playerInv: PlayerInventory, playerInv: PlayerInventory,
val terminal: TerminalBlockEntity, terminal: TerminalBlockEntity,
): ScreenHandler(PhyScreens.TERMINAL, syncId), ): AbstractTerminalScreenHandler<TerminalBlockEntity>(PhyScreens.TERMINAL, syncId, playerInv, terminal) {
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
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf):
this( this(
@ -65,195 +21,4 @@ class TerminalScreenHandler(
PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! 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 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.client.screen.ScrollTrackView
import net.shadowfacts.phycon.util.TerminalSettings
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class TerminalViewController( class TerminalViewController(
val screen: TerminalScreen, screen: TerminalScreen,
val handler: TerminalScreenHandler, handler: TerminalScreenHandler,
val terminal: TerminalBlockEntity, ): AbstractTerminalViewController<TerminalBlockEntity, TerminalScreen, TerminalScreenHandler>(
): ViewController() { screen,
handler,
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
}
}
} }

View File

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

View File

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

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_controller.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlockEntity 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.TerminalBlock
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
@ -29,6 +31,7 @@ object PhyBlockEntities {
val INTERFACE = create(::InterfaceBlockEntity, PhyBlocks.INTERFACE) val INTERFACE = create(::InterfaceBlockEntity, PhyBlocks.INTERFACE)
val TERMINAL = create(::TerminalBlockEntity, PhyBlocks.TERMINAL) val TERMINAL = create(::TerminalBlockEntity, PhyBlocks.TERMINAL)
val CRAFTING_TERMINAL = create(::CraftingTerminalBlockEntity, PhyBlocks.CRAFTING_TERMINAL)
val SWITCH = create(::SwitchBlockEntity, PhyBlocks.SWITCH) val SWITCH = create(::SwitchBlockEntity, PhyBlocks.SWITCH)
val EXTRACTOR = create(::ExtractorBlockEntity, PhyBlocks.EXTRACTOR) val EXTRACTOR = create(::ExtractorBlockEntity, PhyBlocks.EXTRACTOR)
val INSERTER = create(::InserterBlockEntity, PhyBlocks.INSERTER) val INSERTER = create(::InserterBlockEntity, PhyBlocks.INSERTER)
@ -43,6 +46,7 @@ object PhyBlockEntities {
fun init() { fun init() {
register(InterfaceBlock.ID, INTERFACE) register(InterfaceBlock.ID, INTERFACE)
register(TerminalBlock.ID, TERMINAL) register(TerminalBlock.ID, TERMINAL)
register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL)
register(SwitchBlock.ID, SWITCH) register(SwitchBlock.ID, SWITCH)
register(ExtractorBlock.ID, EXTRACTOR) register(ExtractorBlock.ID, EXTRACTOR)
register(InserterBlock.ID, INSERTER) 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.netswitch.SwitchBlock
import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock 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.block.terminal.TerminalBlock
/** /**
@ -24,6 +25,7 @@ object PhyBlocks {
val INTERFACE = InterfaceBlock() val INTERFACE = InterfaceBlock()
val TERMINAL = TerminalBlock() val TERMINAL = TerminalBlock()
val CRAFTING_TERMINAL = CraftingTerminalBlock()
val SWITCH = SwitchBlock() val SWITCH = SwitchBlock()
val EXTRACTOR = ExtractorBlock() val EXTRACTOR = ExtractorBlock()
val INSERTER = InserterBlock() val INSERTER = InserterBlock()
@ -38,6 +40,7 @@ object PhyBlocks {
register(InterfaceBlock.ID, INTERFACE) register(InterfaceBlock.ID, INTERFACE)
register(TerminalBlock.ID, TERMINAL) register(TerminalBlock.ID, TERMINAL)
register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL)
register(SwitchBlock.ID, SWITCH) register(SwitchBlock.ID, SWITCH)
register(ExtractorBlock.ID, EXTRACTOR) register(ExtractorBlock.ID, EXTRACTOR)
register(InserterBlock.ID, INSERTER) 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.PhysicalConnectivity
import net.shadowfacts.phycon.item.ConsoleItem import net.shadowfacts.phycon.item.ConsoleItem
import net.shadowfacts.phycon.item.ScrewdriverItem 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.extractor.ExtractorBlock
import net.shadowfacts.phycon.block.inserter.InserterBlock import net.shadowfacts.phycon.block.inserter.InserterBlock
import net.shadowfacts.phycon.block.miner.MinerBlock 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.netswitch.SwitchBlock
import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock 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.block.terminal.TerminalBlock
import net.shadowfacts.phycon.item.DeviceBlockItem import net.shadowfacts.phycon.item.DeviceBlockItem
import net.shadowfacts.phycon.item.FaceDeviceBlockItem import net.shadowfacts.phycon.item.FaceDeviceBlockItem
@ -30,6 +30,7 @@ object PhyItems {
val INTERFACE = FaceDeviceBlockItem(PhyBlocks.INTERFACE, Item.Settings()) val INTERFACE = FaceDeviceBlockItem(PhyBlocks.INTERFACE, Item.Settings())
val TERMINAL = DeviceBlockItem(PhyBlocks.TERMINAL, 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 SWITCH = BlockItem(PhyBlocks.SWITCH, Item.Settings())
val EXTRACTOR = FaceDeviceBlockItem(PhyBlocks.EXTRACTOR, Item.Settings()) val EXTRACTOR = FaceDeviceBlockItem(PhyBlocks.EXTRACTOR, Item.Settings())
val INSERTER = FaceDeviceBlockItem(PhyBlocks.INSERTER, Item.Settings()) val INSERTER = FaceDeviceBlockItem(PhyBlocks.INSERTER, Item.Settings())
@ -53,6 +54,7 @@ object PhyItems {
register(InterfaceBlock.ID, INTERFACE) register(InterfaceBlock.ID, INTERFACE)
register(TerminalBlock.ID, TERMINAL) register(TerminalBlock.ID, TERMINAL)
register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL)
register(SwitchBlock.ID, SWITCH) register(SwitchBlock.ID, SWITCH)
register(ExtractorBlock.ID, EXTRACTOR) register(ExtractorBlock.ID, EXTRACTOR)
register(InserterBlock.ID, INSERTER) 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.minecraft.screen.ScreenHandlerType
import net.shadowfacts.phycon.block.inserter.InserterScreenHandler import net.shadowfacts.phycon.block.inserter.InserterScreenHandler
import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreenHandler 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 import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler
object PhyScreens { object PhyScreens {
lateinit var TERMINAL: ScreenHandlerType<TerminalScreenHandler> lateinit var TERMINAL: ScreenHandlerType<TerminalScreenHandler>
private set private set
lateinit var CRAFTING_TERMINAL: ScreenHandlerType<CraftingTerminalScreenHandler>
private set
lateinit var INSERTER: ScreenHandlerType<InserterScreenHandler> lateinit var INSERTER: ScreenHandlerType<InserterScreenHandler>
private set private set
lateinit var REDSTONE_EMITTER: ScreenHandlerType<RedstoneEmitterScreenHandler> lateinit var REDSTONE_EMITTER: ScreenHandlerType<RedstoneEmitterScreenHandler>
private set private set
fun init() { 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) INSERTER = ScreenHandlerRegistry.registerExtended(InserterScreenHandler.ID, ::InserterScreenHandler)
REDSTONE_EMITTER = ScreenHandlerRegistry.registerExtended(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler) REDSTONE_EMITTER = ScreenHandlerRegistry.registerExtended(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler)
} }

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

View File

@ -11,9 +11,8 @@ import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.PhysicalConnectivityClient import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings import net.shadowfacts.phycon.util.TerminalSettings
/** /**
@ -23,7 +22,7 @@ object C2STerminalUpdateDisplayedItems: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_update_displayed") 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() val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value) buf.writeIdentifier(terminal.world!!.registryKey.value)
@ -47,7 +46,7 @@ object C2STerminalUpdateDisplayedItems: ServerReceiver {
server.execute { server.execute {
if (player.world.registryKey.value != dimID) return@execute if (player.world.registryKey.value != dimID) return@execute
val screenHandler = player.currentScreenHandler val screenHandler = player.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute if (screenHandler !is AbstractTerminalScreenHandler<*>) return@execute
if (screenHandler.terminal.pos != pos) return@execute if (screenHandler.terminal.pos != pos) return@execute
screenHandler.sendUpdatedItemsToClient(player, query, settings, scrollPosition) 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.Packet
import net.minecraft.network.PacketByteBuf import net.minecraft.network.PacketByteBuf
import net.shadowfacts.phycon.PhysicalConnectivityClient import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.readItemStackWithoutCount import net.shadowfacts.phycon.util.readItemStackWithoutCount
import net.shadowfacts.phycon.util.writeItemStackWithoutCount import net.shadowfacts.phycon.util.writeItemStackWithoutCount
@ -21,7 +20,7 @@ import net.shadowfacts.phycon.util.writeItemStackWithoutCount
object S2CTerminalUpdateDisplayedItems: ClientReceiver { object S2CTerminalUpdateDisplayedItems: ClientReceiver {
override val CHANNEL = C2STerminalUpdateDisplayedItems.CHANNEL 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() val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value) buf.writeIdentifier(terminal.world!!.registryKey.value)
@ -45,9 +44,9 @@ object S2CTerminalUpdateDisplayedItems: ClientReceiver {
val dimID = buf.readIdentifier() val dimID = buf.readIdentifier()
val pos = buf.readBlockPos() val pos = buf.readBlockPos()
val entryCount = buf.readVarInt() val entryCount = buf.readVarInt()
val entries = ArrayList<TerminalScreenHandler.Entry>(entryCount) val entries = ArrayList<AbstractTerminalScreenHandler.Entry>(entryCount)
for (i in 0 until 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() val query = buf.readString()
PhysicalConnectivityClient.terminalSettings.fromTag(buf.readCompoundTag()!!) PhysicalConnectivityClient.terminalSettings.fromTag(buf.readCompoundTag()!!)
@ -57,7 +56,7 @@ object S2CTerminalUpdateDisplayedItems: ClientReceiver {
client.execute { client.execute {
if (client.player!!.world.registryKey.value != dimID) return@execute if (client.player!!.world.registryKey.value != dimID) return@execute
val screenHandler = client.player!!.currentScreenHandler val screenHandler = client.player!!.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute if (screenHandler !is AbstractTerminalScreenHandler<*>) return@execute
if (screenHandler.terminal.pos != pos) return@execute if (screenHandler.terminal.pos != pos) return@execute
screenHandler.receivedUpdatedItemsFromServer(entries, query, scrollPosition, totalEntries) 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.switch": "Network Switch",
"block.phycon.network_interface": "Inventory Interface", "block.phycon.network_interface": "Inventory Interface",
"block.phycon.terminal": "Terminal", "block.phycon.terminal": "Terminal",
"block.phycon.crafting_terminal": "Crafting Terminal",
"block.phycon.cable_white": "White Cable", "block.phycon.cable_white": "White Cable",
"block.phycon.cable_orange": "Orange Cable", "block.phycon.cable_orange": "Orange Cable",
"block.phycon.cable_magenta": "Magenta Cable", "block.phycon.cable_magenta": "Magenta Cable",
@ -34,6 +35,10 @@
"item.phycon.redstone_processor": "Redstone Processor", "item.phycon.redstone_processor": "Redstone Processor",
"gui.phycon.terminal_buffer": "Buffer", "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": "Device Details",
"gui.phycon.console.details.ip": "IP Address: %s", "gui.phycon.console.details.ip": "IP Address: %s",
"gui.phycon.console.details.mac": "MAC 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.interface.description": "Place a Network Interface on a Cable to connect to a Chest",
"advancements.phycon.terminal.title": "Spooky Action", "advancements.phycon.terminal.title": "Spooky Action",
"advancements.phycon.terminal.description": "Use a Terminal to interact with a Chest", "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.title": "Interchange",
"advancements.phycon.switch.description": "Connect multiple devices with a Network Switch", "advancements.phycon.switch.description": "Connect multiple devices with a Network Switch",
"advancements.phycon.console.title": "Console", "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"
}
}