Compare commits

...

11 Commits
main ... future

186 changed files with 9357 additions and 8291 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# reformat
f6f4c12d0304c945c03ab70556048ee8d78e4019

View File

@ -1,7 +1,7 @@
plugins { plugins {
id "fabric-loom" version "0.12.9" id "fabric-loom" version "0.12.9"
id "maven-publish" id "maven-publish"
id "org.jetbrains.kotlin.jvm" version "1.6.10" id "org.jetbrains.kotlin.jvm" version "1.7.10"
} }
archivesBaseName = project.archives_base_name archivesBaseName = project.archives_base_name
@ -79,9 +79,9 @@ repositories {
dependencies { dependencies {
// PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
// You may need to force-disable transitiveness on them. // You may need to force-disable transitiveness on them.
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}" // modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}" // include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-items:${project.libblockattributes_version}" // include "alexiil.mc.lib:libblockattributes-items:${project.libblockattributes_version}"
implementation project(":kiwi-java") implementation project(":kiwi-java")
include project(":kiwi-java") include project(":kiwi-java")

View File

@ -17,71 +17,72 @@ import java.lang.invoke.MethodHandles
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhyConPluginClient: ClientModInitializer, REIClientPlugin, AbstractTerminalScreen.SearchQueryListener { object PhyConPluginClient : ClientModInitializer, REIClientPlugin, AbstractTerminalScreen.SearchQueryListener {
private val logger = LogManager.getLogger() private val logger = LogManager.getLogger()
private var isHighlightingHandle: MethodHandle? = null private var isHighlightingHandle: MethodHandle? = null
override fun onInitializeClient() { override fun onInitializeClient() {
ClientScreenInputEvent.MOUSE_RELEASED_PRE.register { client, screen, mouseX, mouseY, button -> ClientScreenInputEvent.MOUSE_RELEASED_PRE.register { client, screen, mouseX, mouseY, button ->
if (screen is AbstractTerminalScreen<*, *>) { if (screen is AbstractTerminalScreen<*, *>) {
REIRuntime.getInstance().searchTextField?.also { REIRuntime.getInstance().searchTextField?.also {
if (it.isFocused) { if (it.isFocused) {
screen.terminalVC.searchField.resignFirstResponder() screen.terminalVC.searchField.resignFirstResponder()
} else { } else {
screen.terminalVC.searchField.becomeFirstResponder() screen.terminalVC.searchField.becomeFirstResponder()
} }
} }
} }
EventResult.pass() EventResult.pass()
} }
AbstractTerminalScreen.searchQueryListener = this AbstractTerminalScreen.searchQueryListener = this
try { try {
val clazz = Class.forName("me.shedaniel.rei.impl.client.gui.widget.search.OverlaySearchField") val clazz = Class.forName("me.shedaniel.rei.impl.client.gui.widget.search.OverlaySearchField")
isHighlightingHandle = MethodHandles.publicLookup().findStaticGetter(clazz, "isHighlighting", Boolean::class.java) isHighlightingHandle =
} catch (e: ReflectiveOperationException) { MethodHandles.publicLookup().findStaticGetter(clazz, "isHighlighting", Boolean::class.java)
logger.warn("Unable to find OverlaySearchField.isHighlighting, highlight sync will be disabled", e) } catch (e: ReflectiveOperationException) {
} logger.warn("Unable to find OverlaySearchField.isHighlighting, highlight sync will be disabled", e)
} }
}
override fun registerScreens(registry: ScreenRegistry) { override fun registerScreens(registry: ScreenRegistry) {
registry.exclusionZones().register(AbstractTerminalScreen::class.java) { registry.exclusionZones().register(AbstractTerminalScreen::class.java) {
val screen = MinecraftClient.getInstance().currentScreen as AbstractTerminalScreen<*, *> 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(
Rectangle(rect.left.toInt(), rect.top.toInt(), view.bounds.width.toInt(), view.bounds.height.toInt()) Rectangle(rect.left.toInt(), rect.top.toInt(), view.bounds.width.toInt(), view.bounds.height.toInt())
) )
} }
} }
override fun terminalSearchQueryChanged(newValue: String) { override fun terminalSearchQueryChanged(newValue: String) {
if (shouldSync()) { if (shouldSync()) {
REIRuntime.getInstance().searchTextField?.text = newValue REIRuntime.getInstance().searchTextField?.text = newValue
} }
} }
override fun requestTerminalSearchFieldUpdate(): String? { override fun requestTerminalSearchFieldUpdate(): String? {
return if (shouldSync()) { return if (shouldSync()) {
REIRuntime.getInstance().searchTextField?.text REIRuntime.getInstance().searchTextField?.text
} else { } else {
null null
} }
} }
private fun shouldSync(): Boolean { private fun shouldSync(): Boolean {
return when (PhysicalConnectivityClient.terminalSettings[PhyConPluginCommon.REI_SYNC_KEY]) { return when (PhysicalConnectivityClient.terminalSettings[PhyConPluginCommon.REI_SYNC_KEY]) {
REISyncMode.OFF -> false REISyncMode.OFF -> false
REISyncMode.ON -> true REISyncMode.ON -> true
REISyncMode.HIGHLIGHT_ONLY -> { REISyncMode.HIGHLIGHT_ONLY -> {
if (isHighlightingHandle != null) { if (isHighlightingHandle != null) {
isHighlightingHandle!!.invoke() as Boolean isHighlightingHandle!!.invoke() as Boolean
} else { } else {
false false
} }
} }
} }
} }
} }

View File

@ -18,51 +18,55 @@ import java.util.stream.IntStream
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhyConPluginCommon: REIServerPlugin, PhyConPlugin { object PhyConPluginCommon : REIServerPlugin, PhyConPlugin {
const val MODID = "phycon_rei" const val MODID = "phycon_rei"
lateinit var REI_SYNC_KEY: TerminalSettingKey<REISyncMode> lateinit var REI_SYNC_KEY: TerminalSettingKey<REISyncMode>
private set private set
override fun registerMenuInfo(registry: MenuInfoRegistry) { override fun registerMenuInfo(registry: MenuInfoRegistry) {
registry.register(CategoryIdentifier.of("minecraft", "plugins/crafting"), CraftingTerminalScreenHandler::class.java, SimpleMenuInfoProvider.of(::TerminalInfo)) registry.register(
} CategoryIdentifier.of("minecraft", "plugins/crafting"),
CraftingTerminalScreenHandler::class.java,
SimpleMenuInfoProvider.of(::TerminalInfo)
)
}
override fun initializePhyCon(api: PhyConAPI) { override fun initializePhyCon(api: PhyConAPI) {
REI_SYNC_KEY = api.registerTerminalSetting(Identifier(MODID, "rei_sync"), REISyncMode.OFF) REI_SYNC_KEY = api.registerTerminalSetting(Identifier(MODID, "rei_sync"), REISyncMode.OFF)
} }
class TerminalInfo<D: SimpleGridMenuDisplay>( class TerminalInfo<D : SimpleGridMenuDisplay>(
private val display: D, private val display: D,
): SimpleGridMenuInfo<CraftingTerminalScreenHandler, D> { ) : SimpleGridMenuInfo<CraftingTerminalScreenHandler, D> {
override fun getCraftingResultSlotIndex(menu: CraftingTerminalScreenHandler): Int { override fun getCraftingResultSlotIndex(menu: CraftingTerminalScreenHandler): Int {
return menu.resultSlot.id return menu.resultSlot.id
} }
override fun getInputStackSlotIds(context: MenuInfoContext<CraftingTerminalScreenHandler, *, D>): IntStream { override fun getInputStackSlotIds(context: MenuInfoContext<CraftingTerminalScreenHandler, *, D>): IntStream {
return IntStream.range(context.menu.craftingSlotsStart, context.menu.craftingSlotsEnd) return IntStream.range(context.menu.craftingSlotsStart, context.menu.craftingSlotsEnd)
} }
override fun getInventorySlots(context: MenuInfoContext<CraftingTerminalScreenHandler, *, D>): Iterable<SlotAccessor> { override fun getInventorySlots(context: MenuInfoContext<CraftingTerminalScreenHandler, *, D>): Iterable<SlotAccessor> {
val slots = super.getInventorySlots(context).toMutableList() val slots = super.getInventorySlots(context).toMutableList()
for (i in (context.menu.bufferSlotsStart until context.menu.bufferSlotsEnd)) { for (i in (context.menu.bufferSlotsStart until context.menu.bufferSlotsEnd)) {
slots.add(SlotAccessor.fromSlot(context.menu.getSlot(i))) slots.add(SlotAccessor.fromSlot(context.menu.getSlot(i)))
} }
return slots return slots
} }
override fun getCraftingWidth(menu: CraftingTerminalScreenHandler): Int { override fun getCraftingWidth(menu: CraftingTerminalScreenHandler): Int {
return 3 return 3
} }
override fun getCraftingHeight(menu: CraftingTerminalScreenHandler): Int { override fun getCraftingHeight(menu: CraftingTerminalScreenHandler): Int {
return 3 return 3
} }
override fun getDisplay(): D { override fun getDisplay(): D {
return display return display
} }
} }
} }

View File

@ -8,7 +8,7 @@ import net.shadowfacts.phycon.api.TerminalSetting
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
enum class REISyncMode: TerminalSetting { enum class REISyncMode : TerminalSetting {
OFF, OFF,
ON, ON,
HIGHLIGHT_ONLY; HIGHLIGHT_ONLY;

View File

@ -14,18 +14,27 @@ import techreborn.init.TRContent
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhyConTR: ModInitializer { object PhyConTR : ModInitializer {
override fun onInitialize() { override fun onInitialize() {
TRContent.StorageUnit.values().forEach { TRContent.StorageUnit.values().forEach {
ItemAttributes.GROUPED_INV.setBlockAdder(AttributeSourceType.COMPAT_WRAPPER, it.block, ::addStorageUnitGroupedInv) ItemAttributes.GROUPED_INV.setBlockAdder(
} AttributeSourceType.COMPAT_WRAPPER,
} it.block,
::addStorageUnitGroupedInv
)
}
}
private fun addStorageUnitGroupedInv(world: World, pos: BlockPos, state: BlockState, to: AttributeList<GroupedItemInv>) { private fun addStorageUnitGroupedInv(
(world.getBlockEntity(pos) as? StorageUnitBaseBlockEntity)?.also { su -> world: World,
to.offer(StorageUnitWrapper(su)) pos: BlockPos,
} state: BlockState,
} to: AttributeList<GroupedItemInv>
) {
(world.getBlockEntity(pos) as? StorageUnitBaseBlockEntity)?.also { su ->
to.offer(StorageUnitWrapper(su))
}
}
} }

View File

@ -15,67 +15,67 @@ import kotlin.math.min
* @author shadowfacts * @author shadowfacts
*/ */
class StorageUnitWrapper( class StorageUnitWrapper(
val be: StorageUnitBaseBlockEntity, val be: StorageUnitBaseBlockEntity,
): GroupedItemInv { ) : GroupedItemInv {
override fun getStoredStacks(): Set<ItemStack> { override fun getStoredStacks(): Set<ItemStack> {
val set = ItemStackCollections.set() val set = ItemStackCollections.set()
if (!be.storedStack.isEmpty) { if (!be.storedStack.isEmpty) {
set.add(be.storedStack) set.add(be.storedStack)
} }
return set return set
} }
override fun getTotalCapacity(): Int { override fun getTotalCapacity(): Int {
return be.maxCapacity return be.maxCapacity
} }
override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic { override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic {
// todo: should spaceAddable really be zero? that's what SimpleGroupedItemInv does // todo: should spaceAddable really be zero? that's what SimpleGroupedItemInv does
return if (be.storedStack.isEmpty) { return if (be.storedStack.isEmpty) {
GroupedItemInvView.ItemInvStatistic(filter, 0, 0, totalCapacity) GroupedItemInvView.ItemInvStatistic(filter, 0, 0, totalCapacity)
} else if (filter.matches(be.storedStack)) { } else if (filter.matches(be.storedStack)) {
// don't use the storedAmount field, it's only used on the client for rendering // don't use the storedAmount field, it's only used on the client for rendering
val amount = be.getStoredAmount() val amount = be.getStoredAmount()
GroupedItemInvView.ItemInvStatistic(filter, amount, 0, totalCapacity - amount) GroupedItemInvView.ItemInvStatistic(filter, amount, 0, totalCapacity - amount)
} else { } else {
GroupedItemInvView.ItemInvStatistic(filter, 0, 0, 0) GroupedItemInvView.ItemInvStatistic(filter, 0, 0, 0)
} }
} }
override fun attemptInsertion(filter: ItemStack, simulation: Simulation): ItemStack { override fun attemptInsertion(filter: ItemStack, simulation: Simulation): ItemStack {
if (simulation.isAction) { if (simulation.isAction) {
return be.processInput(filter) return be.processInput(filter)
} }
if (be.storedStack.isEmpty) { if (be.storedStack.isEmpty) {
return ItemStack.EMPTY return ItemStack.EMPTY
} }
if (!ItemStackUtil.areEqualIgnoreAmounts(be.storedStack, filter)) { if (!ItemStackUtil.areEqualIgnoreAmounts(be.storedStack, filter)) {
return filter return filter
} }
val availableCapacity = totalCapacity - be.getStoredAmount() val availableCapacity = totalCapacity - be.getStoredAmount()
return if (availableCapacity >= filter.count) { return if (availableCapacity >= filter.count) {
ItemStack.EMPTY ItemStack.EMPTY
} else { } else {
filter.copyWithCount(filter.count - availableCapacity) filter.copyWithCount(filter.count - availableCapacity)
} }
} }
override fun attemptExtraction(filter: ItemFilter, maxAmount: Int, simulation: Simulation): ItemStack { override fun attemptExtraction(filter: ItemFilter, maxAmount: Int, simulation: Simulation): ItemStack {
if (be.storedStack.isEmpty || !filter.matches(be.storedStack)) { if (be.storedStack.isEmpty || !filter.matches(be.storedStack)) {
return ItemStack.EMPTY return ItemStack.EMPTY
} }
val extracted = min(maxAmount, be.getStoredAmount()) val extracted = min(maxAmount, be.getStoredAmount())
if (simulation.isAction) { if (simulation.isAction) {
be.storedStack.decrement(extracted) be.storedStack.decrement(extracted)
} }
return be.storedStack.copyWithCount(extracted) return be.storedStack.copyWithCount(extracted)
} }
} }

View File

@ -16,6 +16,7 @@ public interface Interface {
void send(@NotNull EthernetFrame frame); void send(@NotNull EthernetFrame frame);
default void cableDisconnected() {} default void cableDisconnected() {
}
} }

View File

@ -10,9 +10,9 @@ public interface NetworkDevice {
/** /**
* The IP address of this device. * The IP address of this device.
* * <p>
* If a device has not been assigned an address by a DHCP server, it may self-assign a randomly generated one. * If a device has not been assigned an address by a DHCP server, it may self-assign a randomly generated one.
* * <p>
* The address of a network device should never be the broadcast address. * The address of a network device should never be the broadcast address.
* *
* @return The IP address of this device. * @return The IP address of this device.

View File

@ -1,13 +0,0 @@
package net.shadowfacts.phycon.api;
import alexiil.mc.lib.attributes.Attribute;
import alexiil.mc.lib.attributes.Attributes;
/**
* @author shadowfacts
*/
public class PhyAttributes {
public static final Attribute<PacketSink> PACKET_SINK = Attributes.create(PacketSink.class);
}

View File

@ -13,6 +13,7 @@ public interface TerminalSetting {
int[] getUV(); int[] getUV();
@Nullable Text getTooltip(); @Nullable
Text getTooltip();
} }

View File

@ -23,12 +23,14 @@ public final class IPAddress {
} }
private static final Random ipAddressRandom = new Random(); private static final Random ipAddressRandom = new Random();
@NotNull @NotNull
public static IPAddress random() { public static IPAddress random() {
return random(ipAddressRandom); return random(ipAddressRandom);
} }
private static final Pattern IP_PATTERN = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$"); private static final Pattern IP_PATTERN = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$");
@Nullable @Nullable
public static IPAddress parse(String s) { public static IPAddress parse(String s) {
Matcher matcher = IP_PATTERN.matcher(s); Matcher matcher = IP_PATTERN.matcher(s);

View File

@ -22,6 +22,7 @@ public final class MACAddress {
} }
private static final Random macAddressRandom = new Random(); private static final Random macAddressRandom = new Random();
@NotNull @NotNull
public static MACAddress random() { public static MACAddress random() {
return random(macAddressRandom); return random(macAddressRandom);
@ -61,7 +62,7 @@ public final class MACAddress {
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
MACAddress that = (MACAddress)o; MACAddress that = (MACAddress) o;
return address == that.address; return address == that.address;
} }

View File

@ -26,7 +26,7 @@ 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 AbstractTerminalScreen<?, ?> self) { if ((Object) this instanceof AbstractTerminalScreen<?, ?> self) {
self.drawSlotUnderlay(matrixStack, slot); self.drawSlotUnderlay(matrixStack, slot);
} }
} }
@ -36,7 +36,7 @@ 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 AbstractTerminalScreen<?, ?> self) { if ((Object) this instanceof AbstractTerminalScreen<?, ?> self) {
AbstractTerminalScreenHandler<?> 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); self.drawNetworkSlotAmount(stack, x, y);

View File

@ -7,13 +7,13 @@ import net.shadowfacts.cacao.window.Window
*/ */
interface AbstractCacaoScreen { interface AbstractCacaoScreen {
val windows: List<Window> val windows: List<Window>
fun <T: Window> addWindow(window: T, index: Int): T fun <T : Window> addWindow(window: T, index: Int): T
fun <T: Window> addWindow(window: T): T fun <T : Window> addWindow(window: T): T
fun removeWindow(window: Window) fun removeWindow(window: Window)
fun screenWillAppear() fun screenWillAppear()
} }

View File

@ -20,177 +20,177 @@ import java.util.*
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
open class CacaoHandledScreen<Handler: ScreenHandler>( open class CacaoHandledScreen<Handler : ScreenHandler>(
handler: Handler, handler: Handler,
playerInv: PlayerInventory, playerInv: PlayerInventory,
title: Text, title: Text,
): HandledScreen<Handler>(handler, playerInv, title), AbstractCacaoScreen { ) : HandledScreen<Handler>(handler, playerInv, title), AbstractCacaoScreen {
private val _windows = LinkedList<Window>() private val _windows = LinkedList<Window>()
override val windows: List<Window> = _windows override val windows: List<Window> = _windows
private var hasAppeared = false private var hasAppeared = false
override fun <T: Window> addWindow(window: T, index: Int): T { override fun <T : Window> addWindow(window: T, index: Int): T {
if (window is ScreenHandlerWindow && window.screenHandler != handler) { if (window is ScreenHandlerWindow && window.screenHandler != handler) {
throw RuntimeException("Adding ScreenHandlerWindow to CacaoHandledScreen with different screen handler is not supported") throw RuntimeException("Adding ScreenHandlerWindow to CacaoHandledScreen with different screen handler is not supported")
} }
if (hasAppeared) { if (hasAppeared) {
window.viewController.viewWillAppear() window.viewController.viewWillAppear()
} }
_windows.add(index, window) _windows.add(index, window)
window.screen = this window.screen = this
window.wasAdded() window.wasAdded()
window.resize(width, height) window.resize(width, height)
return window return window
} }
override fun <T : Window> addWindow(window: T): T { override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size) return addWindow(window, _windows.size)
} }
override fun removeWindow(window: Window) { override fun removeWindow(window: Window) {
_windows.remove(window) _windows.remove(window)
if (windows.isEmpty()) { if (windows.isEmpty()) {
close() close()
} }
} }
override fun screenWillAppear() { override fun screenWillAppear() {
windows.forEach { windows.forEach {
it.viewController.viewWillAppear() it.viewController.viewWillAppear()
} }
} }
override fun init() { override fun init() {
super.init() super.init()
windows.forEach { windows.forEach {
it.resize(width, height) it.resize(width, height)
} }
} }
override fun close() { override fun close() {
super.close() super.close()
windows.forEach { windows.forEach {
it.viewController.viewWillDisappear() it.viewController.viewWillDisappear()
it.viewController.viewDidDisappear() it.viewController.viewDidDisappear()
it.firstResponder = null it.firstResponder = null
} }
} }
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
} }
override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
// no-op // no-op
} }
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val mouse = Point(mouseX, mouseY) val mouse = Point(mouseX, mouseY)
matrixStack.push() matrixStack.push()
matrixStack.translate(0.0, 0.0, -350.0) matrixStack.translate(0.0, 0.0, -350.0)
for (i in windows.indices) { for (i in windows.indices) {
val it = windows[i] val it = windows[i]
if (i == windows.size - 1) { if (i == windows.size - 1) {
renderBackground(matrixStack) renderBackground(matrixStack)
} }
if (it is ScreenHandlerWindow) { if (it is ScreenHandlerWindow) {
if (i == windows.size - 1) { if (i == windows.size - 1) {
super.render(matrixStack, mouseX, mouseY, delta) super.render(matrixStack, mouseX, mouseY, delta)
} else { } else {
// if the screen handler window is not the frontmost, we fake the mouse x/y to disable the slot mouseover effect // if the screen handler window is not the frontmost, we fake the mouse x/y to disable the slot mouseover effect
super.render(matrixStack, -1, -1, delta) super.render(matrixStack, -1, -1, delta)
} }
matrixStack.pop() matrixStack.pop()
} }
it.draw(matrixStack, mouse, delta) it.draw(matrixStack, mouse, delta)
} }
drawMouseoverTooltip(matrixStack, mouseX, mouseY) drawMouseoverTooltip(matrixStack, mouseX, mouseY)
} }
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button)) val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) { return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK) RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
true true
} else if (window is ScreenHandlerWindow) { } else if (window is ScreenHandlerWindow) {
super.mouseClicked(mouseX, mouseY, button) super.mouseClicked(mouseX, mouseY, button)
} else { } else {
false false
} }
} }
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY) val startPoint = Point(mouseX, mouseY)
val delta = Point(deltaX, deltaY) val delta = Point(deltaX, deltaY)
val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button)) val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button))
return if (result == true) { return if (result == true) {
true true
} else if (window is ScreenHandlerWindow) { } else if (window is ScreenHandlerWindow) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
} else { } else {
false false
} }
} }
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button)) val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) { return if (result == true) {
true true
} else if (window is ScreenHandlerWindow) { } else if (window is ScreenHandlerWindow) {
super.mouseReleased(mouseX, mouseY, button) super.mouseReleased(mouseX, mouseY, button)
} else { } else {
false false
} }
} }
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean { override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val result = window?.mouseScrolled(Point(mouseX, mouseY), amount) val result = window?.mouseScrolled(Point(mouseX, mouseY), amount)
return result == true return result == true
} }
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW.GLFW_KEY_ESCAPE) { if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
windows.lastOrNull()?.removeFromScreen() windows.lastOrNull()?.removeFromScreen()
return true return true
} else { } else {
val modifiersSet by lazy { KeyModifiers(modifiers) } val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) { if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
return true return true
} }
return super.keyPressed(keyCode, scanCode, modifiers) return super.keyPressed(keyCode, scanCode, modifiers)
} }
} }
override fun charTyped(char: Char, modifiers: Int): Boolean { override fun charTyped(char: Char, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) } val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.charTyped(char, modifiersSet) }) { if (findResponder { it.charTyped(char, modifiersSet) }) {
return true return true
} }
return super.charTyped(char, modifiers) return super.charTyped(char, modifiers)
} }
override fun shouldCloseOnEsc(): Boolean { override fun shouldCloseOnEsc(): Boolean {
return false return false
} }
} }

View File

@ -20,166 +20,167 @@ import java.util.*
* *
* @author shadowfacts * @author shadowfacts
*/ */
open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title), AbstractCacaoScreen { open class CacaoScreen(title: Text = LiteralText("CacaoScreen")) : Screen(title), AbstractCacaoScreen {
// _windows is the internal, mutable object, since we only want it to by mutated by the add/removeWindow methods. // _windows is the internal, mutable object, since we only want it to by mutated by the add/removeWindow methods.
private val _windows = LinkedList<Window>() private val _windows = LinkedList<Window>()
/**
* The list of windows that belong to this screen.
*
* The window at the end of this list is the active window is the only window that will receive input events.
*
* This list should never be modified directly, only by using the [addWindow]/[removeWindow] methods.
*/
override val windows: List<Window> = _windows
private var hasAppeared = false /**
* The list of windows that belong to this screen.
*
* The window at the end of this list is the active window is the only window that will receive input events.
*
* This list should never be modified directly, only by using the [addWindow]/[removeWindow] methods.
*/
override val windows: List<Window> = _windows
/** private var hasAppeared = false
* Adds the given window to this screen's window list at the given position.
*
* @param window The Window to add to this screen.
* @param index The index to insert the window into the window list at.
* @return The window that was added, as a convenience.
*/
override fun <T: Window> addWindow(window: T, index: Int): T {
if (hasAppeared) {
window.viewController.viewWillAppear()
}
_windows.add(index, window) /**
* Adds the given window to this screen's window list at the given position.
*
* @param window The Window to add to this screen.
* @param index The index to insert the window into the window list at.
* @return The window that was added, as a convenience.
*/
override fun <T : Window> addWindow(window: T, index: Int): T {
if (hasAppeared) {
window.viewController.viewWillAppear()
}
window.screen = this _windows.add(index, window)
window.wasAdded()
window.resize(width, height)
return window window.screen = this
} window.wasAdded()
window.resize(width, height)
/** return window
* Adds the given window to the end of this screen's window list, making it the active window. }
*/
override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size)
}
/** /**
* Removes the given window from this screen's window list. * Adds the given window to the end of this screen's window list, making it the active window.
*/ */
override fun removeWindow(window: Window) { override fun <T : Window> addWindow(window: T): T {
_windows.remove(window) return addWindow(window, _windows.size)
if (windows.isEmpty()) { }
close()
}
}
override fun screenWillAppear() { /**
windows.forEach { * Removes the given window from this screen's window list.
it.viewController.viewWillAppear() */
} override fun removeWindow(window: Window) {
} _windows.remove(window)
if (windows.isEmpty()) {
close()
}
}
override fun init() { override fun screenWillAppear() {
super.init() windows.forEach {
it.viewController.viewWillAppear()
}
}
windows.forEach { override fun init() {
it.resize(width, height) super.init()
}
}
override fun close() { windows.forEach {
super.close() it.resize(width, height)
}
}
windows.forEach { override fun close() {
it.viewController.viewWillDisappear() super.close()
it.viewController.viewDidDisappear()
// resign the current first responder (if any) windows.forEach {
it.firstResponder = null it.viewController.viewWillDisappear()
} it.viewController.viewDidDisappear()
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { // resign the current first responder (if any)
if (client != null) { it.firstResponder = null
// workaround this.minecraft sometimes being null causing a crash }
renderBackground(matrixStack) }
}
val mouse = Point(mouseX, mouseY) override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
windows.forEach { if (client != null) {
it.draw(matrixStack, mouse, delta) // workaround this.minecraft sometimes being null causing a crash
} renderBackground(matrixStack)
} }
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { val mouse = Point(mouseX, mouseY)
val window = windows.lastOrNull() windows.forEach {
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button)) it.draw(matrixStack, mouse, delta)
return if (result == true) { }
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK) }
true
} else {
false
}
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY) val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
val delta = Point(deltaX, deltaY) return if (result == true) {
val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button)) RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
return result == true true
} } else {
false
}
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button)) val startPoint = Point(mouseX, mouseY)
return result == true val delta = Point(deltaX, deltaY)
} val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button))
return result == true
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean { override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
val result = window?.mouseScrolled(Point(mouseX, mouseY), amount) val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return result == true return result == true
} }
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
if (keyCode == GLFW.GLFW_KEY_ESCAPE) { val window = windows.lastOrNull()
windows.lastOrNull()?.removeFromScreen() val result = window?.mouseScrolled(Point(mouseX, mouseY), amount)
return true return result == true
} else { }
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
}
override fun keyReleased(i: Int, j: Int, k: Int): Boolean { override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
return super.keyReleased(i, j, k) if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
} windows.lastOrNull()?.removeFromScreen()
return true
} else {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
}
override fun charTyped(char: Char, modifiers: Int): Boolean { override fun keyReleased(i: Int, j: Int, k: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) } return super.keyReleased(i, j, k)
if (findResponder { it.charTyped(char, modifiersSet) }) { }
return true
}
return super.charTyped(char, modifiers)
}
override fun shouldCloseOnEsc(): Boolean { override fun charTyped(char: Char, modifiers: Int): Boolean {
return false val modifiersSet by lazy { KeyModifiers(modifiers) }
} if (findResponder { it.charTyped(char, modifiersSet) }) {
return true
}
return super.charTyped(char, modifiers)
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
} }
fun AbstractCacaoScreen.findResponder(fn: (Responder) -> Boolean): Boolean { fun AbstractCacaoScreen.findResponder(fn: (Responder) -> Boolean): Boolean {
var responder = windows.lastOrNull()?.firstResponder var responder = windows.lastOrNull()?.firstResponder
while (responder != null) { while (responder != null) {
if (fn(responder)) { if (fn(responder)) {
return true return true
} }
responder = responder.nextResponder responder = responder.nextResponder
} }
return false return false
} }

View File

@ -11,22 +11,22 @@ import no.birkett.kiwi.Variable
* @author shadowfacts * @author shadowfacts
*/ */
class LayoutVariable( class LayoutVariable(
val view: View?, val view: View?,
val layoutGuide: LayoutGuide?, val layoutGuide: LayoutGuide?,
val property: String, val property: String,
): Variable("LayoutVariable") { ) : Variable("LayoutVariable") {
constructor(view: View, property: String): this(view, null, property) constructor(view: View, property: String) : this(view, null, property)
constructor(layoutGuide: LayoutGuide, property: String): this(null, layoutGuide, property) constructor(layoutGuide: LayoutGuide, property: String) : this(null, layoutGuide, property)
init { init {
if ((view == null) == (layoutGuide == null)) { if ((view == null) == (layoutGuide == null)) {
throw RuntimeException("LayoutVariable must be constructed with either a view or layout guide") throw RuntimeException("LayoutVariable must be constructed with either a view or layout guide")
} }
} }
override fun getName() = "${view ?: layoutGuide}.$property" override fun getName() = "${view ?: layoutGuide}.$property"
override fun toString() = "LayoutVariable(name=$name, value=$value)" override fun toString() = "LayoutVariable(name=$name, value=$value)"
} }

View File

@ -1,9 +1,12 @@
# Cacao # Cacao
Cacao is a UI framework for Fabric/Minecraft mods based on Apple's [Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API) Cacao is a UI framework for Fabric/Minecraft mods based on Apple's [Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API)
UI toolkit. UI toolkit.
## Architecture ## Architecture
### Screen ### Screen
A [CacaoScreen][] is the object that acts as the interface between Minecraft GUI code and the Cacao framework. A [CacaoScreen][] is the object that acts as the interface between Minecraft GUI code and the Cacao framework.
The CacaoScreen draws Cacao views on screen and passes Minecraft input events to the appropriate Views. The CacaoScreen The CacaoScreen draws Cacao views on screen and passes Minecraft input events to the appropriate Views. The CacaoScreen
@ -12,6 +15,7 @@ owns a group of [Window](#window) objects which are displayed on screen, one on
[CacaoScreen]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt [CacaoScreen]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt
### Window ### Window
A [Window][] object has a root [View Controller](#view-controller) that it displays on screen. A [Window][] object has a root [View Controller](#view-controller) that it displays on screen.
The Window occupies the entire screen space and translates events from the screen to the root View Controller's View. The Window occupies the entire screen space and translates events from the screen to the root View Controller's View.
@ -21,6 +25,7 @@ view hierarchy.
[Window]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/Window.kt [Window]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/Window.kt
### View Controller ### View Controller
A [ViewController][] object owns a view, receives lifecycle events for it, and is generally used to control the view. A [ViewController][] object owns a view, receives lifecycle events for it, and is generally used to control the view.
Each View Controller has a single root [View](#view) which in turn may have subviews. Each View Controller has a single root [View](#view) which in turn may have subviews.
@ -28,6 +33,7 @@ Each View Controller has a single root [View](#view) which in turn may have subv
[ViewController]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt [ViewController]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt
### View ### View
A [View][] object represents a single view on screen. It handles drawing, positioning, and directly handles input.
A [View][] object represents a single view on screen. It handles drawing, positioning, and directly handles input.
[View]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/view/View.kt [View]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/view/View.kt

View File

@ -12,89 +12,89 @@ import net.shadowfacts.cacao.window.Window
*/ */
interface Responder { interface Responder {
/** /**
* The window that this responder is part of. * The window that this responder is part of.
*/ */
val window: Window? val window: Window?
/** /**
* Whether this responder is the first responder of its window. * Whether this responder is the first responder of its window.
* *
* `false` if [window] is null. * `false` if [window] is null.
* *
* @see Window.firstResponder * @see Window.firstResponder
*/ */
val isFirstResponder: Boolean val isFirstResponder: Boolean
get() = window?.firstResponder === this get() = window?.firstResponder === this
/** /**
* The next responder in the chain after this. The next responder will receive an event if this responder did not * The next responder in the chain after this. The next responder will receive an event if this responder did not
* accept it. * accept it.
* *
* The next responder may be `null` if this responder is at the end of the chain. * The next responder may be `null` if this responder is at the end of the chain.
*/ */
val nextResponder: Responder? val nextResponder: Responder?
/** /**
* Makes this responder become the window's first responder. * Makes this responder become the window's first responder.
* @throws RuntimeException if [window] is null * @throws RuntimeException if [window] is null
* @see Window.firstResponder * @see Window.firstResponder
*/ */
fun becomeFirstResponder() { fun becomeFirstResponder() {
if (window == null) { if (window == null) {
throw RuntimeException("Cannot become first responder while not in Window") throw RuntimeException("Cannot become first responder while not in Window")
} }
window!!.firstResponder = this window!!.firstResponder = this
} }
/** /**
* Called immediately after this responder has become the window's first responder. * Called immediately after this responder has become the window's first responder.
* @see Window.firstResponder * @see Window.firstResponder
*/ */
fun didBecomeFirstResponder() {} fun didBecomeFirstResponder() {}
/** /**
* Removes this object as the window's first responder. * Removes this object as the window's first responder.
* @throws RuntimeException if [window] is null * @throws RuntimeException if [window] is null
* @see Window.firstResponder * @see Window.firstResponder
*/ */
fun resignFirstResponder() { fun resignFirstResponder() {
if (window == null) { if (window == null) {
throw RuntimeException("Cannot resign first responder while not in Window") throw RuntimeException("Cannot resign first responder while not in Window")
} }
window!!.firstResponder = null window!!.firstResponder = null
} }
/** /**
* Called immediately before this object is removed as the window's first responder. * Called immediately before this object is removed as the window's first responder.
* @see Window.firstResponder * @see Window.firstResponder
*/ */
fun didResignFirstResponder() {} fun didResignFirstResponder() {}
/** /**
* Called when a character has been typed. * Called when a character has been typed.
* *
* @param char The character that was typed. * @param char The character that was typed.
* @param modifiers The key modifiers that were held down when the character was typed. * @param modifiers The key modifiers that were held down when the character was typed.
* @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder. * @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder.
*/ */
fun charTyped(char: Char, modifiers: KeyModifiers): Boolean { fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
return false return false
} }
/** /**
* Called when a keyboard key is pressed. * Called when a keyboard key is pressed.
* *
* If the pressed key is a typed character, [charTyped] will also be called. The order in which the methods are * If the pressed key is a typed character, [charTyped] will also be called. The order in which the methods are
* invoked is undefined and should not be relied upon. * invoked is undefined and should not be relied upon.
* *
* @param keyCode The integer code of the key that was pressed. * @param keyCode The integer code of the key that was pressed.
* @param modifiers The key modifiers that were held down when the character was typed. * @param modifiers The key modifiers that were held down when the character was typed.
* @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder. * @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder.
* @see org.lwjgl.glfw.GLFW for key code constants * @see org.lwjgl.glfw.GLFW for key code constants
*/ */
fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean { fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return false return false
} }
} }

View File

@ -6,14 +6,14 @@ package net.shadowfacts.cacao.geometry
* @author shadowfacts * @author shadowfacts
*/ */
enum class Axis { enum class Axis {
HORIZONTAL, VERTICAL; HORIZONTAL, VERTICAL;
/** /**
* Gets the axis that is perpendicular to this one. * Gets the axis that is perpendicular to this one.
*/ */
val perpendicular: Axis val perpendicular: Axis
get() = when (this) { get() = when (this) {
HORIZONTAL -> VERTICAL HORIZONTAL -> VERTICAL
VERTICAL -> HORIZONTAL VERTICAL -> HORIZONTAL
} }
} }

View File

@ -6,16 +6,18 @@ package net.shadowfacts.cacao.geometry
* @author shadowfacts * @author shadowfacts
*/ */
enum class AxisPosition { enum class AxisPosition {
/** /**
* Top for vertical, left for horizontal. * Top for vertical, left for horizontal.
*/ */
LEADING, LEADING,
/**
* Center X/Y. /**
*/ * Center X/Y.
CENTER, */
/** CENTER,
* Bottom for vertical, right for horizontal.
*/ /**
TRAILING; * Bottom for vertical, right for horizontal.
} */
TRAILING;
}

View File

@ -10,40 +10,40 @@ import kotlin.math.pow
*/ */
data class BezierCurve(private val points: Array<Point>) { data class BezierCurve(private val points: Array<Point>) {
init { init {
if (points.size != 4) { if (points.size != 4) {
throw RuntimeException("Cubic bezier curve must have exactly four points") throw RuntimeException("Cubic bezier curve must have exactly four points")
} }
} }
fun point(time: Double): Point { fun point(time: Double): Point {
val x = coordinate(time, Axis.HORIZONTAL) val x = coordinate(time, Axis.HORIZONTAL)
val y = coordinate(time, Axis.VERTICAL) val y = coordinate(time, Axis.VERTICAL)
return Point(x, y) return Point(x, y)
} }
private fun coordinate(t: Double, axis: Axis): Double { private fun coordinate(t: Double, axis: Axis): Double {
// B(t)=(1-t)^3*p0+3(1-t)^2*t*p1+3(1-t)*t^2*p2+t^3*p3 // B(t)=(1-t)^3*p0+3(1-t)^2*t*p1+3(1-t)*t^2*p2+t^3*p3
val p0 = points[0][axis] val p0 = points[0][axis]
val p1 = points[1][axis] val p1 = points[1][axis]
val p2 = points[2][axis] val p2 = points[2][axis]
val p3 = points[3][axis] val p3 = points[3][axis]
return ((1 - t).pow(3) * p0) + (3 * (1 - t).pow(2) * t * p1) + (3 * (1 - t) * t.pow(2) * p2) + (t.pow(3) * p3) return ((1 - t).pow(3) * p0) + (3 * (1 - t).pow(2) * t * p1) + (3 * (1 - t) * t.pow(2) * p2) + (t.pow(3) * p3)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as BezierCurve other as BezierCurve
if (!points.contentEquals(other.points)) return false if (!points.contentEquals(other.points)) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return points.contentHashCode() return points.contentHashCode()
} }
} }

View File

@ -7,25 +7,25 @@ package net.shadowfacts.cacao.geometry
*/ */
data class Point(val x: Double, val y: Double) { data class Point(val x: Double, val y: Double) {
constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble()) constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble())
companion object { companion object {
val ORIGIN = Point(0.0, 0.0) val ORIGIN = Point(0.0, 0.0)
} }
operator fun plus(other: Point): Point { operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y) return Point(x + other.x, y + other.y)
} }
operator fun minus(other: Point): Point { operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y) return Point(x - other.x, y - other.y)
} }
operator fun get(axis: Axis): Double { operator fun get(axis: Axis): Double {
return when (axis) { return when (axis) {
Axis.HORIZONTAL -> x Axis.HORIZONTAL -> x
Axis.VERTICAL -> y Axis.VERTICAL -> y
} }
} }
} }

View File

@ -7,35 +7,35 @@ package net.shadowfacts.cacao.geometry
*/ */
data class Rect(val left: Double, val top: Double, val width: Double, val height: Double) { data class Rect(val left: Double, val top: Double, val width: Double, val height: Double) {
constructor(origin: Point, size: Size): this(origin.x, origin.y, size.width, size.height) constructor(origin: Point, size: Size) : this(origin.x, origin.y, size.width, size.height)
val right: Double by lazy { val right: Double by lazy {
left + width left + width
} }
val bottom: Double by lazy { val bottom: Double by lazy {
top + height top + height
} }
val midX: Double by lazy { val midX: Double by lazy {
left + width / 2 left + width / 2
} }
val midY: Double by lazy { val midY: Double by lazy {
top + height / 2 top + height / 2
} }
val origin: Point by lazy { val origin: Point by lazy {
Point(left, top) Point(left, top)
} }
val center: Point by lazy { val center: Point by lazy {
Point(midX, midY) Point(midX, midY)
} }
val size: Size by lazy { val size: Size by lazy {
Size(width, height) Size(width, height)
} }
operator fun contains(point: Point): Boolean { operator fun contains(point: Point): Boolean {
return point.x >= left && point.x < right && point.y >= top && point.y < bottom return point.x >= left && point.x < right && point.y >= top && point.y < bottom
} }
} }

View File

@ -11,27 +11,27 @@ package net.shadowfacts.cacao.util
*/ */
data class Color(val red: Int, val green: Int, val blue: Int, val alpha: Int = 255) { data class Color(val red: Int, val green: Int, val blue: Int, val alpha: Int = 255) {
/** /**
* Constructs a color from the packed RGB color. * Constructs a color from the packed RGB color.
*/ */
constructor(rgb: Int, alpha: Int = 255): this(rgb shr 16, (rgb shr 8) and 255, rgb and 255, alpha) constructor(rgb: Int, alpha: Int = 255) : this(rgb shr 16, (rgb shr 8) and 255, rgb and 255, alpha)
/** /**
* The ARGB packed representation of this color. * The ARGB packed representation of this color.
*/ */
val argb: Int val argb: Int
get() = ((alpha and 255) shl 24) or ((red and 255) shl 16) or ((green and 255) shl 8) or (blue and 255) get() = ((alpha and 255) shl 24) or ((red and 255) shl 16) or ((green and 255) shl 8) or (blue and 255)
companion object { companion object {
val CLEAR = Color(0, alpha = 0) val CLEAR = Color(0, alpha = 0)
val WHITE = Color(0xffffff) val WHITE = Color(0xffffff)
val BLACK = Color(0) val BLACK = Color(0)
val RED = Color(0xff0000) val RED = Color(0xff0000)
val GREEN = Color(0x00ff00) val GREEN = Color(0x00ff00)
val BLUE = Color(0x0000ff) val BLUE = Color(0x0000ff)
val MAGENTA = Color(0xfc46e4) val MAGENTA = Color(0xfc46e4)
val TEXT = Color(0x404040) val TEXT = Color(0x404040)
} }
} }

View File

@ -5,16 +5,16 @@ package net.shadowfacts.cacao.util
*/ */
object EnumHelper { object EnumHelper {
fun <E: Enum<E>> next(value: E): E { fun <E : Enum<E>> next(value: E): E {
val constants = value.declaringClass.enumConstants val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) + 1 val index = constants.indexOf(value) + 1
return if (index < constants.size) constants[index] else constants.first() return if (index < constants.size) constants[index] else constants.first()
} }
fun <E: Enum<E>> previous(value: E): E { fun <E : Enum<E>> previous(value: E): E {
val constants = value.declaringClass.enumConstants val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) - 1 val index = constants.indexOf(value) - 1
return if (index >= 0) constants[index] else constants.last() return if (index >= 0) constants[index] else constants.last()
} }
} }

View File

@ -10,26 +10,26 @@ import org.lwjgl.glfw.GLFW
*/ */
class KeyModifiers(val value: Int) { class KeyModifiers(val value: Int) {
val shift: Boolean val shift: Boolean
get() = this[GLFW.GLFW_MOD_SHIFT] get() = this[GLFW.GLFW_MOD_SHIFT]
val control: Boolean val control: Boolean
get() = this[GLFW.GLFW_MOD_CONTROL] get() = this[GLFW.GLFW_MOD_CONTROL]
val alt: Boolean val alt: Boolean
get() = this[GLFW.GLFW_MOD_ALT] get() = this[GLFW.GLFW_MOD_ALT]
val command: Boolean val command: Boolean
get() = this[GLFW.GLFW_MOD_SUPER] get() = this[GLFW.GLFW_MOD_SUPER]
val capsLock: Boolean val capsLock: Boolean
get() = this[GLFW.GLFW_MOD_CAPS_LOCK] get() = this[GLFW.GLFW_MOD_CAPS_LOCK]
val numLock: Boolean val numLock: Boolean
get() = this[GLFW.GLFW_MOD_NUM_LOCK] get() = this[GLFW.GLFW_MOD_NUM_LOCK]
private operator fun get(mod: Int): Boolean { private operator fun get(mod: Int): Boolean {
return (value and mod) == mod return (value and mod) == mod
} }
} }

View File

@ -15,19 +15,24 @@ import net.shadowfacts.cacao.view.View
* @author shadowfacts * @author shadowfacts
*/ */
class LayoutGuide( class LayoutGuide(
val owningView: View, val owningView: View,
) { ) {
val leftAnchor: LayoutVariable = LayoutVariable(this, "left") val leftAnchor: LayoutVariable = LayoutVariable(this, "left")
val rightAnchor: LayoutVariable = LayoutVariable(this, "right") val rightAnchor: LayoutVariable = LayoutVariable(this, "right")
val topAnchor: LayoutVariable = LayoutVariable(this, "top") val topAnchor: LayoutVariable = LayoutVariable(this, "top")
val bottomAnchor: LayoutVariable = LayoutVariable(this, "bottom") val bottomAnchor: LayoutVariable = LayoutVariable(this, "bottom")
val widthAnchor: LayoutVariable = LayoutVariable(this, "width") val widthAnchor: LayoutVariable = LayoutVariable(this, "width")
val heightAnchor: LayoutVariable = LayoutVariable(this, "height") val heightAnchor: LayoutVariable = LayoutVariable(this, "height")
val centerXAnchor: LayoutVariable = LayoutVariable(this, "centerX") val centerXAnchor: LayoutVariable = LayoutVariable(this, "centerX")
val centerYAnchor: LayoutVariable = LayoutVariable(this, "centerY") val centerYAnchor: LayoutVariable = LayoutVariable(this, "centerY")
val frame: Rect val frame: Rect
get() = Rect(leftAnchor.value - owningView.leftAnchor.value, topAnchor.value - owningView.topAnchor.value, widthAnchor.value, heightAnchor.value) get() = Rect(
leftAnchor.value - owningView.leftAnchor.value,
topAnchor.value - owningView.topAnchor.value,
widthAnchor.value,
heightAnchor.value
)
} }

View File

@ -14,43 +14,43 @@ import kotlin.NoSuchElementException
*/ */
object LowestCommonAncestor { object LowestCommonAncestor {
fun <Node> find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? { fun <Node> find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? {
@Suppress("NAME_SHADOWING") var node1: Node? = node1 @Suppress("NAME_SHADOWING") var node1: Node? = node1
@Suppress("NAME_SHADOWING") var node2: Node? = node2 @Suppress("NAME_SHADOWING") var node2: Node? = node2
val parent1 = LinkedList<Node>() val parent1 = LinkedList<Node>()
while (node1 != null) { while (node1 != null) {
parent1.push(node1) parent1.push(node1)
node1 = node1.parent() node1 = node1.parent()
} }
val parent2 = LinkedList<Node>() val parent2 = LinkedList<Node>()
while (node2 != null) { while (node2 != null) {
parent2.push(node2) parent2.push(node2)
node2 = node2.parent() node2 = node2.parent()
} }
// paths don't converge on the same root element // paths don't converge on the same root element
if (parent1.first != parent2.first) { if (parent1.first != parent2.first) {
return null return null
} }
var oldNode: Node? = null var oldNode: Node? = null
while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) { while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) {
oldNode = node1 oldNode = node1
node1 = parent1.popOrNull() node1 = parent1.popOrNull()
node2 = parent2.popOrNull() node2 = parent2.popOrNull()
} }
return if (node1 == node2) node1!! return if (node1 == node2) node1!!
else oldNode!! else oldNode!!
} }
} }
private fun <T> LinkedList<T>.popOrNull(): T? { private fun <T> LinkedList<T>.popOrNull(): T? {
return try { return try {
pop() pop()
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
null null
} }
} }

View File

@ -4,16 +4,16 @@ package net.shadowfacts.cacao.util
* @author shadowfacts * @author shadowfacts
*/ */
enum class MouseButton { enum class MouseButton {
LEFT, RIGHT, MIDDLE, UNKNOWN; LEFT, RIGHT, MIDDLE, UNKNOWN;
companion object { companion object {
fun fromMC(button: Int): MouseButton { fun fromMC(button: Int): MouseButton {
return when (button) { return when (button) {
0 -> LEFT 0 -> LEFT
1 -> RIGHT 1 -> RIGHT
2 -> MIDDLE 2 -> MIDDLE
else -> UNKNOWN else -> UNKNOWN
} }
} }
} }
} }

View File

@ -23,107 +23,153 @@ import kotlin.math.roundToInt
* *
* @author shadowfacts * @author shadowfacts
*/ */
object RenderHelper: DrawableHelper() { object RenderHelper : DrawableHelper() {
val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean() val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean()
// TODO: find a better place for this // TODO: find a better place for this
fun playSound(event: SoundEvent) { fun playSound(event: SoundEvent) {
if (disabled) return if (disabled) return
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(event, 1f)) MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(event, 1f))
} }
/** /**
* Draws a solid [rect] filled with the given [color]. * Draws a solid [rect] filled with the given [color].
*/ */
fun fill(matrixStack: MatrixStack, rect: Rect, color: Color) { fun fill(matrixStack: MatrixStack, rect: Rect, color: Color) {
if (disabled) return if (disabled) return
fill(matrixStack, rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb) fill(matrixStack, rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb)
} }
/** /**
* Binds and draws the given [texture] filling the [rect]. * Binds and draws the given [texture] filling the [rect].
*/ */
fun draw(matrixStack: MatrixStack, rect: Rect, texture: Texture) { fun draw(matrixStack: MatrixStack, rect: Rect, texture: Texture) {
if (disabled) return if (disabled) return
RenderSystem.setShader(GameRenderer::getPositionTexShader) RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, texture.location) RenderSystem.setShaderTexture(0, texture.location)
draw(matrixStack, rect.left, rect.top, texture.u, texture.v, rect.width, rect.height, texture.width, texture.height) draw(
} matrixStack,
rect.left,
rect.top,
texture.u,
texture.v,
rect.width,
rect.height,
texture.width,
texture.height
)
}
fun drawLine(start: Point, end: Point, z: Double, width: Float, color: Color) { fun drawLine(start: Point, end: Point, z: Double, width: Float, color: Color) {
if (disabled) return if (disabled) return
RenderSystem.lineWidth(width) RenderSystem.lineWidth(width)
val tessellator = Tessellator.getInstance() RenderSystem.enableBlend()
val buffer = tessellator.buffer RenderSystem.disableTexture()
buffer.begin(VertexFormat.DrawMode.LINES, VertexFormats.POSITION_COLOR) RenderSystem.defaultBlendFunc()
buffer.vertex(start.x, start.y, z).color(color).next() RenderSystem.setShader(GameRenderer::getPositionColorShader)
buffer.vertex(end.x, end.y, z).color(color).next() val tessellator = Tessellator.getInstance()
tessellator.draw() val buffer = tessellator.buffer
} buffer.begin(VertexFormat.DrawMode.LINES, VertexFormats.POSITION_COLOR)
buffer.vertex(start.x, start.y, z).color(color).next()
buffer.vertex(end.x, end.y, z).color(color).next()
tessellator.draw()
}
/** /**
* Draws the bound texture with the given screen and texture position and size. * Draws the bound texture with the given screen and texture position and size.
*/ */
fun draw(matrixStack: MatrixStack, x: Double, y: Double, u: Int, v: Int, width: Double, height: Double, textureWidth: Int, textureHeight: Int) { fun draw(
if (disabled) return matrixStack: MatrixStack,
val uStart = u.toFloat() / textureWidth x: Double,
val uEnd = (u + width).toFloat() / textureWidth y: Double,
val vStart = v.toFloat() / textureHeight u: Int,
val vEnd = (v + height).toFloat() / textureHeight v: Int,
drawTexturedQuad(matrixStack.peek().positionMatrix, x, x + width, y, y + height, 0.0, uStart, uEnd, vStart, vEnd) width: Double,
} height: Double,
textureWidth: Int,
textureHeight: Int
) {
if (disabled) return
val uStart = u.toFloat() / textureWidth
val uEnd = (u + width).toFloat() / textureWidth
val vStart = v.toFloat() / textureHeight
val vEnd = (v + height).toFloat() / textureHeight
drawTexturedQuad(
matrixStack.peek().positionMatrix,
x,
x + width,
y,
y + height,
0.0,
uStart,
uEnd,
vStart,
vEnd
)
}
// Copied from net.minecraft.client.gui.DrawableHelper // Copied from net.minecraft.client.gui.DrawableHelper
// TODO: use an access transformer to just call minecraft's impl // TODO: use an access transformer to just call minecraft's impl
private fun drawTexturedQuad(matrix: Matrix4f, x0: Double, x1: Double, y0: Double, y1: Double, z: Double, u0: Float, u1: Float, v0: Float, v1: Float) { private fun drawTexturedQuad(
val bufferBuilder = Tessellator.getInstance().buffer matrix: Matrix4f,
bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE) x0: Double,
bufferBuilder.vertex(matrix, x0.toFloat(), y1.toFloat(), z.toFloat()).texture(u0, v1).next() x1: Double,
bufferBuilder.vertex(matrix, x1.toFloat(), y1.toFloat(), z.toFloat()).texture(u1, v1).next() y0: Double,
bufferBuilder.vertex(matrix, x1.toFloat(), y0.toFloat(), z.toFloat()).texture(u1, v0).next() y1: Double,
bufferBuilder.vertex(matrix, x0.toFloat(), y0.toFloat(), z.toFloat()).texture(u0, v0).next() z: Double,
bufferBuilder.end() u0: Float,
BufferRenderer.draw(bufferBuilder) u1: Float,
} v0: Float,
v1: Float
) {
val bufferBuilder = Tessellator.getInstance().buffer
bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE)
bufferBuilder.vertex(matrix, x0.toFloat(), y1.toFloat(), z.toFloat()).texture(u0, v1).next()
bufferBuilder.vertex(matrix, x1.toFloat(), y1.toFloat(), z.toFloat()).texture(u1, v1).next()
bufferBuilder.vertex(matrix, x1.toFloat(), y0.toFloat(), z.toFloat()).texture(u1, v0).next()
bufferBuilder.vertex(matrix, x0.toFloat(), y0.toFloat(), z.toFloat()).texture(u0, v0).next()
bufferBuilder.end()
BufferRenderer.draw(bufferBuilder)
}
fun drawTooltip(matrixStack: MatrixStack, text: Text, mouse: Point) { fun drawTooltip(matrixStack: MatrixStack, text: Text, mouse: Point) {
drawTooltip(matrixStack, listOf(text.asOrderedText()), mouse) drawTooltip(matrixStack, listOf(text.asOrderedText()), mouse)
} }
fun drawTooltip(matrixStack: MatrixStack, texts: List<Text>, mouse: Point) { fun drawTooltip(matrixStack: MatrixStack, texts: List<Text>, mouse: Point) {
drawTooltip(matrixStack, texts.map(Text::asOrderedText), mouse) drawTooltip(matrixStack, texts.map(Text::asOrderedText), mouse)
} }
private val dummyScreen = object: Screen(LiteralText("")) { private val dummyScreen = object : Screen(LiteralText("")) {
init { init {
textRenderer = MinecraftClient.getInstance().textRenderer textRenderer = MinecraftClient.getInstance().textRenderer
itemRenderer = MinecraftClient.getInstance().itemRenderer itemRenderer = MinecraftClient.getInstance().itemRenderer
} }
} }
@JvmName("drawOrderedTooltip") @JvmName("drawOrderedTooltip")
fun drawTooltip(matrixStack: MatrixStack, texts: List<OrderedText>, mouse: Point) { fun drawTooltip(matrixStack: MatrixStack, texts: List<OrderedText>, mouse: Point) {
if (disabled) return if (disabled) return
if (texts.isEmpty()) return if (texts.isEmpty()) return
val client = MinecraftClient.getInstance() val client = MinecraftClient.getInstance()
dummyScreen.width = client.window.scaledWidth dummyScreen.width = client.window.scaledWidth
dummyScreen.height = client.window.scaledHeight dummyScreen.height = client.window.scaledHeight
dummyScreen.renderOrderedTooltip(matrixStack, texts, mouse.x.roundToInt(), mouse.y.roundToInt()) dummyScreen.renderOrderedTooltip(matrixStack, texts, mouse.x.roundToInt(), mouse.y.roundToInt())
} }
/** /**
* @see org.lwjgl.opengl.GL11.glColor4f * @see org.lwjgl.opengl.GL11.glColor4f
*/ */
fun color(r: Float, g: Float, b: Float, alpha: Float) { fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return if (disabled) return
RenderSystem.setShaderColor(r, g, b, alpha) RenderSystem.setShaderColor(r, g, b, alpha)
} }
private fun VertexConsumer.color(color: Color): VertexConsumer { private fun VertexConsumer.color(color: Color): VertexConsumer {
return color(color.red, color.green, color.blue, color.alpha) return color(color.red, color.green, color.blue, color.alpha)
} }
} }

View File

@ -10,5 +10,5 @@ import no.birkett.kiwi.Variable
* @author shadowfacts * @author shadowfacts
*/ */
fun Constraint.getVariables(): List<Variable> { fun Constraint.getVariables(): List<Variable> {
return expression.terms.map(Term::getVariable) return expression.terms.map(Term::getVariable)
} }

View File

@ -5,20 +5,20 @@ import kotlin.reflect.KProperty
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class ObservableLateInitProperty<T: Any>(val observer: (T) -> Unit) { class ObservableLateInitProperty<T : Any>(val observer: (T) -> Unit) {
lateinit var storage: T lateinit var storage: T
val isInitialized: Boolean val isInitialized: Boolean
get() = this::storage.isInitialized get() = this::storage.isInitialized
operator fun getValue(thisRef: Any, property: KProperty<*>): T { operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return storage return storage
} }
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) { operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
storage = value storage = value
observer(value) observer(value)
} }
} }

View File

@ -7,14 +7,14 @@ import kotlin.reflect.KProperty
*/ */
class ObservableLazyProperty<Value>(val create: () -> Value, val onCreate: () -> Unit) { class ObservableLazyProperty<Value>(val create: () -> Value, val onCreate: () -> Unit) {
var storage: Value? = null var storage: Value? = null
operator fun getValue(thisRef: Any, property: KProperty<*>): Value { operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
if (storage == null) { if (storage == null) {
storage = create() storage = create()
onCreate() onCreate()
} }
return storage!! return storage!!
} }
} }

View File

@ -6,19 +6,19 @@ import kotlin.reflect.KProperty
* @author shadowfacts * @author shadowfacts
*/ */
class ResettableLazyProperty<Value>(val initializer: () -> Value) { class ResettableLazyProperty<Value>(val initializer: () -> Value) {
var value: Value? = null var value: Value? = null
val isInitialized: Boolean val isInitialized: Boolean
get() = value != null get() = value != null
operator fun getValue(thisRef: Any, property: KProperty<*>): Value { operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
if (value == null) { if (value == null) {
value = initializer() value = initializer()
} }
return value!! return value!!
} }
fun reset() { fun reset() {
value = null value = null
} }
} }

View File

@ -15,47 +15,53 @@ import net.minecraft.util.Identifier
* @param centerWidth The width of the center patch. * @param centerWidth The width of the center patch.
* @param centerHeight The height of the center patch. * @param centerHeight The height of the center patch.
*/ */
data class NinePatchTexture(val texture: Texture, val cornerWidth: Int, val cornerHeight: Int, val centerWidth: Int, val centerHeight: Int) { data class NinePatchTexture(
val texture: Texture,
val cornerWidth: Int,
val cornerHeight: Int,
val centerWidth: Int,
val centerHeight: Int
) {
companion object { companion object {
val PANEL_BG = NinePatchTexture(Texture(Identifier("textures/gui/demo_background.png"), 0, 0), 5, 5, 238, 156) val PANEL_BG = NinePatchTexture(Texture(Identifier("textures/gui/demo_background.png"), 0, 0), 5, 5, 238, 156)
val BUTTON_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 66), 3, 3, 194, 14) val BUTTON_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 66), 3, 3, 194, 14)
val BUTTON_HOVERED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 86), 3, 3, 194, 14) val BUTTON_HOVERED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 86), 3, 3, 194, 14)
val BUTTON_DISABLED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 46), 3, 3, 194, 14) val BUTTON_DISABLED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 46), 3, 3, 194, 14)
} }
// Corners // Corners
val topLeft by lazy { val topLeft by lazy {
texture texture
} }
val topRight by lazy { val topRight by lazy {
Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height) Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height)
} }
val bottomLeft by lazy { val bottomLeft by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height) Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height)
} }
val bottomRight by lazy { val bottomRight by lazy {
Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height) Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height)
} }
// Edges // Edges
val topMiddle by lazy { val topMiddle by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height) Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height)
} }
val bottomMiddle by lazy { val bottomMiddle by lazy {
Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height) Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height)
} }
val leftMiddle by lazy { val leftMiddle by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height) Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height)
} }
val rightMiddle by lazy { val rightMiddle by lazy {
Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height) Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height)
} }
// Center // Center
val center by lazy { val center by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v + cornerHeight, texture.width, texture.height) Texture(texture.location, texture.u + cornerWidth, texture.v + cornerHeight, texture.width, texture.height)
} }
} }

View File

@ -9,30 +9,30 @@ import net.shadowfacts.cacao.util.RenderHelper
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class BezierCurveView(val curve: BezierCurve): View() { class BezierCurveView(val curve: BezierCurve) : View() {
private val points by lazy { private val points by lazy {
val step = 0.05 val step = 0.05
var t = 0.0 var t = 0.0
val points = mutableListOf<Point>() val points = mutableListOf<Point>()
while (t <= 1) { while (t <= 1) {
points.add(curve.point(t)) points.add(curve.point(t))
t += step t += step
} }
points points
} }
var lineWidth = 3f var lineWidth = 3f
var lineColor = Color.BLACK var lineColor = Color.BLACK
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push() matrixStack.push()
matrixStack.scale(bounds.width.toFloat(), bounds.height.toFloat(), 1f) matrixStack.scale(bounds.width.toFloat(), bounds.height.toFloat(), 1f)
for ((index, point) in points.withIndex()) { for ((index, point) in points.withIndex()) {
val next = points.getOrNull(index + 1) ?: break val next = points.getOrNull(index + 1) ?: break
RenderHelper.drawLine(point, next, zIndex, lineWidth, lineColor) RenderHelper.drawLine(point, next, zIndex, lineWidth, lineColor)
} }
matrixStack.pop() matrixStack.pop()
} }
} }

View File

@ -14,103 +14,103 @@ import net.shadowfacts.kiwidsl.dsl
* @author shadowfacts * @author shadowfacts
*/ */
class DialogView( class DialogView(
val title: Text, val title: Text,
val message: Text, val message: Text,
val buttonTypes: Array<ButtonType>, val buttonTypes: Array<ButtonType>,
val iconTexture: Texture?, val iconTexture: Texture?,
val buttonCallback: (ButtonType, Window) -> Unit val buttonCallback: (ButtonType, Window) -> Unit
): View() { ) : View() {
interface ButtonType { interface ButtonType {
val localizedName: Text val localizedName: Text
} }
enum class DefaultButtonType: ButtonType { enum class DefaultButtonType : ButtonType {
CANCEL, CONFIRM, OK, CLOSE; CANCEL, CONFIRM, OK, CLOSE;
override val localizedName: Text override val localizedName: Text
get() = LiteralText(name.lowercase().replaceFirstChar(Char::titlecase)) // todo: actually localize me get() = LiteralText(name.lowercase().replaceFirstChar(Char::titlecase)) // todo: actually localize me
} }
private lateinit var background: NinePatchView private lateinit var background: NinePatchView
private lateinit var hStack: StackView private lateinit var hStack: StackView
private var iconView: TextureView? = null private var iconView: TextureView? = null
private lateinit var vStack: StackView private lateinit var vStack: StackView
private lateinit var messageLabel: Label private lateinit var messageLabel: Label
private var buttonContainer: View? = null private var buttonContainer: View? = null
private var buttonStack: StackView? = null private var buttonStack: StackView? = null
override fun wasAdded() { override fun wasAdded() {
background = addSubview(NinePatchView(NinePatchTexture.PANEL_BG).apply { zIndex = -1.0 }) background = addSubview(NinePatchView(NinePatchTexture.PANEL_BG).apply { zIndex = -1.0 })
hStack = addSubview(StackView(Axis.HORIZONTAL, StackView.Distribution.LEADING, spacing = 8.0)) hStack = addSubview(StackView(Axis.HORIZONTAL, StackView.Distribution.LEADING, spacing = 8.0))
if (iconTexture != null) { if (iconTexture != null) {
iconView = hStack.addArrangedSubview(TextureView(iconTexture)) iconView = hStack.addArrangedSubview(TextureView(iconTexture))
} }
vStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL, spacing = 4.0)) vStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL, spacing = 4.0))
vStack.addArrangedSubview(Label(title, shadow = false).apply { vStack.addArrangedSubview(Label(title, shadow = false).apply {
textColor = Color(0x404040) textColor = Color(0x404040)
}) })
messageLabel = vStack.addArrangedSubview(Label(message, shadow = false).apply { messageLabel = vStack.addArrangedSubview(Label(message, shadow = false).apply {
textColor = Color(0x404040) textColor = Color(0x404040)
}) })
if (buttonTypes.isNotEmpty()) { if (buttonTypes.isNotEmpty()) {
buttonContainer = vStack.addArrangedSubview(View()) buttonContainer = vStack.addArrangedSubview(View())
buttonStack = buttonContainer!!.addSubview(StackView(Axis.HORIZONTAL)) buttonStack = buttonContainer!!.addSubview(StackView(Axis.HORIZONTAL))
for (type in buttonTypes) { for (type in buttonTypes) {
buttonStack!!.addArrangedSubview(Button(Label(type.localizedName)).apply { buttonStack!!.addArrangedSubview(Button(Label(type.localizedName)).apply {
handler = { handler = {
this@DialogView.buttonCallback(type, this@DialogView.window!!) this@DialogView.buttonCallback(type, this@DialogView.window!!)
} }
}) })
} }
} }
super.wasAdded() super.wasAdded()
} }
override fun createInternalConstraints() { override fun createInternalConstraints() {
super.createInternalConstraints() super.createInternalConstraints()
solver.dsl { solver.dsl {
centerXAnchor equalTo window!!.centerXAnchor centerXAnchor equalTo window!!.centerXAnchor
centerYAnchor equalTo window!!.centerYAnchor centerYAnchor equalTo window!!.centerYAnchor
widthAnchor greaterThanOrEqualTo 175 widthAnchor greaterThanOrEqualTo 175
background.leftAnchor equalTo leftAnchor - 8 background.leftAnchor equalTo leftAnchor - 8
background.rightAnchor equalTo rightAnchor + 8 background.rightAnchor equalTo rightAnchor + 8
background.topAnchor equalTo topAnchor - 8 background.topAnchor equalTo topAnchor - 8
background.bottomAnchor equalTo bottomAnchor + 8 background.bottomAnchor equalTo bottomAnchor + 8
hStack.leftAnchor equalTo leftAnchor hStack.leftAnchor equalTo leftAnchor
hStack.rightAnchor equalTo rightAnchor hStack.rightAnchor equalTo rightAnchor
hStack.topAnchor equalTo topAnchor hStack.topAnchor equalTo topAnchor
hStack.bottomAnchor equalTo bottomAnchor hStack.bottomAnchor equalTo bottomAnchor
if (iconView != null) { if (iconView != null) {
hStack.bottomAnchor greaterThanOrEqualTo iconView!!.bottomAnchor hStack.bottomAnchor greaterThanOrEqualTo iconView!!.bottomAnchor
} }
hStack.bottomAnchor greaterThanOrEqualTo vStack.bottomAnchor hStack.bottomAnchor greaterThanOrEqualTo vStack.bottomAnchor
if (iconView != null) { if (iconView != null) {
iconView!!.widthAnchor equalTo 30 iconView!!.widthAnchor equalTo 30
iconView!!.heightAnchor equalTo 30 iconView!!.heightAnchor equalTo 30
} }
messageLabel.heightAnchor greaterThanOrEqualTo 50 messageLabel.heightAnchor greaterThanOrEqualTo 50
if (buttonContainer != null) { if (buttonContainer != null) {
buttonStack!!.heightAnchor equalTo buttonContainer!!.heightAnchor buttonStack!!.heightAnchor equalTo buttonContainer!!.heightAnchor
buttonStack!!.centerYAnchor equalTo buttonContainer!!.centerYAnchor buttonStack!!.centerYAnchor equalTo buttonContainer!!.centerYAnchor
buttonStack!!.rightAnchor equalTo buttonContainer!!.rightAnchor buttonStack!!.rightAnchor equalTo buttonContainer!!.rightAnchor
} }
} }
} }
} }

View File

@ -26,117 +26,117 @@ import kotlin.math.min
* wrapping. * wrapping.
*/ */
class Label( class Label(
text: Text, text: Text,
var shadow: Boolean = false, var shadow: Boolean = false,
val maxLines: Int = 0, val maxLines: Int = 0,
val wrappingMode: WrappingMode = WrappingMode.WRAP, val wrappingMode: WrappingMode = WrappingMode.WRAP,
var textAlignment: TextAlignment = TextAlignment.LEFT var textAlignment: TextAlignment = TextAlignment.LEFT
): View() { ) : View() {
companion object { companion object {
private val textRenderer: TextRenderer private val textRenderer: TextRenderer
get() = MinecraftClient.getInstance().textRenderer get() = MinecraftClient.getInstance().textRenderer
} }
enum class WrappingMode { enum class WrappingMode {
WRAP, NO_WRAP WRAP, NO_WRAP
} }
enum class TextAlignment { enum class TextAlignment {
LEFT, CENTER, RIGHT LEFT, CENTER, RIGHT
} }
constructor( constructor(
text: String, text: String,
shadow: Boolean = false, shadow: Boolean = false,
maxLines: Int = 0, maxLines: Int = 0,
wrappingMode: WrappingMode = WrappingMode.WRAP, wrappingMode: WrappingMode = WrappingMode.WRAP,
textAlignment: TextAlignment = TextAlignment.LEFT, textAlignment: TextAlignment = TextAlignment.LEFT,
): this(LiteralText(text), shadow, maxLines, wrappingMode, textAlignment) ) : this(LiteralText(text), shadow, maxLines, wrappingMode, textAlignment)
/** /**
* The text of this label. Mutating this field will update the intrinsic content size and trigger a layout. * The text of this label. Mutating this field will update the intrinsic content size and trigger a layout.
*/ */
var text: Text = text var text: Text = text
set(value) { set(value) {
field = value field = value
// todo: uhhhh // todo: uhhhh
updateIntrinsicContentSize(true) updateIntrinsicContentSize(true)
// todo: setNeedsLayout instead of force unwrapping window // todo: setNeedsLayout instead of force unwrapping window
window!!.layout() window!!.layout()
} }
private lateinit var lines: List<OrderedText> private lateinit var lines: List<OrderedText>
var textColor = Color.WHITE var textColor = Color.WHITE
set(value) { set(value) {
field = value field = value
textColorARGB = value.argb textColorARGB = value.argb
} }
private var textColorARGB: Int = textColor.argb private var textColorARGB: Int = textColor.argb
override fun wasAdded() { override fun wasAdded() {
super.wasAdded() super.wasAdded()
updateIntrinsicContentSize(false) updateIntrinsicContentSize(false)
} }
private fun updateIntrinsicContentSize(canWrap: Boolean, isFromDidLayout: Boolean = false): Boolean { private fun updateIntrinsicContentSize(canWrap: Boolean, isFromDidLayout: Boolean = false): Boolean {
if (RenderHelper.disabled) return false if (RenderHelper.disabled) return false
val oldSize = intrinsicContentSize val oldSize = intrinsicContentSize
// don't wrap until we've laid out without wrapping to ensure the current bounds reflect the maximum available space // don't wrap until we've laid out without wrapping to ensure the current bounds reflect the maximum available space
if (wrappingMode == WrappingMode.WRAP && canWrap && hasSolver && isFromDidLayout) { if (wrappingMode == WrappingMode.WRAP && canWrap && hasSolver && isFromDidLayout) {
val lines = textRenderer.wrapLines(text, bounds.width.toInt()) val lines = textRenderer.wrapLines(text, bounds.width.toInt())
val height = (if (maxLines == 0) lines.size else min(lines.size, maxLines)) * textRenderer.fontHeight val height = (if (maxLines == 0) lines.size else min(lines.size, maxLines)) * textRenderer.fontHeight
intrinsicContentSize = Size(bounds.width, height.toDouble()) intrinsicContentSize = Size(bounds.width, height.toDouble())
} else { } else {
val width = textRenderer.getWidth(text) val width = textRenderer.getWidth(text)
val height = textRenderer.fontHeight val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble()) intrinsicContentSize = Size(width.toDouble(), height.toDouble())
} }
return oldSize != intrinsicContentSize return oldSize != intrinsicContentSize
} }
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
if (!this::lines.isInitialized) { if (!this::lines.isInitialized) {
computeLines() computeLines()
} }
for (i in 0 until lines.size) { for (i in 0 until lines.size) {
val x = when (textAlignment) { val x = when (textAlignment) {
TextAlignment.LEFT -> 0.0 TextAlignment.LEFT -> 0.0
TextAlignment.CENTER -> (bounds.width - textRenderer.getWidth(lines[i])) / 2 TextAlignment.CENTER -> (bounds.width - textRenderer.getWidth(lines[i])) / 2
TextAlignment.RIGHT -> bounds.width - textRenderer.getWidth(lines[i]) TextAlignment.RIGHT -> bounds.width - textRenderer.getWidth(lines[i])
} }
val y = i * textRenderer.fontHeight val y = i * textRenderer.fontHeight
if (shadow) { if (shadow) {
textRenderer.drawWithShadow(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB) textRenderer.drawWithShadow(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
} else { } else {
textRenderer.draw(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB) textRenderer.draw(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
} }
} }
} }
override fun didLayout() { override fun didLayout() {
super.didLayout() super.didLayout()
computeLines() computeLines()
if (updateIntrinsicContentSize(true, true)) { if (updateIntrinsicContentSize(true, true)) {
// if the intrinsic content size changes, relayout // if the intrinsic content size changes, relayout
window!!.layout() window!!.layout()
} }
} }
private fun computeLines() { private fun computeLines() {
if (wrappingMode == WrappingMode.WRAP) { if (wrappingMode == WrappingMode.WRAP) {
var lines = textRenderer.wrapLines(text, bounds.width.toInt()) var lines = textRenderer.wrapLines(text, bounds.width.toInt())
if (maxLines > 0 && maxLines < lines.size) { if (maxLines > 0 && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines) lines = lines.dropLast(lines.size - maxLines)
} }
this.lines = lines this.lines = lines
} else { } else {
this.lines = listOf(text.asOrderedText()) this.lines = listOf(text.asOrderedText())
} }
} }
} }

View File

@ -17,121 +17,260 @@ import kotlin.math.roundToInt
* @author shadowfacts * @author shadowfacts
* @param ninePatch The nine patch texture that this view will draw. * @param ninePatch The nine patch texture that this view will draw.
*/ */
open class NinePatchView(val ninePatch: NinePatchTexture): View() { open class NinePatchView(val ninePatch: NinePatchTexture) : View() {
// Corners // Corners
private val topLeftDelegate = ResettableLazyProperty { private val topLeftDelegate = ResettableLazyProperty {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
} }
protected open val topLeft by topLeftDelegate protected open val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty { private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(
} bounds.width - ninePatch.cornerWidth,
protected open val topRight by topRightDelegate 0.0,
ninePatch.cornerWidth.toDouble(),
ninePatch.cornerHeight.toDouble()
)
}
protected open val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty { private val bottomLeftDelegate = ResettableLazyProperty {
Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(
} 0.0,
protected open val bottomLeft by bottomLeftDelegate bounds.height - ninePatch.cornerHeight,
ninePatch.cornerWidth.toDouble(),
ninePatch.cornerHeight.toDouble()
)
}
protected open val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty { private val bottomRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(
} bounds.width - ninePatch.cornerWidth,
protected open val bottomRight by bottomRightDelegate bounds.height - ninePatch.cornerHeight,
ninePatch.cornerWidth.toDouble(),
ninePatch.cornerHeight.toDouble()
)
}
protected open val bottomRight by bottomRightDelegate
// Edges // Edges
private val topMiddleDelegate = ResettableLazyProperty { private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble()) Rect(
} ninePatch.cornerWidth.toDouble(),
protected open val topMiddle by topMiddleDelegate topLeft.top,
bounds.width - 2 * ninePatch.cornerWidth,
ninePatch.cornerHeight.toDouble()
)
}
protected open val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty { private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height) Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
} }
protected open val bottomMiddle by bottomMiddleDelegate protected open val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty { private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight) Rect(
} topLeft.left,
protected open val leftMiddle by leftMiddleDelegate ninePatch.cornerHeight.toDouble(),
ninePatch.cornerWidth.toDouble(),
bounds.height - 2 * ninePatch.cornerHeight
)
}
protected open val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty { private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height) Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
} }
protected open val rightMiddle by rightMiddleDelegate protected open val rightMiddle by rightMiddleDelegate
// Center // Center
private val centerDelegate = ResettableLazyProperty { private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height) Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
} }
protected open val center by centerDelegate protected open val center by centerDelegate
private val delegates = listOf(topLeftDelegate, topRightDelegate, bottomLeftDelegate, bottomRightDelegate, topMiddleDelegate, bottomMiddleDelegate, leftMiddleDelegate, rightMiddleDelegate, centerDelegate) private val delegates = listOf(
topLeftDelegate,
topRightDelegate,
bottomLeftDelegate,
bottomRightDelegate,
topMiddleDelegate,
bottomMiddleDelegate,
leftMiddleDelegate,
rightMiddleDelegate,
centerDelegate
)
override fun didLayout() { override fun didLayout() {
super.didLayout() super.didLayout()
delegates.forEach(ResettableLazyProperty<Rect>::reset) delegates.forEach(ResettableLazyProperty<Rect>::reset)
} }
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
drawCorners(matrixStack) drawCorners(matrixStack)
drawEdges(matrixStack) drawEdges(matrixStack)
drawCenter(matrixStack) drawCenter(matrixStack)
} }
private fun drawCorners(matrixStack: MatrixStack) { private fun drawCorners(matrixStack: MatrixStack) {
RenderHelper.draw(matrixStack, topLeft, ninePatch.topLeft) RenderHelper.draw(matrixStack, topLeft, ninePatch.topLeft)
RenderHelper.draw(matrixStack, topRight, ninePatch.topRight) RenderHelper.draw(matrixStack, topRight, ninePatch.topRight)
RenderHelper.draw(matrixStack, bottomLeft, ninePatch.bottomLeft) RenderHelper.draw(matrixStack, bottomLeft, ninePatch.bottomLeft)
RenderHelper.draw(matrixStack, bottomRight, ninePatch.bottomRight) RenderHelper.draw(matrixStack, bottomRight, ninePatch.bottomRight)
} }
private fun drawEdges(matrixStack: MatrixStack) { private fun drawEdges(matrixStack: MatrixStack) {
// Horizontal // Horizontal
for (i in 0 until (topMiddle.width.roundToInt() / ninePatch.centerWidth)) { for (i in 0 until (topMiddle.width.roundToInt() / ninePatch.centerWidth)) {
RenderHelper.draw(matrixStack, topMiddle.left + i * ninePatch.centerWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, ninePatch.centerWidth.toDouble(), topMiddle.height, ninePatch.texture.width, ninePatch.texture.height) RenderHelper.draw(
RenderHelper.draw(matrixStack, bottomMiddle.left + i * ninePatch.centerWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, ninePatch.centerWidth.toDouble(), bottomMiddle.height, ninePatch.texture.width, ninePatch.texture.height) matrixStack,
} topMiddle.left + i * ninePatch.centerWidth,
val remWidth = topMiddle.width.roundToInt() % ninePatch.centerWidth topMiddle.top,
if (remWidth > 0) { ninePatch.topMiddle.u,
RenderHelper.draw(matrixStack, topMiddle.right - remWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, remWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height) ninePatch.topMiddle.v,
RenderHelper.draw(matrixStack, bottomMiddle.right - remWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, remWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height) ninePatch.centerWidth.toDouble(),
} topMiddle.height,
ninePatch.texture.width,
ninePatch.texture.height
)
RenderHelper.draw(
matrixStack,
bottomMiddle.left + i * ninePatch.centerWidth,
bottomMiddle.top,
ninePatch.bottomMiddle.u,
ninePatch.bottomMiddle.v,
ninePatch.centerWidth.toDouble(),
bottomMiddle.height,
ninePatch.texture.width,
ninePatch.texture.height
)
}
val remWidth = topMiddle.width.roundToInt() % ninePatch.centerWidth
if (remWidth > 0) {
RenderHelper.draw(
matrixStack,
topMiddle.right - remWidth,
topMiddle.top,
ninePatch.topMiddle.u,
ninePatch.topMiddle.v,
remWidth.toDouble(),
ninePatch.cornerHeight.toDouble(),
ninePatch.texture.width,
ninePatch.texture.height
)
RenderHelper.draw(
matrixStack,
bottomMiddle.right - remWidth,
bottomMiddle.top,
ninePatch.bottomMiddle.u,
ninePatch.bottomMiddle.v,
remWidth.toDouble(),
ninePatch.cornerHeight.toDouble(),
ninePatch.texture.width,
ninePatch.texture.height
)
}
// Vertical // Vertical
for (i in 0 until (leftMiddle.height.roundToInt() / ninePatch.centerHeight)) { for (i in 0 until (leftMiddle.height.roundToInt() / ninePatch.centerHeight)) {
RenderHelper.draw(matrixStack, leftMiddle.left, leftMiddle.top + i * ninePatch.centerHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height) RenderHelper.draw(
RenderHelper.draw(matrixStack, rightMiddle.left, rightMiddle.top + i * ninePatch.centerHeight, ninePatch.rightMiddle.u, ninePatch.rightMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height) matrixStack,
} leftMiddle.left,
val remHeight = leftMiddle.height.roundToInt() % ninePatch.centerHeight leftMiddle.top + i * ninePatch.centerHeight,
if (remHeight > 0) { ninePatch.leftMiddle.u,
RenderHelper.draw(matrixStack, leftMiddle.left, leftMiddle.bottom - remHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), remHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height) ninePatch.leftMiddle.v,
RenderHelper.draw(matrixStack, rightMiddle.left, rightMiddle.bottom - remHeight, ninePatch.rightMiddle.u, ninePatch.rightMiddle.v, ninePatch.cornerWidth.toDouble(), remHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height) ninePatch.cornerWidth.toDouble(),
} ninePatch.centerHeight.toDouble(),
} ninePatch.texture.width,
ninePatch.texture.height
)
RenderHelper.draw(
matrixStack,
rightMiddle.left,
rightMiddle.top + i * ninePatch.centerHeight,
ninePatch.rightMiddle.u,
ninePatch.rightMiddle.v,
ninePatch.cornerWidth.toDouble(),
ninePatch.centerHeight.toDouble(),
ninePatch.texture.width,
ninePatch.texture.height
)
}
val remHeight = leftMiddle.height.roundToInt() % ninePatch.centerHeight
if (remHeight > 0) {
RenderHelper.draw(
matrixStack,
leftMiddle.left,
leftMiddle.bottom - remHeight,
ninePatch.leftMiddle.u,
ninePatch.leftMiddle.v,
ninePatch.cornerWidth.toDouble(),
remHeight.toDouble(),
ninePatch.texture.width,
ninePatch.texture.height
)
RenderHelper.draw(
matrixStack,
rightMiddle.left,
rightMiddle.bottom - remHeight,
ninePatch.rightMiddle.u,
ninePatch.rightMiddle.v,
ninePatch.cornerWidth.toDouble(),
remHeight.toDouble(),
ninePatch.texture.width,
ninePatch.texture.height
)
}
}
private fun drawCenter(matrixStack: MatrixStack) { private fun drawCenter(matrixStack: MatrixStack) {
for (i in 0 until (center.height.roundToInt() / ninePatch.centerHeight)) { for (i in 0 until (center.height.roundToInt() / ninePatch.centerHeight)) {
drawCenterRow(matrixStack, center.top + i * ninePatch.centerHeight.toDouble(), ninePatch.centerHeight.toDouble()) drawCenterRow(
} matrixStack,
val remHeight = center.height.roundToInt() % ninePatch.centerHeight center.top + i * ninePatch.centerHeight.toDouble(),
if (remHeight > 0) { ninePatch.centerHeight.toDouble()
drawCenterRow(matrixStack, center.bottom - remHeight, remHeight.toDouble()) )
} }
} val remHeight = center.height.roundToInt() % ninePatch.centerHeight
if (remHeight > 0) {
drawCenterRow(matrixStack, center.bottom - remHeight, remHeight.toDouble())
}
}
private fun drawCenterRow(matrixStack: MatrixStack, y: Double, height: Double) { private fun drawCenterRow(matrixStack: MatrixStack, y: Double, height: Double) {
for (i in 0 until (center.width.roundToInt() / ninePatch.centerWidth)) { for (i in 0 until (center.width.roundToInt() / ninePatch.centerWidth)) {
RenderHelper.draw(matrixStack, center.left + i * ninePatch.centerWidth, y, ninePatch.center.u, ninePatch.center.v, ninePatch.centerWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height) RenderHelper.draw(
} matrixStack,
val remWidth = center.width.roundToInt() % ninePatch.centerWidth center.left + i * ninePatch.centerWidth,
if (remWidth > 0) { y,
RenderHelper.draw(matrixStack, center.right - remWidth, y, ninePatch.center.u, ninePatch.center.v, remWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height) ninePatch.center.u,
} ninePatch.center.v,
} ninePatch.centerWidth.toDouble(),
height,
ninePatch.texture.width,
ninePatch.texture.height
)
}
val remWidth = center.width.roundToInt() % ninePatch.centerWidth
if (remWidth > 0) {
RenderHelper.draw(
matrixStack,
center.right - remWidth,
y,
ninePatch.center.u,
ninePatch.center.v,
remWidth.toDouble(),
height,
ninePatch.texture.width,
ninePatch.texture.height
)
}
}
} }

View File

@ -22,251 +22,265 @@ import java.util.*
* @param spacing The distance between arranged subviews along the primary axis. * @param spacing The distance between arranged subviews along the primary axis.
*/ */
open class StackView( open class StackView(
val axis: Axis, val axis: Axis,
val distribution: Distribution = Distribution.FILL, val distribution: Distribution = Distribution.FILL,
val spacing: Double = 0.0 val spacing: Double = 0.0
): View() { ) : View() {
// the internal, mutable list of arranged subviews // the internal, mutable list of arranged subviews
private val _arrangedSubviews = LinkedList<View>() private val _arrangedSubviews = LinkedList<View>()
/**
* The list of arranged subviews belonging to this stack view.
* This list should never be mutated directly, only be calling the [addArrangedSubview]/[removeArrangedSubview]
* methods.
*/
val arrangedSubviews: List<View> = _arrangedSubviews
private var leadingConnection: Constraint? = null /**
private var trailingConnection: Constraint? = null * The list of arranged subviews belonging to this stack view.
private var arrangedSubviewConnections = mutableListOf<Constraint>() * This list should never be mutated directly, only be calling the [addArrangedSubview]/[removeArrangedSubview]
* methods.
*/
val arrangedSubviews: List<View> = _arrangedSubviews
/** private var leadingConnection: Constraint? = null
* Adds an arranged subview to this view. private var trailingConnection: Constraint? = null
* Arranged subviews are laid out according to the stack. If you wish to add a subview that is laid out separately, private var arrangedSubviewConnections = mutableListOf<Constraint>()
* use the normal [addSubview] method.
*
* @param view The view to add.
* @param index The index in this stack to add the view at.
* By default, adds the view to the end of the stack.
* @return The view that was added, as a convenience.
*/
fun <T: View> addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T {
addSubview(view)
_arrangedSubviews.add(index, view)
addConstraintsForArrangedView(view, index) /**
* Adds an arranged subview to this view.
* Arranged subviews are laid out according to the stack. If you wish to add a subview that is laid out separately,
* use the normal [addSubview] method.
*
* @param view The view to add.
* @param index The index in this stack to add the view at.
* By default, adds the view to the end of the stack.
* @return The view that was added, as a convenience.
*/
fun <T : View> addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T {
addSubview(view)
_arrangedSubviews.add(index, view)
return view addConstraintsForArrangedView(view, index)
}
/** return view
* Removes the given arranged subview from this stack view's arranged subviews. }
*/
fun removeArrangedSubview(view: View) {
val index = arrangedSubviews.indexOf(view)
if (index < 0) {
throw RuntimeException("Cannot remove view that is not arranged subview")
}
if (index == 0) { /**
solver.removeConstraint(leadingConnection) * Removes the given arranged subview from this stack view's arranged subviews.
val next = arrangedSubviews.getOrNull(1) */
if (next != null) { fun removeArrangedSubview(view: View) {
solver.dsl { val index = arrangedSubviews.indexOf(view)
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next) if (index < 0) {
} throw RuntimeException("Cannot remove view that is not arranged subview")
} else { }
leadingConnection = null
}
}
if (index == arrangedSubviews.size - 1) {
solver.removeConstraint(trailingConnection)
val prev = arrangedSubviews.getOrNull(arrangedSubviews.size - 2)
if (prev != null) {
solver.dsl {
trailingConnection = anchor(TRAILING) equalTo anchor(TRAILING, prev)
}
} else {
trailingConnection = null
}
}
// if the removed view is in the middle if (index == 0) {
if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) { solver.removeConstraint(leadingConnection)
val prev = arrangedSubviews[index - 1] val next = arrangedSubviews.getOrNull(1)
val next = arrangedSubviews[index + 1] if (next != null) {
solver.dsl { solver.dsl {
solver.removeConstraint(arrangedSubviewConnections[index - 1]) leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next)
solver.removeConstraint(arrangedSubviewConnections[index]) }
} else {
leadingConnection = null
}
}
if (index == arrangedSubviews.size - 1) {
solver.removeConstraint(trailingConnection)
val prev = arrangedSubviews.getOrNull(arrangedSubviews.size - 2)
if (prev != null) {
solver.dsl {
trailingConnection = anchor(TRAILING) equalTo anchor(TRAILING, prev)
}
} else {
trailingConnection = null
}
}
// todo: double check me // if the removed view is in the middle
arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next) if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) {
arrangedSubviewConnections.removeAt(index) val prev = arrangedSubviews[index - 1]
} val next = arrangedSubviews[index + 1]
} solver.dsl {
solver.removeConstraint(arrangedSubviewConnections[index - 1])
solver.removeConstraint(arrangedSubviewConnections[index])
_arrangedSubviews.remove(view) // todo: double check me
removeSubview(view) arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next)
} arrangedSubviewConnections.removeAt(index)
}
}
override fun removeSubview(view: View) { _arrangedSubviews.remove(view)
if (arrangedSubviews.contains(view)) { removeSubview(view)
removeArrangedSubview(view) }
} else {
super.removeSubview(view)
}
}
private fun addConstraintsForArrangedView(view: View, index: Int) { override fun removeSubview(view: View) {
if (index == 0) { if (arrangedSubviews.contains(view)) {
if (leadingConnection != null) { removeArrangedSubview(view)
solver.removeConstraint(leadingConnection) } else {
} super.removeSubview(view)
solver.dsl { }
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, view) }
}
}
if (index == arrangedSubviews.size - 1) {
if (trailingConnection != null) {
solver.removeConstraint(trailingConnection)
}
solver.dsl {
trailingConnection = anchor(TRAILING, view) equalTo anchor(TRAILING)
}
}
if (arrangedSubviews.size > 1) {
solver.dsl {
val previous = arrangedSubviews.getOrNull(index - 1)
val next = arrangedSubviews.getOrNull(index + 1)
if (next != null) {
arrangedSubviewConnections.add(index, anchor(TRAILING, view) equalTo (anchor(LEADING, next) + spacing))
}
if (previous != null) {
arrangedSubviewConnections.add(index - 1, anchor(TRAILING, previous) equalTo (anchor(LEADING, view) - spacing))
}
}
}
solver.dsl {
when (distribution) {
Distribution.LEADING ->
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
Distribution.TRAILING ->
perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
Distribution.FILL -> {
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
}
Distribution.CENTER ->
perpAnchor(CENTER) equalTo perpAnchor(CENTER, view)
}
}
}
private fun anchor(position: AxisPosition, view: View = this): LayoutVariable { private fun addConstraintsForArrangedView(view: View, index: Int) {
return view.getAnchor(axis, position) if (index == 0) {
} if (leadingConnection != null) {
private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable { solver.removeConstraint(leadingConnection)
return view.getAnchor(axis.perpendicular, position) }
} solver.dsl {
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, view)
}
}
if (index == arrangedSubviews.size - 1) {
if (trailingConnection != null) {
solver.removeConstraint(trailingConnection)
}
solver.dsl {
trailingConnection = anchor(TRAILING, view) equalTo anchor(TRAILING)
}
}
if (arrangedSubviews.size > 1) {
solver.dsl {
val previous = arrangedSubviews.getOrNull(index - 1)
val next = arrangedSubviews.getOrNull(index + 1)
if (next != null) {
arrangedSubviewConnections.add(
index,
anchor(TRAILING, view) equalTo (anchor(LEADING, next) + spacing)
)
}
if (previous != null) {
arrangedSubviewConnections.add(
index - 1,
anchor(TRAILING, previous) equalTo (anchor(LEADING, view) - spacing)
)
}
}
}
solver.dsl {
when (distribution) {
Distribution.LEADING ->
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
/** Distribution.TRAILING ->
* Defines the modes of how content is distributed in a stack view along the perpendicular axis (i.e. the perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
* non-primary axis).
* Distribution.FILL -> {
* ASCII-art examples are shown below in a stack view with the primary axis [Axis.VERTICAL]. perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
*/ perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
enum class Distribution { }
/**
* The leading edges of arranged subviews are pinned to the leading edge of the stack view. Distribution.CENTER ->
* ``` perpAnchor(CENTER) equalTo perpAnchor(CENTER, view)
* }
* }
* }
*
* private fun anchor(position: AxisPosition, view: View = this): LayoutVariable {
* return view.getAnchor(axis, position)
* }
*
* private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable {
* return view.getAnchor(axis.perpendicular, position)
* }
*
* /**
* * Defines the modes of how content is distributed in a stack view along the perpendicular axis (i.e. the
* * non-primary axis).
* *
* * ASCII-art examples are shown below in a stack view with the primary axis [Axis.VERTICAL].
* ``` */
*/ enum class Distribution {
LEADING, /**
/** * The leading edges of arranged subviews are pinned to the leading edge of the stack view.
* The centers of the arranged subviews are pinned to the center of the stack view. * ```
* ``` *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* * ```
* ``` */
*/ LEADING,
CENTER,
/** /**
* The trailing edges of arranged subviews are pinned to the leading edge of the stack view. * The centers of the arranged subviews are pinned to the center of the stack view.
* ``` * ```
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* ``` * ```
*/ */
TRAILING, CENTER,
/**
* The arranged subviews fill the perpendicular axis of the stack view. /**
* ``` * The trailing edges of arranged subviews are pinned to the leading edge of the stack view.
* * ```
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* *
* ``` *
*/ * ```
FILL */
} TRAILING,
/**
* The arranged subviews fill the perpendicular axis of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
FILL
}
} }

View File

@ -11,12 +11,12 @@ import net.shadowfacts.cacao.util.texture.Texture
* *
* @author shadowfacts * @author shadowfacts
*/ */
class TextureView(var texture: Texture?): View() { class TextureView(var texture: Texture?) : View() {
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
texture?.also { texture?.also {
RenderHelper.draw(matrixStack, bounds, it) RenderHelper.draw(matrixStack, bounds, it)
} }
} }
} }

View File

@ -20,502 +20,521 @@ import kotlin.math.floor
* *
* @author shadowfacts * @author shadowfacts
*/ */
open class View(): Responder { open class View() : Responder {
/** /**
* The window whose view hierarchy this view belongs to. * The window whose view hierarchy this view belongs to.
* Not initialized until the root view in this hierarchy has been added to a hierarchy, * Not initialized until the root view in this hierarchy has been added to a hierarchy,
* using it before that will throw a runtime exception. * using it before that will throw a runtime exception.
*/ */
override var window: Window? = null override var window: Window? = null
/** /**
* The next responder after this one. * The next responder after this one.
* For views, the next responder is the view's superview. * For views, the next responder is the view's superview.
*/ */
override val nextResponder: Responder? override val nextResponder: Responder?
// todo: should the view controller be a Responder? // todo: should the view controller be a Responder?
get() = superview get() = superview
private val solverDelegate = ObservableLateInitProperty<Solver> { private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) { for (v in subviews) {
v.solver = it v.solver = it
} }
} }
/**
* The constraint solver used by the [Window] this view belongs to.
* Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
*/
var solver: Solver by solverDelegate
val hasSolver: Boolean /**
get() = solverDelegate.isInitialized * The constraint solver used by the [Window] this view belongs to.
* Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
*/
var solver: Solver by solverDelegate
/** val hasSolver: Boolean
* Layout anchor for the left edge of this view in the window's coordinate system. get() = solverDelegate.isInitialized
*/
val leftAnchor = LayoutVariable(this, "left")
/**
* Layout anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = LayoutVariable(this, "right")
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
val topAnchor = LayoutVariable(this, "top")
/**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = LayoutVariable(this, "bottom")
/**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = LayoutVariable(this, "width")
/**
* Layout anchor for the height of this view in the window's coordinate system.
*/
val heightAnchor = LayoutVariable(this, "height")
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = LayoutVariable(this, "centerX")
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
val centerYAnchor = LayoutVariable(this, "centerY")
private val _layoutGuides = LinkedList<LayoutGuide>() /**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
val leftAnchor = LayoutVariable(this, "left")
/** /**
* All the layout guides attached to this view. * Layout anchor for the right edge of this view in the window's coordinate system.
* */
* To add a layout guide, call [addLayoutGuide]. val rightAnchor = LayoutVariable(this, "right")
*
* @see LayoutGuide
*/
val layoutGuides: List<LayoutGuide> = _layoutGuides
/** /**
* Whether this view uses constraint-based layout. * Layout anchor for the top edge of this view in the window's coordinate system.
* If `false`, the view's `frame` must be set manually and the layout anchors may not be used. */
* Note: some views (such as [StackView] require arranged subviews to use constraint based layout. val topAnchor = LayoutVariable(this, "top")
*
* Default is `true`.
*/
var usesConstraintBasedLayout = true
/** /**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview). * Layout anchor for the bottom edge of this view in the window's coordinate system.
* If using constraint based layout, this property has zero dimensions until [didLayout] called. */
* Otherwise, this must be set manually. val bottomAnchor = LayoutVariable(this, "bottom")
* Setting this property updates the [bounds].
*/
var frame = Rect(0.0, 0.0, 0.0, 0.0)
set(value) {
field = value
bounds = Rect(Point.ORIGIN, value.size)
}
/** /**
* The rectangle for this view in its own coordinate system. * Layout anchor for the width of this view in the window's coordinate system.
* If using constraint based layout, this property has zero dimensions until [didLayout] called. */
* Otherwise, this will be initialized when [frame] is set. val widthAnchor = LayoutVariable(this, "width")
*/
var bounds = Rect(0.0, 0.0, 0.0, 0.0)
/** /**
* The position on the Z-axis of this view. * Layout anchor for the height of this view in the window's coordinate system.
* Views are rendered from lowest Z index to highest. Clicks are handled from highest to lowest. */
*/ val heightAnchor = LayoutVariable(this, "height")
var zIndex: Double = 0.0
/** /**
* The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no * Layout anchor for the center X position of this view in the window's coordinate system.
* intrinsic size. */
* val centerXAnchor = LayoutVariable(this, "centerX")
* Setting this creates/updates [no.birkett.kiwi.Strength.MEDIUM] constraints on this view's width/height using
* the size.
*/
var intrinsicContentSize: Size? = null
set(value) {
updateIntrinsicContentSizeConstraints(intrinsicContentSize, value)
field = value
}
private var intrinsicContentSizeWidthConstraint: Constraint? = null
private var intrinsicContentSizeHeightConstraint: Constraint? = null
/** /**
* The background color of this view. * Layout anchor for the center Y position of this view in the window's coordinate system.
*/ */
var backgroundColor = Color.CLEAR val centerYAnchor = LayoutVariable(this, "centerY")
var respondsToDragging = false private val _layoutGuides = LinkedList<LayoutGuide>()
/** /**
* This view's parent view. If `null`, this view is a top-level view in the [Window]. * All the layout guides attached to this view.
*/ *
var superview: View? = null * To add a layout guide, call [addLayoutGuide].
// _subviews is the internal, mutable object since we only want it to be mutated by the add/removeSubview methods *
private val _subviews = LinkedList<View>() * @see LayoutGuide
private var subviewsSortedByZIndex: List<View> = listOf() */
val layoutGuides: List<LayoutGuide> = _layoutGuides
/** /**
* The list of all the subviews of this view. * Whether this view uses constraint-based layout.
* This list should never by mutated directly, only by the [addSubview]/[removeSubview] methods. * If `false`, the view's `frame` must be set manually and the layout anchors may not be used.
*/ * Note: some views (such as [StackView] require arranged subviews to use constraint based layout.
val subviews: List<View> = _subviews *
* Default is `true`.
*/
var usesConstraintBasedLayout = true
constructor(frame: Rect): this() { /**
this.usesConstraintBasedLayout = false * The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
this.frame = frame * If using constraint based layout, this property has zero dimensions until [didLayout] called.
} * Otherwise, this must be set manually.
* Setting this property updates the [bounds].
*/
var frame = Rect(0.0, 0.0, 0.0, 0.0)
set(value) {
field = value
bounds = Rect(Point.ORIGIN, value.size)
}
/** /**
* Helper method for retrieve the anchor for a specific position on the given axis. * The rectangle for this view in its own coordinate system.
*/ * If using constraint based layout, this property has zero dimensions until [didLayout] called.
fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable { * Otherwise, this will be initialized when [frame] is set.
return when (axis) { */
Axis.HORIZONTAL -> var bounds = Rect(0.0, 0.0, 0.0, 0.0)
when (position) {
AxisPosition.LEADING -> leftAnchor
AxisPosition.CENTER -> centerXAnchor
AxisPosition.TRAILING -> rightAnchor
}
Axis.VERTICAL ->
when (position) {
AxisPosition.LEADING -> topAnchor
AxisPosition.CENTER -> centerYAnchor
AxisPosition.TRAILING -> bottomAnchor
}
}
}
/** /**
* Adds the given subview as a child of this view. * The position on the Z-axis of this view.
* * Views are rendered from lowest Z index to highest. Clicks are handled from highest to lowest.
* @param view The view to add. */
* @return The view that was added, as a convenience. var zIndex: Double = 0.0
*/
fun <T: View> addSubview(view: T): T {
_subviews.add(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = this /**
if (hasSolver) { * The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no
view.solver = solver * intrinsic size.
} *
view.window = window * Setting this creates/updates [no.birkett.kiwi.Strength.MEDIUM] constraints on this view's width/height using
* the size.
*/
var intrinsicContentSize: Size? = null
set(value) {
updateIntrinsicContentSizeConstraints(intrinsicContentSize, value)
field = value
}
private var intrinsicContentSizeWidthConstraint: Constraint? = null
private var intrinsicContentSizeHeightConstraint: Constraint? = null
view.wasAdded() /**
* The background color of this view.
*/
var backgroundColor = Color.CLEAR
return view var respondsToDragging = false
}
/** /**
* Removes the given view from this view's children and removes all constraints that connect the subview or any of * This view's parent view. If `null`, this view is a top-level view in the [Window].
* its children (recursively) to a view outside of the subview's hierarchy. Constraints internal to the subview's */
* hierarchy (e.g., one between the subview and its child) will be left in place. var superview: View? = null
*
* This method may be overridden by layout-providing views (such as [StackView]) to update its layout when a managed
* subview is removed.
*
* @param view The view to removed as a child of this view.
* @throws RuntimeException If the given [view] is not a subview of this view.
*/
open fun removeSubview(view: View) {
if (view.superview !== this) {
throw RuntimeException("Cannot remove subview whose superview is not this view")
}
_subviews.remove(view) // _subviews is the internal, mutable object since we only want it to be mutated by the add/removeSubview methods
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex) private val _subviews = LinkedList<View>()
private var subviewsSortedByZIndex: List<View> = listOf()
view.superview = null /**
* The list of all the subviews of this view.
* This list should never by mutated directly, only by the [addSubview]/[removeSubview] methods.
*/
val subviews: List<View> = _subviews
// we need to remove constraints for this subview that cross the boundary between the subview and ourself constructor(frame: Rect) : this() {
val constraintsToRemove = solver.constraints.filter { constraint -> this.usesConstraintBasedLayout = false
val variables = constraint.getVariables().mapNotNull { it as? LayoutVariable } this.frame = frame
}
for (a in 0 until variables.size - 1) { /**
for (b in a + 1 until variables.size) { * Helper method for retrieve the anchor for a specific position on the given axis.
// if the variable views have no common ancestor after the removed view's superview is unset, */
// the constraint crossed the this<->view boundary and should be removed fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable {
val ancestor = LowestCommonAncestor.find(variables[a].viewOrLayoutGuideView, variables[b].viewOrLayoutGuideView, View::superview) return when (axis) {
if (ancestor == null) { Axis.HORIZONTAL ->
return@filter true when (position) {
} AxisPosition.LEADING -> leftAnchor
} AxisPosition.CENTER -> centerXAnchor
} AxisPosition.TRAILING -> rightAnchor
false }
}
constraintsToRemove.forEach(solver::removeConstraint)
// todo: does this need to be reset Axis.VERTICAL ->
when (position) {
AxisPosition.LEADING -> topAnchor
AxisPosition.CENTER -> centerYAnchor
AxisPosition.TRAILING -> bottomAnchor
}
}
}
/**
* Adds the given subview as a child of this view.
*
* @param view The view to add.
* @return The view that was added, as a convenience.
*/
fun <T : View> addSubview(view: T): T {
_subviews.add(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = this
if (hasSolver) {
view.solver = solver
}
view.window = window
view.wasAdded()
return view
}
/**
* Removes the given view from this view's children and removes all constraints that connect the subview or any of
* its children (recursively) to a view outside of the subview's hierarchy. Constraints internal to the subview's
* hierarchy (e.g., one between the subview and its child) will be left in place.
*
* This method may be overridden by layout-providing views (such as [StackView]) to update its layout when a managed
* subview is removed.
*
* @param view The view to removed as a child of this view.
* @throws RuntimeException If the given [view] is not a subview of this view.
*/
open fun removeSubview(view: View) {
if (view.superview !== this) {
throw RuntimeException("Cannot remove subview whose superview is not this view")
}
_subviews.remove(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = null
// we need to remove constraints for this subview that cross the boundary between the subview and ourself
val constraintsToRemove = solver.constraints.filter { constraint ->
val variables = constraint.getVariables().mapNotNull { it as? LayoutVariable }
for (a in 0 until variables.size - 1) {
for (b in a + 1 until variables.size) {
// if the variable views have no common ancestor after the removed view's superview is unset,
// the constraint crossed the this<->view boundary and should be removed
val ancestor = LowestCommonAncestor.find(
variables[a].viewOrLayoutGuideView,
variables[b].viewOrLayoutGuideView,
View::superview
)
if (ancestor == null) {
return@filter true
}
}
}
false
}
constraintsToRemove.forEach(solver::removeConstraint)
// todo: does this need to be reset
// view.solver = null // view.solver = null
view.window = null view.window = null
// todo: is this necessary? // todo: is this necessary?
// view.wasRemoved() // view.wasRemoved()
} }
/** /**
* Creates and returns a new layout guide with this view as its owner. * Creates and returns a new layout guide with this view as its owner.
*/ */
fun addLayoutGuide(): LayoutGuide { fun addLayoutGuide(): LayoutGuide {
val guide = LayoutGuide(this) val guide = LayoutGuide(this)
_layoutGuides.add(guide) _layoutGuides.add(guide)
if (hasSolver) { if (hasSolver) {
guide.attachTo(solver) guide.attachTo(solver)
} }
return guide return guide
} }
/** /**
* Removes this view from its superview, if it has one. * Removes this view from its superview, if it has one.
*/ */
fun removeFromSuperview() { fun removeFromSuperview() {
superview?.removeSubview(this) superview?.removeSubview(this)
} }
/** /**
* Finds all subviews that contain the given point. * Finds all subviews that contain the given point.
* *
* @param point The point to find subviews for, in the coordinate system of this view. * @param point The point to find subviews for, in the coordinate system of this view.
* @return All views that contain the given point. * @return All views that contain the given point.
*/ */
fun subviewsAtPoint(point: Point): List<View> { fun subviewsAtPoint(point: Point): List<View> {
return subviews.filter { point in it.frame } return subviews.filter { point in it.frame }
} }
/** /**
* Attempts to find a subview which contains the given point. * Attempts to find a subview which contains the given point.
* If multiple subviews contain the given point, which one this method returns is undefined. * If multiple subviews contain the given point, which one this method returns is undefined.
* [subviewsAtPoint] may be used, and the resulting List sorted by [View.zIndex]. * [subviewsAtPoint] may be used, and the resulting List sorted by [View.zIndex].
* *
* @param point The point to find a subview for, in the coordinate system of this view. * @param point The point to find a subview for, in the coordinate system of this view.
* @return The view, if any, that contains the given point. * @return The view, if any, that contains the given point.
*/ */
fun subviewAtPoint(point: Point): View? { fun subviewAtPoint(point: Point): View? {
return subviews.firstOrNull { point in it.frame } return subviews.firstOrNull { point in it.frame }
} }
/** /**
* Called when this view was added to a view hierarchy. * Called when this view was added to a view hierarchy.
* If overridden, the super-class method must be called. * If overridden, the super-class method must be called.
*/ */
open fun wasAdded() { open fun wasAdded() {
createInternalConstraints() createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize) updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
layoutGuides.forEach { layoutGuides.forEach {
it.attachTo(solver) it.attachTo(solver)
} }
} }
/** /**
* Called during [wasAdded] to add any constraints to the [solver] that are internal to this view. * Called during [wasAdded] to add any constraints to the [solver] that are internal to this view.
* If overridden, the super-class method must be called. * If overridden, the super-class method must be called.
*/ */
protected open fun createInternalConstraints() { protected open fun createInternalConstraints() {
if (!usesConstraintBasedLayout) return if (!usesConstraintBasedLayout) return
solver.dsl { solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor) rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor) bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2) centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2) centerYAnchor equalTo (topAnchor + heightAnchor / 2)
} }
} }
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) { private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!usesConstraintBasedLayout || !hasSolver) return if (!usesConstraintBasedLayout || !hasSolver) return
if (old != null) { if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!) solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
solver.removeConstraint(intrinsicContentSizeHeightConstraint!!) solver.removeConstraint(intrinsicContentSizeHeightConstraint!!)
} }
if (new != null) { if (new != null) {
solver.dsl { solver.dsl {
this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = MEDIUM)) this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = MEDIUM))
this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = MEDIUM)) this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = MEDIUM))
} }
} }
} }
/** /**
* Called after this view has been laid-out. * Called after this view has been laid-out.
* If overridden, the super-class method must be called. * If overridden, the super-class method must be called.
*/ */
open fun didLayout() { open fun didLayout() {
if (usesConstraintBasedLayout) { if (usesConstraintBasedLayout) {
val superviewLeft = superview?.leftAnchor?.value ?: 0.0 val superviewLeft = superview?.leftAnchor?.value ?: 0.0
val superviewTop = superview?.topAnchor?.value ?: 0.0 val superviewTop = superview?.topAnchor?.value ?: 0.0
frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value) frame = Rect(
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value) leftAnchor.value - superviewLeft,
} topAnchor.value - superviewTop,
widthAnchor.value,
heightAnchor.value
)
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value)
}
subviews.forEach(View::didLayout) subviews.forEach(View::didLayout)
} }
/** /**
* Called to draw this view. * Called to draw this view.
* This method should not be called directly, it is called by the parent view/window. * This method should not be called directly, it is called by the parent view/window.
* This method generally should not be overridden, but it is left open for internal framework use. * This method generally should not be overridden, but it is left open for internal framework use.
* Use [drawContent] to draw any custom content. * Use [drawContent] to draw any custom content.
* *
* @param mouse The position of the mouse in the coordinate system of this view. * @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame. * @param delta The time since the last frame.
*/ */
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) { open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push() matrixStack.push()
matrixStack.translate(frame.left, frame.top, 0.0) matrixStack.translate(frame.left, frame.top, 0.0)
RenderHelper.fill(matrixStack, bounds, backgroundColor) RenderHelper.fill(matrixStack, bounds, backgroundColor)
drawContent(matrixStack, mouse, delta) drawContent(matrixStack, mouse, delta)
subviewsSortedByZIndex.forEach { subviewsSortedByZIndex.forEach {
val mouseInView = convert(mouse, to = it) val mouseInView = convert(mouse, to = it)
it.draw(matrixStack, mouseInView, delta) it.draw(matrixStack, mouseInView, delta)
} }
matrixStack.pop() matrixStack.pop()
} }
/** /**
* Called during [draw] to draw content that's part of this view. * Called during [draw] to draw content that's part of this view.
* During this method, the OpenGL coordinate system has been translated so the origin is at the top left corner * During this method, the OpenGL coordinate system has been translated so the origin is at the top left corner
* of this view. Be careful not to translate additionally, and not to draw outside the [bounds] of the view. * of this view. Be careful not to translate additionally, and not to draw outside the [bounds] of the view.
* *
* @param mouse The position of the mouse in the coordinate system of this view. * @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame. * @param delta The time since the last frame.
*/ */
open fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {} open fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {}
/** /**
* Called when this view is clicked. * Called when this view is clicked.
* *
* The base implementation of this method forwards the click event to the first subview (sorted by [zIndex]) that * The base implementation of this method forwards the click event to the first subview (sorted by [zIndex]) that
* contains the clicked point. Additionally, any subviews of this view that do not contain the clicked point receive * contains the clicked point. Additionally, any subviews of this view that do not contain the clicked point receive
* the [mouseClickedOutside] event. If multiple views contain the point, any after one that returns `true` from this * the [mouseClickedOutside] event. If multiple views contain the point, any after one that returns `true` from this
* method will not receive the event or the click-outside event. * method will not receive the event or the click-outside event.
* *
* If overridden, the super-class method does not have to be called. Intentionally not calling it may be used * If overridden, the super-class method does not have to be called. Intentionally not calling it may be used
* to prevent [subviews] from receiving click events. * to prevent [subviews] from receiving click events.
* *
* @param point The point in the coordinate system of this view that the mouse was clicked. * @param point The point in the coordinate system of this view that the mouse was clicked.
* @param mouseButton The mouse button used to click. * @param mouseButton The mouse button used to click.
* @return Whether the mouse click was handled by this view or any subviews. * @return Whether the mouse click was handled by this view or any subviews.
*/ */
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val (inside, outside) = subviews.partition { point in it.frame } val (inside, outside) = subviews.partition { point in it.frame }
val view = inside.maxByOrNull(View::zIndex) val view = inside.maxByOrNull(View::zIndex)
var result = false var result = false
if (view != null) { if (view != null) {
val pointInView = convert(point, to = view) val pointInView = convert(point, to = view)
result = view.mouseClicked(pointInView, mouseButton) result = view.mouseClicked(pointInView, mouseButton)
} }
for (v in outside) { for (v in outside) {
val pointInV = convert(point, to = v) val pointInV = convert(point, to = v)
v.mouseClickedOutside(pointInV, mouseButton) v.mouseClickedOutside(pointInV, mouseButton)
} }
return result return result
} }
/** /**
* Called when the mouse was clicked outside this view. * Called when the mouse was clicked outside this view.
* *
* The base implementation of this method simply forwards the event to all of this view's subviews. * The base implementation of this method simply forwards the event to all of this view's subviews.
* *
* @param point The clicked point _in the coordinate space of this view_. * @param point The clicked point _in the coordinate space of this view_.
* @param mouseButton The mouse button used to click. * @param mouseButton The mouse button used to click.
*/ */
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) { open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
for (view in subviews) { for (view in subviews) {
val pointInView = convert(point, to = view) val pointInView = convert(point, to = view)
view.mouseClickedOutside(pointInView, mouseButton) view.mouseClickedOutside(pointInView, mouseButton)
} }
} }
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean { open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(startPoint).maxByOrNull(View::zIndex) val view = subviewsAtPoint(startPoint).maxByOrNull(View::zIndex)
if (view != null) { if (view != null) {
val startInView = convert(startPoint, to = view) val startInView = convert(startPoint, to = view)
return view.mouseDragged(startInView, delta, mouseButton) return view.mouseDragged(startInView, delta, mouseButton)
} }
return false return false
} }
open fun mouseDragEnded(point: Point, mouseButton: MouseButton) { open fun mouseDragEnded(point: Point, mouseButton: MouseButton) {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex) val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
if (view != null) { if (view != null) {
val pointInView = convert(point, to = view) val pointInView = convert(point, to = view)
return view.mouseDragEnded(pointInView, mouseButton) return view.mouseDragEnded(pointInView, mouseButton)
} }
} }
open fun mouseScrolled(point: Point, amount: Double): Boolean { open fun mouseScrolled(point: Point, amount: Double): Boolean {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex) val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
if (view != null) { if (view != null) {
val pointInView = convert(point, to = view) val pointInView = convert(point, to = view)
return view.mouseScrolled(pointInView, amount) return view.mouseScrolled(pointInView, amount)
} }
return false return false
} }
/** /**
* Converts the given point in this view's coordinate system to the coordinate system of another view or the window. * Converts the given point in this view's coordinate system to the coordinate system of another view or the window.
* *
* @param point The point to convert, in the coordinate system of this view. * @param point The point to convert, in the coordinate system of this view.
* @param to The view to convert to. If `null`, it will be converted to the window's coordinate system. * @param to The view to convert to. If `null`, it will be converted to the window's coordinate system.
* @return The point in the coordinate system of the [to] view. * @return The point in the coordinate system of the [to] view.
*/ */
fun convert(point: Point, to: View?): Point { fun convert(point: Point, to: View?): Point {
if (to != null) { if (to != null) {
val ancestor = LowestCommonAncestor.find(this, to, View::superview) val ancestor = LowestCommonAncestor.find(this, to, View::superview)
@Suppress("NAME_SHADOWING") var point = point @Suppress("NAME_SHADOWING") var point = point
// Convert up to the LCA // Convert up to the LCA
var view: View? = this var view: View? = this
while (view != null && view != ancestor) { while (view != null && view != ancestor) {
point = Point(point.x + view.frame.left, point.y + view.frame.top) point = Point(point.x + view.frame.left, point.y + view.frame.top)
view = view.superview view = view.superview
} }
// Convert back down to the other view // Convert back down to the other view
view = to view = to
while (view != null && view != ancestor) { while (view != null && view != ancestor) {
point = Point(point.x - view.frame.left, point.y - view.frame.top) point = Point(point.x - view.frame.left, point.y - view.frame.top)
view = view.superview view = view.superview
} }
return point return point
} else { } else {
return Point(leftAnchor.value + point.x, topAnchor.value + point.y) return Point(leftAnchor.value + point.x, topAnchor.value + point.y)
} }
} }
/** /**
* Converts the given rectangle in this view's coordinate system to the coordinate system of another view or the window. * Converts the given rectangle in this view's coordinate system to the coordinate system of another view or the window.
* *
* @param rect The rectangle to convert, in the coordinate system of this view. * @param rect The rectangle to convert, in the coordinate system of this view.
* @param to The view to convert to. If `null`, it will be converted to the window's coordinate system. * @param to The view to convert to. If `null`, it will be converted to the window's coordinate system.
* @return The rectangle in the coordinate system of the [to] view. * @return The rectangle in the coordinate system of the [to] view.
*/ */
fun convert(rect: Rect, to: View?): Rect { fun convert(rect: Rect, to: View?): Rect {
return Rect(convert(rect.origin, to), rect.size) return Rect(convert(rect.origin, to), rect.size)
} }
} }
private fun LayoutGuide.attachTo(solver: Solver) { private fun LayoutGuide.attachTo(solver: Solver) {
solver.dsl { solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor) rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor) bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2) centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2) centerYAnchor equalTo (topAnchor + heightAnchor / 2)
} }
} }
private val LayoutVariable.viewOrLayoutGuideView: View private val LayoutVariable.viewOrLayoutGuideView: View
get() = view ?: layoutGuide!!.owningView get() = view ?: layoutGuide!!.owningView

View File

@ -22,134 +22,136 @@ import kotlin.math.floor
* Will be added as a subview of the button and laid out using constraints. * Will be added as a subview of the button and laid out using constraints.
* @param padding The padding between the [content] and the edges of the button. * @param padding The padding between the [content] and the edges of the button.
*/ */
abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val padding: Double = 4.0): View() { abstract class AbstractButton<Impl : AbstractButton<Impl>>(val content: View, val padding: Double = 4.0) : View() {
/** /**
* The function that handles when this button is clicked. * The function that handles when this button is clicked.
* The parameter is the type of the concrete button implementation that was used. * The parameter is the type of the concrete button implementation that was used.
*/ */
var handler: ((Impl) -> Unit)? = null var handler: ((Impl) -> Unit)? = null
/** /**
* Whether the button is disabled. * Whether the button is disabled.
* Disabled buttons have a different background ([disabledBackground]) and do not receive click events. * Disabled buttons have a different background ([disabledBackground]) and do not receive click events.
*/ */
var disabled = false var disabled = false
/** /**
* The normal background view to draw behind the button content. It will be added as a subview during [wasAdded], * The normal background view to draw behind the button content. It will be added as a subview during [wasAdded],
* so all background view properties must be specified prior to the button being added to a view hierarchy. * so all background view properties must be specified prior to the button being added to a view hierarchy.
* *
* The background will fill the entire button (going beneath the content [padding]). * The background will fill the entire button (going beneath the content [padding]).
* There are also [hoveredBackground] and [disabledBackground] for those states. * There are also [hoveredBackground] and [disabledBackground] for those states.
* If a [backgroundColor] is specified, it will be drawn behind the background View and thus not visible * If a [backgroundColor] is specified, it will be drawn behind the background View and thus not visible
* 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) { set(value) {
field?.removeFromSuperview() field?.removeFromSuperview()
field = value field = value
value?.also(::addBackground) value?.also(::addBackground)
} }
/**
* The background to draw when the button is hovered over by the mouse.
* If `null`, the normal [background] will be used.
* @see background
*/
var hoveredBackground: View? = NinePatchView(NinePatchTexture.BUTTON_HOVERED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used.
* @see background
*/
var disabledBackground: View? = NinePatchView(NinePatchTexture.BUTTON_DISABLED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/** /**
* The tooltip text shown when this button is hovered. * The background to draw when the button is hovered over by the mouse.
*/ * If `null`, the normal [background] will be used.
var tooltip: Text? = null * @see background
*/
var hoveredBackground: View? = NinePatchView(NinePatchTexture.BUTTON_HOVERED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
override fun wasAdded() { /**
solver.dsl { * The background to draw when the button is [disabled].
addSubview(content) * If `null`, the normal [background] will be used.
content.centerXAnchor equalTo centerXAnchor * @see background
content.centerYAnchor equalTo centerYAnchor */
var disabledBackground: View? = NinePatchView(NinePatchTexture.BUTTON_DISABLED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
content.leftAnchor.lessThanOrEqualTo((leftAnchor + padding), WEAK) /**
content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK) * The tooltip text shown when this button is hovered.
content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK) */
content.bottomAnchor.greaterThanOrEqualTo(bottomAnchor - padding, WEAK) var tooltip: Text? = null
}
listOfNotNull(background, hoveredBackground, disabledBackground).forEach(::addBackground) override fun wasAdded() {
solver.dsl {
addSubview(content)
content.centerXAnchor equalTo centerXAnchor
content.centerYAnchor equalTo centerYAnchor
super.wasAdded() content.leftAnchor.lessThanOrEqualTo((leftAnchor + padding), WEAK)
} content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK)
content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK)
content.bottomAnchor.greaterThanOrEqualTo(bottomAnchor - padding, WEAK)
}
private fun addBackground(view: View) { listOfNotNull(background, hoveredBackground, disabledBackground).forEach(::addBackground)
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) { super.wasAdded()
matrixStack.push() }
matrixStack.translate(frame.left, frame.top, 0.0)
RenderHelper.fill(matrixStack, bounds, backgroundColor) 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
}
}
}
// don't need to convert mouse to background coordinate system override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
// the edges are all pinned, so the coordinate space is the same matrixStack.push()
getCurrentBackground(mouse)?.draw(matrixStack, mouse, delta) matrixStack.translate(frame.left, frame.top, 0.0)
val mouseInContent = convert(mouse, to = content) RenderHelper.fill(matrixStack, bounds, backgroundColor)
content.draw(matrixStack, mouseInContent, delta)
// don't draw subviews, otherwise all background views + content will get drawn // don't need to convert mouse to background coordinate system
// the edges are all pinned, so the coordinate space is the same
getCurrentBackground(mouse)?.draw(matrixStack, mouse, delta)
matrixStack.pop() val mouseInContent = convert(mouse, to = content)
content.draw(matrixStack, mouseInContent, delta)
if (tooltip != null && mouse in bounds) { // don't draw subviews, otherwise all background views + content will get drawn
window!!.drawTooltip(listOf(tooltip!!))
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { matrixStack.pop()
if (disabled) return false
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation if (tooltip != null && mouse in bounds) {
// of AbstractButton. window!!.drawTooltip(listOf(tooltip!!))
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>` }
@Suppress("UNCHECKED_CAST") }
handler?.invoke(this as Impl)
return true override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
} if (disabled) return false
protected open fun getCurrentBackground(mouse: Point): View? { // We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
return if (disabled) { // of AbstractButton.
disabledBackground ?: background // For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
} else if (mouse in bounds) { @Suppress("UNCHECKED_CAST")
hoveredBackground ?: background handler?.invoke(this as Impl)
} else {
background return true
} }
}
protected open fun getCurrentBackground(mouse: Point): View? {
return if (disabled) {
disabledBackground ?: background
} else if (mouse in bounds) {
hoveredBackground ?: background
} else {
background
}
}
} }

View File

@ -11,11 +11,11 @@ import net.shadowfacts.cacao.view.View
* @param handler The handler function to invoke when this button is pressed. * @param handler The handler function to invoke when this button is pressed.
*/ */
class Button( class Button(
content: View, content: View,
padding: Double = 4.0, padding: Double = 4.0,
handler: ((Button) -> Unit)? = null handler: ((Button) -> Unit)? = null
): AbstractButton<Button>(content, padding) { ) : AbstractButton<Button>(content, padding) {
init { init {
this.handler = handler this.handler = handler
} }
} }

View File

@ -33,62 +33,62 @@ import net.shadowfacts.kiwidsl.dsl
* Positioning of content views is handled by the dropdown. * Positioning of content views is handled by the dropdown.
* @param updateView A function for updating the view used as the button's 'label' that's visible even when the dropdown isn't. * @param updateView A function for updating the view used as the button's 'label' that's visible even when the dropdown isn't.
*/ */
class DropdownButton<Value, ContentView: View>( class DropdownButton<Value, ContentView : View>(
val initialValue: Value, val initialValue: Value,
val allValues: Iterable<Value>, val allValues: Iterable<Value>,
val createView: (Value) -> ContentView, val createView: (Value) -> ContentView,
val updateView: (newValue: Value, view: ContentView) -> Unit, val updateView: (newValue: Value, view: ContentView) -> Unit,
padding: Double = 4.0 padding: Double = 4.0
): AbstractButton<DropdownButton<Value, ContentView>>( ) : AbstractButton<DropdownButton<Value, ContentView>>(
StackView(Axis.HORIZONTAL), StackView(Axis.HORIZONTAL),
padding padding
) { ) {
companion object { companion object {
val DROPDOWN_INDICATOR = Texture(Identifier("asmr", "textures/gui/dropdown.png"), 0, 0) val DROPDOWN_INDICATOR = Texture(Identifier("asmr", "textures/gui/dropdown.png"), 0, 0)
} }
private val stackView: StackView private val stackView: StackView
get() = content as StackView get() = content as StackView
private val contentView: ContentView private val contentView: ContentView
get() = stackView.arrangedSubviews.first() as ContentView get() = stackView.arrangedSubviews.first() as ContentView
private lateinit var dropdownIndicator: TextureView private lateinit var dropdownIndicator: TextureView
/** /**
* The currently selected [Value] of the dropdown. * The currently selected [Value] of the dropdown.
*/ */
var value: Value = initialValue var value: Value = initialValue
set(value) { set(value) {
field = value field = value
updateView(value, contentView) updateView(value, contentView)
// todo: setNeedsLayout instead of force unwrapping window // todo: setNeedsLayout instead of force unwrapping window
window!!.layout() window!!.layout()
} }
override fun wasAdded() { override fun wasAdded() {
super.wasAdded() super.wasAdded()
stackView.addArrangedSubview(createView(initialValue)) stackView.addArrangedSubview(createView(initialValue))
dropdownIndicator = stackView.addArrangedSubview(TextureView(DROPDOWN_INDICATOR)) dropdownIndicator = stackView.addArrangedSubview(TextureView(DROPDOWN_INDICATOR))
solver.dsl { solver.dsl {
dropdownIndicator.widthAnchor equalTo 9 dropdownIndicator.widthAnchor equalTo 9
dropdownIndicator.heightAnchor equalTo 9 dropdownIndicator.heightAnchor equalTo 9
} }
} }
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
return if (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT) { return if (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT) {
showDropdown() showDropdown()
true true
} else { } else {
super.mouseClicked(point, mouseButton) super.mouseClicked(point, mouseButton)
} }
} }
private fun showDropdown() { private fun showDropdown() {
// val dropdownWindow = window.screen.addWindow(Window()) // val dropdownWindow = window.screen.addWindow(Window())
// val dropdownBackground = dropdownWindow.addView(NinePatchView(NinePatchTexture.BUTTON_BG).apply { // val dropdownBackground = dropdownWindow.addView(NinePatchView(NinePatchTexture.BUTTON_BG).apply {
// zIndex = -1.0 // zIndex = -1.0
@ -131,76 +131,96 @@ class DropdownButton<Value, ContentView: View>(
// dropdownBackground.bottomAnchor equalTo stack.bottomAnchor // dropdownBackground.bottomAnchor equalTo stack.bottomAnchor
// } // }
// dropdownWindow.layout() // dropdownWindow.layout()
} }
private fun valueSelected(value: Value) { private fun valueSelected(value: Value) {
this.value = value this.value = value
handler?.invoke(this) handler?.invoke(this)
} }
} }
private class DropdownItemBackgroundView( private class DropdownItemBackgroundView(
private val first: Boolean, private val first: Boolean,
private val last: Boolean, private val last: Boolean,
ninePatch: NinePatchTexture ninePatch: NinePatchTexture
): NinePatchView(ninePatch) { ) : NinePatchView(ninePatch) {
// Corners // Corners
private val topLeftDelegate = ResettableLazyProperty { private val topLeftDelegate = ResettableLazyProperty {
super.topLeft super.topLeft
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), if (first) ninePatch.cornerHeight.toDouble() else 0.0) Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), if (first) ninePatch.cornerHeight.toDouble() else 0.0)
} }
override val topLeft by topLeftDelegate override val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty { private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, topLeft.width, topLeft.height) Rect(bounds.width - ninePatch.cornerWidth, 0.0, topLeft.width, topLeft.height)
} }
override val topRight by topRightDelegate override val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty { private val bottomLeftDelegate = ResettableLazyProperty {
Rect(topLeft.left, bounds.height - ninePatch.cornerHeight, topLeft.width, if (last) ninePatch.cornerHeight.toDouble() else 0.0) Rect(
} topLeft.left,
override val bottomLeft by bottomLeftDelegate bounds.height - ninePatch.cornerHeight,
topLeft.width,
if (last) ninePatch.cornerHeight.toDouble() else 0.0
)
}
override val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty { private val bottomRightDelegate = ResettableLazyProperty {
Rect(topRight.left, bottomLeft.top, topLeft.width, bottomLeft.height) Rect(topRight.left, bottomLeft.top, topLeft.width, bottomLeft.height)
} }
override val bottomRight by bottomRightDelegate override val bottomRight by bottomRightDelegate
// Edges // Edges
private val topMiddleDelegate = ResettableLazyProperty { private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, topLeft.height) Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, topLeft.height)
} }
override val topMiddle by topMiddleDelegate override val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty { private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, bottomLeft.height) Rect(topMiddle.left, bottomLeft.top, topMiddle.width, bottomLeft.height)
} }
override val bottomMiddle by bottomMiddleDelegate override val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty { private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, topLeft.bottom, topLeft.width, bounds.height - (if (first && last) 2 else if (first || last) 1 else 0) * ninePatch.cornerHeight) Rect(
} topLeft.left,
override val leftMiddle by leftMiddleDelegate topLeft.bottom,
topLeft.width,
bounds.height - (if (first && last) 2 else if (first || last) 1 else 0) * ninePatch.cornerHeight
)
}
override val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty { private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, topRight.bottom, topRight.width, leftMiddle.height) Rect(topRight.left, topRight.bottom, topRight.width, leftMiddle.height)
} }
override val rightMiddle by rightMiddleDelegate override val rightMiddle by rightMiddleDelegate
// Center // Center
private val centerDelegate = ResettableLazyProperty { private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topMiddle.bottom, topMiddle.width, leftMiddle.height) Rect(topLeft.right, topMiddle.bottom, topMiddle.width, leftMiddle.height)
} }
override val center by centerDelegate override val center by centerDelegate
private val delegates = listOf(topLeftDelegate, topRightDelegate, bottomLeftDelegate, bottomRightDelegate, topMiddleDelegate, bottomMiddleDelegate, leftMiddleDelegate, rightMiddleDelegate, centerDelegate) private val delegates = listOf(
topLeftDelegate,
topRightDelegate,
bottomLeftDelegate,
bottomRightDelegate,
topMiddleDelegate,
bottomMiddleDelegate,
leftMiddleDelegate,
rightMiddleDelegate,
centerDelegate
)
override fun didLayout() { override fun didLayout() {
super.didLayout() super.didLayout()
delegates.forEach(ResettableLazyProperty<Rect>::reset) delegates.forEach(ResettableLazyProperty<Rect>::reset)
} }
} }

View File

@ -16,38 +16,38 @@ import net.shadowfacts.cacao.view.Label
* @param initialValue The initial enum value for this button. * @param initialValue The initial enum value for this button.
* @param localizer A function that takes an enum value and converts into a [Text] for the button's label. * @param localizer A function that takes an enum value and converts into a [Text] for the button's label.
*/ */
class EnumButton<E: Enum<E>>( class EnumButton<E : Enum<E>>(
initialValue: E, initialValue: E,
val localizer: (E) -> Text val localizer: (E) -> Text
): AbstractButton<EnumButton<E>>( ) : AbstractButton<EnumButton<E>>(
Label(localizer(initialValue), shadow = true) Label(localizer(initialValue), shadow = true)
) { ) {
private val label: Label private val label: Label
get() = content as Label get() = content as Label
/** /**
* The current value of the enum button. * The current value of the enum button.
* Updating this property will use the [localizer] to update the label. * Updating this property will use the [localizer] to update the label.
*/ */
var value: E = initialValue var value: E = initialValue
set(value) { set(value) {
field = value field = value
label.text = localizer(value) label.text = localizer(value)
} }
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) { if (!disabled) {
value = when (mouseButton) { value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value) MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value) MouseButton.RIGHT -> EnumHelper.previous(value)
else -> { else -> {
return false return false
} }
} }
} }
return super.mouseClicked(point, mouseButton) return super.mouseClicked(point, mouseButton)
} }
} }

View File

@ -17,54 +17,54 @@ import net.shadowfacts.cacao.view.View
* @param handler The handler function to invoke when this button is pressed. * @param handler The handler function to invoke when this button is pressed.
*/ */
class ToggleButton( class ToggleButton(
initialState: Boolean, initialState: Boolean,
handler: ((ToggleButton) -> Unit)? = null, handler: ((ToggleButton) -> Unit)? = null,
): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF), padding = 0.0) { ) : AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF), padding = 0.0) {
companion object { companion object {
val OFF = Texture(Identifier("textures/gui/checkbox.png"), 0, 0, 64, 64) val OFF = Texture(Identifier("textures/gui/checkbox.png"), 0, 0, 64, 64)
val OFF_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 0, 64, 64) val OFF_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 0, 64, 64)
val ON = Texture(Identifier("textures/gui/checkbox.png"), 0, 20, 64, 64) val ON = Texture(Identifier("textures/gui/checkbox.png"), 0, 20, 64, 64)
val ON_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 20, 64, 64) val ON_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 20, 64, 64)
} }
private val textureView: TextureView private val textureView: TextureView
get() = content as TextureView get() = content as TextureView
/** /**
* The button's current on/off state. * The button's current on/off state.
* Updating this property updates the button's texture. * Updating this property updates the button's texture.
*/ */
var state: Boolean = initialState var state: Boolean = initialState
init { init {
this.handler = handler this.handler = handler
intrinsicContentSize = Size(20.0, 20.0) intrinsicContentSize = Size(20.0, 20.0)
background = null background = null
disabledBackground = null disabledBackground = null
hoveredBackground = null hoveredBackground = null
} }
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) { if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
state = !state state = !state
} }
return super.mouseClicked(point, mouseButton) return super.mouseClicked(point, mouseButton)
} }
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
val hovered = mouse in bounds val hovered = mouse in bounds
textureView.texture = if (state) { textureView.texture = if (state) {
if (hovered) ON_HOVERED else ON if (hovered) ON_HOVERED else ON
} else { } else {
if (hovered) OFF_HOVERED else OFF if (hovered) OFF_HOVERED else OFF
} }
super.draw(matrixStack, mouse, delta) super.draw(matrixStack, mouse, delta)
} }
override fun getCurrentBackground(mouse: Point) = null override fun getCurrentBackground(mouse: Point) = null
} }

View File

@ -20,157 +20,158 @@ import org.lwjgl.glfw.GLFW
* the exact type of text field. * the exact type of text field.
* @param initialText The initial value of the text field. * @param initialText The initial value of the text field.
*/ */
abstract class AbstractTextField<Impl: AbstractTextField<Impl>>( abstract class AbstractTextField<Impl : AbstractTextField<Impl>>(
initialText: String initialText: String
): View() { ) : View() {
/** /**
* A function that is invoked when the text in this text field changes. * A function that is invoked when the text in this text field changes.
*/ */
var handler: ((Impl) -> Unit)? = null var handler: ((Impl) -> Unit)? = null
/** /**
* Whether the text field is disabled. * Whether the text field is disabled.
* Disabled text fields cannot be interacted with. * Disabled text fields cannot be interacted with.
*/ */
var disabled = false var disabled = false
/** /**
* Whether this text field is focused (i.e. [isFirstResponder]) and receives key events. * Whether this text field is focused (i.e. [isFirstResponder]) and receives key events.
*/ */
val focused: Boolean val focused: Boolean
get() = isFirstResponder get() = isFirstResponder
/** /**
* The current text of this text field. * The current text of this text field.
*/ */
var text: String var text: String
get() = minecraftWidget.text get() = minecraftWidget.text
set(value) { set(value) {
minecraftWidget.text = value minecraftWidget.text = value
} }
/** /**
* The maximum length of text that this text field can hold. * The maximum length of text that this text field can hold.
* *
* Defaults to the Minecraft text field's maximum length (currently 32, subject to change). * Defaults to the Minecraft text field's maximum length (currently 32, subject to change).
*/ */
var maxLength: Int var maxLength: Int
get() = (minecraftWidget as TextFieldWidgetAccessor).cacao_getMaxLength() get() = (minecraftWidget as TextFieldWidgetAccessor).cacao_getMaxLength()
set(value) { set(value) {
minecraftWidget.setMaxLength(value) minecraftWidget.setMaxLength(value)
} }
/** /**
* Whether the Minecraft builtin black background and border are drawn. Defaults to true. * Whether the Minecraft builtin black background and border are drawn. Defaults to true.
*/ */
var drawBackground = true var drawBackground = true
set(value) { set(value) {
field = value field = value
minecraftWidget.setDrawsBackground(value) minecraftWidget.setDrawsBackground(value)
} }
private lateinit var originInWindow: Point private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget() private var minecraftWidget = ProxyWidget()
init { init {
minecraftWidget.text = initialText minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) } minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setDrawsBackground(drawBackground) minecraftWidget.setDrawsBackground(drawBackground)
} }
/** /**
* A function used by subclasses to determine whether a proposed value is acceptable for this field. * A function used by subclasses to determine whether a proposed value is acceptable for this field.
*/ */
abstract fun validate(proposedText: String): Boolean abstract fun validate(proposedText: String): Boolean
override fun didLayout() { override fun didLayout() {
super.didLayout() super.didLayout()
originInWindow = convert(bounds.origin, to = null) originInWindow = convert(bounds.origin, to = null)
// offset View dimensions by 1 on each side because TextFieldWidget draws the border outside its dimensions // offset View dimensions by 1 on each side because TextFieldWidget draws the border outside its dimensions
minecraftWidget.x = originInWindow.x.toInt() + 1 minecraftWidget.x = originInWindow.x.toInt() + 1
minecraftWidget.y = originInWindow.y.toInt() + 1 minecraftWidget.y = originInWindow.y.toInt() + 1
minecraftWidget.width = bounds.width.toInt() - 2 minecraftWidget.width = bounds.width.toInt() - 2
minecraftWidget.height = bounds.height.toInt() - 2 minecraftWidget.height = bounds.height.toInt() - 2
// after dimensions change call setText on the widget to make sure its internal scroll position is up-to-date // after dimensions change call setText on the widget to make sure its internal scroll position is up-to-date
minecraftWidget.text = text minecraftWidget.text = text
} }
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push() matrixStack.push()
matrixStack.translate(-originInWindow.x, -originInWindow.y, 0.0) matrixStack.translate(-originInWindow.x, -originInWindow.y, 0.0)
val mouseXInWindow = (mouse.x + originInWindow.x).toInt() val mouseXInWindow = (mouse.x + originInWindow.x).toInt()
val mouseYInWindow = (mouse.y + originInWindow.y).toInt() val mouseYInWindow = (mouse.y + originInWindow.y).toInt()
minecraftWidget.render(matrixStack, mouseXInWindow, mouseYInWindow, delta) minecraftWidget.render(matrixStack, mouseXInWindow, mouseYInWindow, delta)
matrixStack.pop() matrixStack.pop()
} }
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) { if (!disabled) {
if (focused) { if (focused) {
val mouseXInWindow = (point.x + originInWindow.x) val mouseXInWindow = (point.x + originInWindow.x)
val mouseYInWindow = (point.y + originInWindow.y) val mouseYInWindow = (point.y + originInWindow.y)
minecraftWidget.mouseClicked(mouseXInWindow, mouseYInWindow, mouseButton.ordinal) minecraftWidget.mouseClicked(mouseXInWindow, mouseYInWindow, mouseButton.ordinal)
} else { } else {
becomeFirstResponder() becomeFirstResponder()
} }
} }
// don't play sound when interacting with text field // don't play sound when interacting with text field
return false return false
} }
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) { override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
if (focused) { if (focused) {
resignFirstResponder() resignFirstResponder()
} }
} }
override fun didBecomeFirstResponder() { override fun didBecomeFirstResponder() {
super.didBecomeFirstResponder() super.didBecomeFirstResponder()
minecraftWidget.setTextFieldFocused(true) minecraftWidget.setTextFieldFocused(true)
} }
override fun didResignFirstResponder() { override fun didResignFirstResponder() {
super.didResignFirstResponder() super.didResignFirstResponder()
minecraftWidget.setTextFieldFocused(false) minecraftWidget.setTextFieldFocused(false)
} }
override fun charTyped(char: Char, modifiers: KeyModifiers): Boolean { override fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
val oldText = text val oldText = text
val result = minecraftWidget.charTyped(char, modifiers.value) val result = minecraftWidget.charTyped(char, modifiers.value)
if (text != oldText) { if (text != oldText) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl) handler?.invoke(this as Impl)
} }
return result return result
} }
override fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean { override fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
val oldText = text val oldText = text
// scanCode isn't used by TextFieldWidget, hopefully this doesn't break :/ // scanCode isn't used by TextFieldWidget, hopefully this doesn't break :/
val result = minecraftWidget.keyPressed(keyCode, -1, modifiers.value) val result = minecraftWidget.keyPressed(keyCode, -1, modifiers.value)
if (text != oldText) { if (text != oldText) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl) handler?.invoke(this as Impl)
} }
return result || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE) return result || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE)
} }
fun tick() { fun tick() {
minecraftWidget.tick() minecraftWidget.tick()
} }
// todo: label for the TextFieldWidget? // todo: label for the TextFieldWidget?
private class ProxyWidget: TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) { private class ProxyWidget :
// AbstractButtonWidget.height is protected TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
fun setHeight(height: Int) { // AbstractButtonWidget.height is protected
this.height = height fun setHeight(height: Int) {
} this.height = height
} }
}
} }

View File

@ -4,43 +4,43 @@ package net.shadowfacts.cacao.view.textfield
* @author shadowfacts * @author shadowfacts
*/ */
open class NumberField( open class NumberField(
initialValue: Int, initialValue: Int,
handler: ((NumberField) -> Unit)? = null, handler: ((NumberField) -> Unit)? = null,
): AbstractTextField<NumberField>(initialValue.toString()) { ) : AbstractTextField<NumberField>(initialValue.toString()) {
var number: Int? var number: Int?
get() { get() {
return if (isTextTemporarilyAllowed(text)) { return if (isTextTemporarilyAllowed(text)) {
null null
} else { } else {
try { try {
Integer.parseInt(text) Integer.parseInt(text)
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
null null
} }
} }
} }
set(value) { set(value) {
text = value?.toString() ?: "" text = value?.toString() ?: ""
} }
var validator: ((Int) -> Boolean)? = null var validator: ((Int) -> Boolean)? = null
init { init {
this.handler = handler this.handler = handler
} }
override fun validate(proposedText: String): Boolean { override fun validate(proposedText: String): Boolean {
return isTextTemporarilyAllowed(proposedText) || try { return isTextTemporarilyAllowed(proposedText) || try {
val value = Integer.parseInt(proposedText) val value = Integer.parseInt(proposedText)
validator?.invoke(value) ?: true validator?.invoke(value) ?: true
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
false false
} }
} }
private fun isTextTemporarilyAllowed(s: String): Boolean { private fun isTextTemporarilyAllowed(s: String): Boolean {
return s.isEmpty() || s == "-" return s.isEmpty() || s == "-"
} }
} }

View File

@ -8,14 +8,14 @@ package net.shadowfacts.cacao.view.textfield
* @param handler A function that is invoked when the value of the text field changes. * @param handler A function that is invoked when the value of the text field changes.
*/ */
open class TextField( open class TextField(
initialText: String, initialText: String,
handler: ((TextField) -> Unit)? = null handler: ((TextField) -> Unit)? = null
): AbstractTextField<TextField>(initialText) { ) : AbstractTextField<TextField>(initialText) {
init { init {
this.handler = handler this.handler = handler
} }
override fun validate(proposedText: String): Boolean { override fun validate(proposedText: String): Boolean {
return true return true
} }
} }

View File

@ -34,266 +34,267 @@ import java.lang.RuntimeException
* @param onTabChange A function invoked immediately after the active tab has changed (and the content view has been * @param onTabChange A function invoked immediately after the active tab has changed (and the content view has been
* updated). * updated).
*/ */
class TabViewController<T: TabViewController.Tab>( class TabViewController<T : TabViewController.Tab>(
val tabs: List<T>, val tabs: List<T>,
initialTab: T = tabs.first(), initialTab: T = tabs.first(),
val onTabChange: ((T) -> Unit)? = null val onTabChange: ((T) -> Unit)? = null
): ViewController() { ) : ViewController() {
/** /**
* The Tab interface defines the requirements for tab objects that can be used with this view controller. * The Tab interface defines the requirements for tab objects that can be used with this view controller.
* *
* This is an interface to allow for tab objects to carry additional data. A simple implementation is provided. * This is an interface to allow for tab objects to carry additional data. A simple implementation is provided.
* @see SimpleTab * @see SimpleTab
*/ */
interface Tab { interface Tab {
/** /**
* The view displayed on the button for this tab. * The view displayed on the button for this tab.
*/ */
val tabView: View val tabView: View
/** /**
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown. * The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
*/ */
val tooltip: Text? val tooltip: Text?
/** /**
* The view controller used as content when this tab is active. When switching tabs, the returned content VC * The view controller used as content when this tab is active. When switching tabs, the returned content VC
* may be reused or created from scratch each time. * may be reused or created from scratch each time.
*/ */
val controller: ViewController val controller: ViewController
/** /**
* Used by the tab view controller to determine whether the button for this tab should be displayed. * Used by the tab view controller to determine whether the button for this tab should be displayed.
* If the conditions that control this change, call [TabViewController.visibleTabsChanged]. * If the conditions that control this change, call [TabViewController.visibleTabsChanged].
*/ */
val isVisible: Boolean val isVisible: Boolean
get() = true get() = true
} }
/** /**
* A simple [Tab] implementation that provides the minimum necessary information. * A simple [Tab] implementation that provides the minimum necessary information.
* @param tabView The view to display on the tab's button. * @param tabView The view to display on the tab's button.
* @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none). * @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none).
* @param controller The content view controller for this tab. * @param controller The content view controller for this tab.
* @param visible A function that determines if the tab should currently be visible. * @param visible A function that determines if the tab should currently be visible.
*/ */
class SimpleTab( class SimpleTab(
override val tabView: View, override val tabView: View,
override val tooltip: Text? = null, override val tooltip: Text? = null,
override val controller: ViewController, override val controller: ViewController,
private val visible: (() -> Boolean)? = null private val visible: (() -> Boolean)? = null
): Tab { ) : Tab {
override val isVisible: Boolean override val isVisible: Boolean
get() = visible?.invoke() ?: true get() = visible?.invoke() ?: true
} }
/** /**
* The currently selected tab. * The currently selected tab.
*/ */
var currentTab: T = initialTab var currentTab: T = initialTab
private set private set
private lateinit var tabButtons: List<TabButton<T>> private lateinit var tabButtons: List<TabButton<T>>
private lateinit var outerStack: StackView private lateinit var outerStack: StackView
private lateinit var tabStack: StackView private lateinit var tabStack: StackView
private lateinit var currentTabController: ViewController private lateinit var currentTabController: ViewController
// todo: this shouldn't be public, use layout guides
lateinit var tabVCContainer: View
private set
override fun viewDidLoad() { // todo: this shouldn't be public, use layout guides
super.viewDidLoad() lateinit var tabVCContainer: View
private set
// todo: might be simpler to just not use a stack view override fun viewDidLoad() {
// padding is -4 so tab button texture overlaps with panel BG as expected super.viewDidLoad()
outerStack = StackView(Axis.VERTICAL, StackView.Distribution.FILL, -4.0)
view.addSubview(outerStack)
tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL) // todo: might be simpler to just not use a stack view
tabStack.zIndex = 1.0 // padding is -4 so tab button texture overlaps with panel BG as expected
outerStack.addArrangedSubview(tabStack) outerStack = StackView(Axis.VERTICAL, StackView.Distribution.FILL, -4.0)
updateTabButtons() view.addSubview(outerStack)
val background = NinePatchView(NinePatchTexture.PANEL_BG) tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL)
outerStack.addArrangedSubview(background) tabStack.zIndex = 1.0
outerStack.addArrangedSubview(tabStack)
updateTabButtons()
tabVCContainer = View() val background = NinePatchView(NinePatchTexture.PANEL_BG)
tabVCContainer.zIndex = 1.0 outerStack.addArrangedSubview(background)
view.addSubview(tabVCContainer)
currentTabController = currentTab.controller tabVCContainer = View()
currentTabController.willMoveTo(this) tabVCContainer.zIndex = 1.0
embedChild(currentTabController, tabVCContainer) view.addSubview(tabVCContainer)
currentTabController.didMoveTo(this)
// will/did appear events for the initial VC are provided by this class' implementations of those
view.solver.dsl { currentTabController = currentTab.controller
outerStack.leftAnchor equalTo view.leftAnchor currentTabController.willMoveTo(this)
outerStack.rightAnchor equalTo view.rightAnchor embedChild(currentTabController, tabVCContainer)
outerStack.topAnchor equalTo view.topAnchor currentTabController.didMoveTo(this)
outerStack.bottomAnchor equalTo view.bottomAnchor // will/did appear events for the initial VC are provided by this class' implementations of those
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6) view.solver.dsl {
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6) outerStack.leftAnchor equalTo view.leftAnchor
tabVCContainer.topAnchor equalTo (background.topAnchor + 6) outerStack.rightAnchor equalTo view.rightAnchor
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6) outerStack.topAnchor equalTo view.topAnchor
} outerStack.bottomAnchor equalTo view.bottomAnchor
}
override fun viewWillAppear() { tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
super.viewWillAppear() tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
currentTabController.viewWillAppear() tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
} tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
}
override fun viewWillDisappear() { override fun viewWillAppear() {
super.viewWillDisappear() super.viewWillAppear()
currentTabController.viewWillDisappear() currentTabController.viewWillAppear()
} }
override fun viewDidDisappear() { override fun viewWillDisappear() {
super.viewDidDisappear() super.viewWillDisappear()
currentTabController.viewDidDisappear() currentTabController.viewWillDisappear()
} }
private fun updateTabButtons() { override fun viewDidDisappear() {
while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first()) super.viewDidDisappear()
currentTabController.viewDidDisappear()
}
tabButtons = tabs.mapNotNull { tab -> private fun updateTabButtons() {
if (!tab.isVisible) { while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first())
return@mapNotNull null
}
val btn = TabButton(tab) tabButtons = tabs.mapNotNull { tab ->
btn.handler = { selectTab(it.tab) } if (!tab.isVisible) {
if (tab == currentTab) { return@mapNotNull null
btn.setSelected(true) }
}
btn
}
// todo: batch calls to addArrangedSubview
tabButtons.forEach(tabStack::addArrangedSubview)
// spacer val btn = TabButton(tab)
tabStack.addArrangedSubview(View()) btn.handler = { selectTab(it.tab) }
if (tab == currentTab) {
btn.setSelected(true)
}
btn
}
// todo: batch calls to addArrangedSubview
tabButtons.forEach(tabStack::addArrangedSubview)
window!!.layout() // spacer
} tabStack.addArrangedSubview(View())
/** window!!.layout()
* Call this method when the conditions that make the configured tabs visible change. }
*/
fun visibleTabsChanged() {
updateTabButtons()
}
/** /**
* Sets the provided tab as the currently active tab for this controller. Updates the state of tab bar buttons and * Call this method when the conditions that make the configured tabs visible change.
* swaps the content view controller. */
* fun visibleTabsChanged() {
* After the tab and the content are changed, [onTabChange] is invoked. updateTabButtons()
* }
* @throws RuntimeException If the provided tab was not passed in as part of the [tabs] list.
*/
fun selectTab(tab: T) {
if (!tabs.contains(tab)) {
throw RuntimeException("Cannot activate tab not in TabViewController.tabs")
}
val oldTab = currentTab /**
currentTab = tab * Sets the provided tab as the currently active tab for this controller. Updates the state of tab bar buttons and
* swaps the content view controller.
*
* After the tab and the content are changed, [onTabChange] is invoked.
*
* @throws RuntimeException If the provided tab was not passed in as part of the [tabs] list.
*/
fun selectTab(tab: T) {
if (!tabs.contains(tab)) {
throw RuntimeException("Cannot activate tab not in TabViewController.tabs")
}
tabButtons.forEach { val oldTab = currentTab
it.setSelected(it.tab === tab) currentTab = tab
}
currentTabController.viewWillDisappear()
currentTabController.view.removeFromSuperview()
currentTabController.viewDidDisappear()
currentTabController.willMoveTo(null)
currentTabController.removeFromParent()
currentTabController.didMoveTo(null)
currentTabController = currentTab.controller tabButtons.forEach {
it.setSelected(it.tab === tab)
}
currentTabController.viewWillDisappear()
currentTabController.view.removeFromSuperview()
currentTabController.viewDidDisappear()
currentTabController.willMoveTo(null)
currentTabController.removeFromParent()
currentTabController.didMoveTo(null)
currentTabController.willMoveTo(this) currentTabController = currentTab.controller
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
currentTabController.viewWillAppear()
onTabChange?.invoke(currentTab) currentTabController.willMoveTo(this)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
currentTabController.viewWillAppear()
// todo: setNeedsLayout onTabChange?.invoke(currentTab)
window!!.layout()
}
private class TabButton<T: Tab>( // todo: setNeedsLayout
val tab: T, window!!.layout()
): AbstractButton<TabButton<T>>( }
tab.tabView,
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("phycon:textures/gui/tabs.png")
}
private var selected = false private class TabButton<T : Tab>(
private var backgroundView = TextureView(Texture(BACKGROUND, 0, 0)) val tab: T,
) : AbstractButton<TabButton<T>>(
tab.tabView,
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("phycon:textures/gui/tabs.png")
}
init { private var selected = false
intrinsicContentSize = Size(28.0, 32.0) private var backgroundView = TextureView(Texture(BACKGROUND, 0, 0))
background = null
hoveredBackground = null
disabledBackground = null
}
override fun wasAdded() { init {
super.wasAdded() intrinsicContentSize = Size(28.0, 32.0)
backgroundView.usesConstraintBasedLayout = false background = null
backgroundView.frame = Rect(0.0, 0.0, 28.0, 32.0) hoveredBackground = null
backgroundView.zIndex = -1.0 disabledBackground = null
addSubview(backgroundView) }
solver.dsl {
content.bottomAnchor lessThanOrEqualTo (bottomAnchor - 4)
}
}
override fun didLayout() { override fun wasAdded() {
super.didLayout() super.wasAdded()
updateBackgroundTexture() backgroundView.usesConstraintBasedLayout = false
} backgroundView.frame = Rect(0.0, 0.0, 28.0, 32.0)
backgroundView.zIndex = -1.0
addSubview(backgroundView)
solver.dsl {
content.bottomAnchor lessThanOrEqualTo (bottomAnchor - 4)
}
}
fun setSelected(selected: Boolean) { override fun didLayout() {
this.selected = selected super.didLayout()
updateBackgroundTexture() updateBackgroundTexture()
} }
override fun getCurrentBackground(mouse: Point) = backgroundView fun setSelected(selected: Boolean) {
this.selected = selected
updateBackgroundTexture()
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) { override fun getCurrentBackground(mouse: Point) = backgroundView
super.draw(matrixStack, mouse, delta)
if (mouse in bounds && tab.tooltip != null) { override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
window!!.drawTooltip(listOf(tab.tooltip!!)) super.draw(matrixStack, mouse, delta)
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { if (mouse in bounds && tab.tooltip != null) {
if (selected) return false window!!.drawTooltip(listOf(tab.tooltip!!))
else return super.mouseClicked(point, mouseButton) }
} }
private fun updateBackgroundTexture() { override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val v = if (selected) 32 else 0 if (selected) return false
val u = when { else return super.mouseClicked(point, mouseButton)
superview == null -> 0 }
frame.left == 0.0 -> 0
frame.right == superview!!.bounds.right -> 56 private fun updateBackgroundTexture() {
else -> 28 val v = if (selected) 32 else 0
} val u = when {
backgroundView.texture = Texture(BACKGROUND, u, v) superview == null -> 0
backgroundView.frame = Rect(0.0, 0.0, 28.0, if (selected) 32.0 else 28.0) frame.left == 0.0 -> 0
} frame.right == superview!!.bounds.right -> 56
} else -> 28
}
backgroundView.texture = Texture(BACKGROUND, u, v)
backgroundView.frame = Rect(0.0, 0.0, 28.0, if (selected) 32.0 else 28.0)
}
}
} }

View File

@ -16,111 +16,112 @@ import java.util.*
*/ */
abstract class ViewController { abstract class ViewController {
/** /**
* The window that contains this view controller. * The window that contains this view controller.
* This property is not set until either: * This property is not set until either:
* a) a [Window] is initialized with this VC as it's root view controller or * a) a [Window] is initialized with this VC as it's root view controller or
* b) this VC is added as a child of another view controller. * b) this VC is added as a child of another view controller.
*/ */
var window: Window? = null var window: Window? = null
set(value) { set(value) {
field = value field = value
for (vc in children) { for (vc in children) {
vc.window = value vc.window = value
} }
} }
/** /**
* Helper function for creating layout constraints in the domain of this VC's window. * Helper function for creating layout constraints in the domain of this VC's window.
* This function is not usable until [window] is initialized. * This function is not usable until [window] is initialized.
*/ */
val createConstraints val createConstraints
get() = window!!.solver::dsl get() = window!!.solver::dsl
/** /**
* The view that this View Controller has. * The view that this View Controller has.
* This property is created by [loadView] and is not initialized before that method has been called. * This property is created by [loadView] and is not initialized before that method has been called.
* *
* @see loadView * @see loadView
*/ */
lateinit var view: View lateinit var view: View
protected set protected set
/** /**
* This VC's parent view controller. If `null`, this VC is the root view controller of its [window]. * This VC's parent view controller. If `null`, this VC is the root view controller of its [window].
*/ */
var parent: ViewController? = null var parent: ViewController? = null
set(value) { set(value) {
willMoveTo(value) willMoveTo(value)
field = value field = value
didMoveTo(value) didMoveTo(value)
} }
// _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods // _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods
private var _children = LinkedList<ViewController>() private var _children = LinkedList<ViewController>()
/**
* The list of all the child VCs of this view controller.
* This list should never be mutated directly, only by the [embedChild]/[removeChild] methods.
*/
val children: List<ViewController> = _children
/** /**
* This method somehow loads a [View] and sets this VC's [view] property to it. * The list of all the child VCs of this view controller.
* * This list should never be mutated directly, only by the [embedChild]/[removeChild] methods.
* This method should only be called by the framework. After the [view] property is set, the framework is */
* responsible for initializing its [View.window]/[View.solver] properties and calling [View.wasAdded]. val children: List<ViewController> = _children
*
* The default implementation simply creates a [View] and does nothing else with it.
*/
open fun loadView() {
view = View()
}
/** /**
* If the view for this controller has already been loaded. * This method somehow loads a [View] and sets this VC's [view] property to it.
*/ *
val isViewLoaded: Boolean * This method should only be called by the framework. After the [view] property is set, the framework is
get() = ::view.isInitialized * responsible for initializing its [View.window]/[View.solver] properties and calling [View.wasAdded].
*
* The default implementation simply creates a [View] and does nothing else with it.
*/
open fun loadView() {
view = View()
}
/** /**
* Calls [loadView] to load this controller's view only if it has not already been loaded. * If the view for this controller has already been loaded.
*/ */
fun loadViewIfNeeded() { val isViewLoaded: Boolean
if (!isViewLoaded) { get() = ::view.isInitialized
loadView()
}
}
/** /**
* This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been * Calls [loadView] to load this controller's view only if it has not already been loaded.
* called. */
*/ fun loadViewIfNeeded() {
open fun viewDidLoad() {} if (!isViewLoaded) {
loadView()
}
}
/** /**
* This method is called immediately before the [Window.solver] is going to solve constraints and update variables. * This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been
* If overridden, the superclass method must be called. * called.
*/ */
open fun viewWillLayoutSubviews() { open fun viewDidLoad() {}
children.forEach(ViewController::viewWillLayoutSubviews)
}
/** /**
* This method is called immediately after the [Window.solver] has solved constraints and variables have been updated. * This method is called immediately before the [Window.solver] is going to solve constraints and update variables.
* This method is responsible for invoking the VC's [View.didLayout] method. * If overridden, the superclass method must be called.
* If overridden, the superclass method must be called. */
*/ open fun viewWillLayoutSubviews() {
open fun viewDidLayoutSubviews() { children.forEach(ViewController::viewWillLayoutSubviews)
view.didLayout() }
children.forEach(ViewController::viewDidLayoutSubviews)
}
/** /**
* Called when the VC's view has been added to the screen and is about to be displayed. * This method is called immediately after the [Window.solver] has solved constraints and variables have been updated.
*/ * This method is responsible for invoking the VC's [View.didLayout] method.
open fun viewWillAppear() { * If overridden, the superclass method must be called.
children.forEach(ViewController::viewWillAppear) */
} open fun viewDidLayoutSubviews() {
view.didLayout()
children.forEach(ViewController::viewDidLayoutSubviews)
}
/**
* Called when the VC's view has been added to the screen and is about to be displayed.
*/
open fun viewWillAppear() {
children.forEach(ViewController::viewWillAppear)
}
// /** // /**
// * Called immediately after the VC's view has first been displayed on screen. // * Called immediately after the VC's view has first been displayed on screen.
@ -129,87 +130,87 @@ abstract class ViewController {
// children.forEach(ViewController::viewDidAppear) // children.forEach(ViewController::viewDidAppear)
// } // }
/** /**
* Called before the view will disappear from the screen, either because the VC has been removed from it's parent/screen * Called before the view will disappear from the screen, either because the VC has been removed from it's parent/screen
* or because the [net.shadowfacts.cacao.CacaoScreen] has been closed. * or because the [net.shadowfacts.cacao.CacaoScreen] has been closed.
*/ */
open fun viewWillDisappear() { open fun viewWillDisappear() {
children.forEach(ViewController::viewWillDisappear) children.forEach(ViewController::viewWillDisappear)
} }
/** /**
* Called after the view has disappeared from the screen. * Called after the view has disappeared from the screen.
*/ */
open fun viewDidDisappear() { open fun viewDidDisappear() {
children.forEach(ViewController::viewDidDisappear) children.forEach(ViewController::viewDidDisappear)
} }
/** /**
* Called before the view controller's parent changes to the given new value. * Called before the view controller's parent changes to the given new value.
* *
* @param parent The new parent view controller. * @param parent The new parent view controller.
*/ */
open fun willMoveTo(parent: ViewController?) {} open fun willMoveTo(parent: ViewController?) {}
/** /**
* Called after the view controller's parent has changed to the given new value. * Called after the view controller's parent has changed to the given new value.
* *
* @param parent The new parent view controller. * @param parent The new parent view controller.
*/ */
open fun didMoveTo(parent: ViewController?) {} open fun didMoveTo(parent: ViewController?) {}
/** /**
* Embeds a child view controller in this VC. * Embeds a child view controller in this VC.
* *
* @param viewController The new child VC. * @param viewController The new child VC.
* @param container The view that will be used as the superview for the child VC's view. Defaults to this VC's [view]. * @param container The view that will be used as the superview for the child VC's view. Defaults to this VC's [view].
* @param pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges. * @param pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges.
* Defaults to `true`. * Defaults to `true`.
*/ */
fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) { fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) {
viewController.parent = this viewController.parent = this
viewController.window = window viewController.window = window
_children.add(viewController) _children.add(viewController)
val wasViewLoaded = viewController.isViewLoaded val wasViewLoaded = viewController.isViewLoaded
viewController.loadViewIfNeeded() viewController.loadViewIfNeeded()
container.addSubview(viewController.view) container.addSubview(viewController.view)
if (pinEdges) { if (pinEdges) {
createConstraints { createConstraints {
viewController.view.leftAnchor equalTo container.leftAnchor viewController.view.leftAnchor equalTo container.leftAnchor
viewController.view.rightAnchor equalTo container.rightAnchor viewController.view.rightAnchor equalTo container.rightAnchor
viewController.view.topAnchor equalTo container.topAnchor viewController.view.topAnchor equalTo container.topAnchor
viewController.view.bottomAnchor equalTo container.bottomAnchor viewController.view.bottomAnchor equalTo container.bottomAnchor
} }
} }
if (!wasViewLoaded) { if (!wasViewLoaded) {
viewController.viewDidLoad() viewController.viewDidLoad()
} }
} }
/** /**
* Removes the given view controller * Removes the given view controller
* *
* @param viewController The child VC to remove from this view controller. * @param viewController The child VC to remove from this view controller.
* @throws RuntimeException If the given [viewController] is not a child of this VC. * @throws RuntimeException If the given [viewController] is not a child of this VC.
*/ */
fun removeChild(viewController: ViewController) { fun removeChild(viewController: ViewController) {
if (viewController.parent != this) { if (viewController.parent != this) {
throw RuntimeException("Cannot remove child view controller whose parent is not this view controller") throw RuntimeException("Cannot remove child view controller whose parent is not this view controller")
} }
viewController.parent = null viewController.parent = null
_children.remove(viewController) _children.remove(viewController)
} }
/** /**
* Removes this view controller from its parent, if it has one. * Removes this view controller from its parent, if it has one.
*/ */
fun removeFromParent() { fun removeFromParent() {
parent?.removeChild(this) parent?.removeChild(this)
view.removeFromSuperview() view.removeFromSuperview()
} }
} }

View File

@ -7,8 +7,8 @@ import net.shadowfacts.cacao.viewcontroller.ViewController
* @author shadowfacts * @author shadowfacts
*/ */
class ScreenHandlerWindow( class ScreenHandlerWindow(
val screenHandler: ScreenHandler, val screenHandler: ScreenHandler,
viewController: ViewController viewController: ViewController
): Window(viewController) { ) : Window(viewController) {
} }

View File

@ -27,249 +27,256 @@ import java.lang.RuntimeException
* @param viewController The root view controller for this window. * @param viewController The root view controller for this window.
*/ */
open class Window( open class Window(
/** /**
* The root view controller for this window. * The root view controller for this window.
*/ */
val viewController: ViewController val viewController: ViewController
) { ) {
/** /**
* The screen that this window belongs to. * The screen that this window belongs to.
* Not initialized until this window is added to a screen, using it before that point will throw a runtime exception. * Not initialized until this window is added to a screen, using it before that point will throw a runtime exception.
*/ */
lateinit var screen: AbstractCacaoScreen lateinit var screen: AbstractCacaoScreen
/** /**
* The constraint solver used by this window and all its views and subviews. * The constraint solver used by this window and all its views and subviews.
*/ */
var solver = Solver() var solver = Solver()
/** /**
* Layout anchor for the left edge of this view in the window's coordinate system. * Layout anchor for the left edge of this view in the window's coordinate system.
*/ */
val leftAnchor = Variable("left") val leftAnchor = Variable("left")
/**
* Layout anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = Variable("right")
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
val topAnchor = Variable("top")
/**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = Variable("bottom")
/**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = Variable("width")
/**
* Layout anchor for the height of this view in the window's coordinate system.
*/
val heightAnchor = Variable("height")
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = Variable("centerX")
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
val centerYAnchor = Variable("centerY")
/** /**
* The first responder of the a window is the first object that receives indirect events (e.g., keypresses). * Layout anchor for the right edge of this view in the window's coordinate system.
* */
* When an indirect event is received by the window, it is given to the first responder. If the first responder does val rightAnchor = Variable("right")
* not accept it (i.e. returns `false` from the appropriate method), the event will be passed to that responder's
* [Responder.nextResponder], and so on.
*
* The following is the order of events when setting this property:
* 1. The old first responder (if any) has [Responder.didResignFirstResponder] invoked.
* 2. The value of the field is updated.
* 3. The new value (if any) has [Responder.didBecomeFirstResponder] invoked.
*/
var firstResponder: Responder? = null
set(value) {
field?.didResignFirstResponder()
field = value
field?.didBecomeFirstResponder()
}
// internal constraints that specify the window size based on the MC screen size /**
// stored so that they can be removed when the screen is resized * Layout anchor for the top edge of this view in the window's coordinate system.
private var widthConstraint: Constraint? = null */
private var heightConstraint: Constraint? = null val topAnchor = Variable("top")
private var currentDragReceiver: View? = null /**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = Variable("bottom")
private var currentDeferredTooltip: List<Text>? = null /**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = Variable("width")
init { /**
createInternalConstraints() * Layout anchor for the height of this view in the window's coordinate system.
} */
val heightAnchor = Variable("height")
fun wasAdded() { /**
viewController.window = this * Layout anchor for the center X position of this view in the window's coordinate system.
viewController.loadViewIfNeeded() */
val centerXAnchor = Variable("centerX")
viewController.view.window = this /**
viewController.view.solver = solver * Layout anchor for the center Y position of this view in the window's coordinate system.
viewController.view.wasAdded() */
viewController.createConstraints { val centerYAnchor = Variable("centerY")
viewController.view.leftAnchor equalTo leftAnchor
viewController.view.rightAnchor equalTo rightAnchor
viewController.view.topAnchor equalTo topAnchor
viewController.view.bottomAnchor equalTo bottomAnchor
}
viewController.viewDidLoad() /**
* The first responder of the a window is the first object that receives indirect events (e.g., keypresses).
*
* When an indirect event is received by the window, it is given to the first responder. If the first responder does
* not accept it (i.e. returns `false` from the appropriate method), the event will be passed to that responder's
* [Responder.nextResponder], and so on.
*
* The following is the order of events when setting this property:
* 1. The old first responder (if any) has [Responder.didResignFirstResponder] invoked.
* 2. The value of the field is updated.
* 3. The new value (if any) has [Responder.didBecomeFirstResponder] invoked.
*/
var firstResponder: Responder? = null
set(value) {
field?.didResignFirstResponder()
field = value
field?.didBecomeFirstResponder()
}
layout() // internal constraints that specify the window size based on the MC screen size
} // stored so that they can be removed when the screen is resized
private var widthConstraint: Constraint? = null
private var heightConstraint: Constraint? = null
/** private var currentDragReceiver: View? = null
* Creates the internal constraints used by the window.
* If overridden, the super-class method must be called.
*/
protected fun createInternalConstraints() {
solver.dsl {
leftAnchor equalTo 0
topAnchor equalTo 0
rightAnchor equalTo (leftAnchor + widthAnchor) private var currentDeferredTooltip: List<Text>? = null
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
/** init {
* Called by the window's [screen] when the Minecraft screen is resized. createInternalConstraints()
* Used to update the window's width and height constraints and re-layout views. }
*/
internal fun resize(width: Int, height: Int) {
if (widthConstraint != null) solver.removeConstraint(widthConstraint)
if (heightConstraint != null) solver.removeConstraint(heightConstraint)
solver.dsl {
widthConstraint = (widthAnchor equalTo width)
heightConstraint = (heightAnchor equalTo height)
}
layout()
}
/** fun wasAdded() {
* Convenience method that removes this window from its [screen]. viewController.window = this
*/ viewController.loadViewIfNeeded()
fun removeFromScreen() {
viewController.viewWillDisappear()
screen.removeWindow(this)
viewController.viewDidDisappear()
}
/** viewController.view.window = this
* Instructs the solver to solve all of the provided constraints. viewController.view.solver = solver
* Should be called after the view hierarchy is setup. viewController.view.wasAdded()
*/ viewController.createConstraints {
fun layout() { viewController.view.leftAnchor equalTo leftAnchor
viewController.viewWillLayoutSubviews() viewController.view.rightAnchor equalTo rightAnchor
solver.updateVariables() viewController.view.topAnchor equalTo topAnchor
viewController.viewDidLayoutSubviews() viewController.view.bottomAnchor equalTo bottomAnchor
} }
/** viewController.viewDidLoad()
* Draws this window and all of its views.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param mouse The point in the coordinate system of the window.
* @param delta The time elapsed since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
currentDeferredTooltip = null
val mouseInView = Point(mouse.x - viewController.view.frame.left, mouse.y - viewController.view.frame.top) layout()
viewController.view.draw(matrixStack, mouseInView, delta) }
if (currentDeferredTooltip != null) { /**
RenderHelper.drawTooltip(matrixStack, currentDeferredTooltip!!, mouse) * Creates the internal constraints used by the window.
} * If overridden, the super-class method must be called.
} */
protected fun createInternalConstraints() {
solver.dsl {
leftAnchor equalTo 0
topAnchor equalTo 0
/** rightAnchor equalTo (leftAnchor + widthAnchor)
* Draw a tooltip containing the given lines at the mouse pointer location. bottomAnchor equalTo (topAnchor + heightAnchor)
* centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
* Implementation note: the tooltip is not drawn immediately, it is done after the window is done drawing all of its centerYAnchor equalTo (topAnchor + heightAnchor / 2)
* views. This is done to prevent other views from being drawn in front of the tooltip. Additionally, more than one }
* tooltip cannot be drawn in a frame as they would appear at the same position. }
*/
fun drawTooltip(text: List<Text>) {
if (currentDeferredTooltip != null) {
throw RuntimeException("Deferred tooltip already registered for current frame")
}
currentDeferredTooltip = text
}
/** /**
* Called when a mouse button is clicked and this is the active window. * Called by the window's [screen] when the Minecraft screen is resized.
* This method is called by [CacaoScreen] and generally shouldn't be called directly. * Used to update the window's width and height constraints and re-layout views.
* */
* @param point The point in the window of the click. internal fun resize(width: Int, height: Int) {
* @param mouseButton The mouse button that was used to click. if (widthConstraint != null) solver.removeConstraint(widthConstraint)
* @return Whether the mouse click was handled by a view. if (heightConstraint != null) solver.removeConstraint(heightConstraint)
*/ solver.dsl {
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { widthConstraint = (widthAnchor equalTo width)
// todo: isn't this always true? heightConstraint = (heightAnchor equalTo height)
if (point in viewController.view.frame) { }
val mouseInView = Point(point.x - viewController.view.frame.left, point.y - viewController.view.frame.top) layout()
return viewController.view.mouseClicked(mouseInView, mouseButton) }
} else {
// remove the window from the screen when the mouse clicks outside the window and this is not the primary window
if (screen.windows.size > 1) {
removeFromScreen()
}
}
return false
}
fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean { /**
val currentlyDraggedView = this.currentDragReceiver * Convenience method that removes this window from its [screen].
if (currentlyDraggedView != null) { */
val pointInView = viewController.view.convert(startPoint, to = currentlyDraggedView) fun removeFromScreen() {
return currentlyDraggedView.mouseDragged(pointInView, delta, mouseButton) viewController.viewWillDisappear()
} else if (startPoint in viewController.view.frame) { screen.removeWindow(this)
val startInView = viewController.viewDidDisappear()
Point(startPoint.x - viewController.view.frame.left, startPoint.y - viewController.view.frame.top) }
var prevView: View? = null
var view = viewController.view.subviewsAtPoint(startInView).maxByOrNull(View::zIndex)
while (view != null && !view.respondsToDragging) {
prevView = view
val pointInView = viewController.view.convert(startInView, to = view)
view = view.subviewsAtPoint(pointInView).maxByOrNull(View::zIndex)
}
this.currentDragReceiver = view ?: prevView
return if (this.currentDragReceiver != null) {
val pointInView = viewController.view.convert(startPoint, to = this.currentDragReceiver!!)
this.currentDragReceiver!!.mouseDragged(pointInView, delta, mouseButton)
} else {
false
}
}
return false
}
fun mouseReleased(point: Point, mouseButton: MouseButton): Boolean { /**
val currentlyDraggedView = this.currentDragReceiver * Instructs the solver to solve all of the provided constraints.
if (currentlyDraggedView != null) { * Should be called after the view hierarchy is setup.
val pointInView = viewController.view.convert(point, to = currentlyDraggedView) */
currentlyDraggedView.mouseDragEnded(pointInView, mouseButton) fun layout() {
this.currentDragReceiver = null viewController.viewWillLayoutSubviews()
return true solver.updateVariables()
} viewController.viewDidLayoutSubviews()
return false }
}
fun mouseScrolled(point: Point, amount: Double): Boolean { /**
return viewController.view.mouseScrolled(point, amount) * Draws this window and all of its views.
} * This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param mouse The point in the coordinate system of the window.
* @param delta The time elapsed since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
currentDeferredTooltip = null
val mouseInView = Point(mouse.x - viewController.view.frame.left, mouse.y - viewController.view.frame.top)
viewController.view.draw(matrixStack, mouseInView, delta)
if (currentDeferredTooltip != null) {
RenderHelper.drawTooltip(matrixStack, currentDeferredTooltip!!, mouse)
}
}
/**
* Draw a tooltip containing the given lines at the mouse pointer location.
*
* Implementation note: the tooltip is not drawn immediately, it is done after the window is done drawing all of its
* views. This is done to prevent other views from being drawn in front of the tooltip. Additionally, more than one
* tooltip cannot be drawn in a frame as they would appear at the same position.
*/
fun drawTooltip(text: List<Text>) {
if (currentDeferredTooltip != null) {
throw RuntimeException("Deferred tooltip already registered for current frame")
}
currentDeferredTooltip = text
}
/**
* Called when a mouse button is clicked and this is the active window.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param point The point in the window of the click.
* @param mouseButton The mouse button that was used to click.
* @return Whether the mouse click was handled by a view.
*/
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
// todo: isn't this always true?
if (point in viewController.view.frame) {
val mouseInView = Point(point.x - viewController.view.frame.left, point.y - viewController.view.frame.top)
return viewController.view.mouseClicked(mouseInView, mouseButton)
} else {
// remove the window from the screen when the mouse clicks outside the window and this is not the primary window
if (screen.windows.size > 1) {
removeFromScreen()
}
}
return false
}
fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
val pointInView = viewController.view.convert(startPoint, to = currentlyDraggedView)
return currentlyDraggedView.mouseDragged(pointInView, delta, mouseButton)
} else if (startPoint in viewController.view.frame) {
val startInView =
Point(startPoint.x - viewController.view.frame.left, startPoint.y - viewController.view.frame.top)
var prevView: View? = null
var view = viewController.view.subviewsAtPoint(startInView).maxByOrNull(View::zIndex)
while (view != null && !view.respondsToDragging) {
prevView = view
val pointInView = viewController.view.convert(startInView, to = view)
view = view.subviewsAtPoint(pointInView).maxByOrNull(View::zIndex)
}
this.currentDragReceiver = view ?: prevView
return if (this.currentDragReceiver != null) {
val pointInView = viewController.view.convert(startPoint, to = this.currentDragReceiver!!)
this.currentDragReceiver!!.mouseDragged(pointInView, delta, mouseButton)
} else {
false
}
}
return false
}
fun mouseReleased(point: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
val pointInView = viewController.view.convert(point, to = currentlyDraggedView)
currentlyDraggedView.mouseDragEnded(pointInView, mouseButton)
this.currentDragReceiver = null
return true
}
return false
}
fun mouseScrolled(point: Point, amount: Double): Boolean {
return viewController.view.mouseScrolled(point, amount)
}
} }

View File

@ -6,98 +6,104 @@ import no.birkett.kiwi.*
* @author shadowfacts * @author shadowfacts
*/ */
class KiwiContext(val solver: Solver) { class KiwiContext(val solver: Solver) {
val REQUIRED = Strength.REQUIRED val REQUIRED = Strength.REQUIRED
val STRONG = Strength.STRONG val STRONG = Strength.STRONG
val MEDIUM = Strength.MEDIUM val MEDIUM = Strength.MEDIUM
val WEAK = Strength.WEAK val WEAK = Strength.WEAK
// Constraints // Constraints
infix fun ExpressionConvertible.equalTo(other: ExpressionConvertible): Constraint { infix fun ExpressionConvertible.equalTo(other: ExpressionConvertible): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).apply(solver::addConstraint) return Symbolics.equals(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
} }
fun ExpressionConvertible.equalTo(other: ExpressionConvertible, strength: Double): Constraint { fun ExpressionConvertible.equalTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint) return Symbolics.equals(this.toExpression(), other.toExpression()).setStrength(strength)
} .apply(solver::addConstraint)
}
infix fun ExpressionConvertible.equalTo(constant: Number): Constraint { infix fun ExpressionConvertible.equalTo(constant: Number): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).apply(solver::addConstraint) return Symbolics.equals(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
} }
fun ExpressionConvertible.equalTo(constant: Number, strength: Double): Constraint { fun ExpressionConvertible.equalTo(constant: Number, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint) return Symbolics.equals(this.toExpression(), constant.toDouble()).setStrength(strength)
} .apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible): Constraint { infix fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint) return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
} }
fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint { fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint) return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength)
} .apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(constant: Number): Constraint { infix fun ExpressionConvertible.lessThanOrEqualTo(constant: Number): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint) return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
} }
fun ExpressionConvertible.lessThanOrEqualTo(constant: Number, strength: Double): Constraint { fun ExpressionConvertible.lessThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint) return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength)
} .apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible): Constraint { infix fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint) return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
} }
fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint { fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint) return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength)
} .apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number): Constraint { infix fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint) return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
} }
fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number, strength: Double): Constraint { fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint) return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength)
} .apply(solver::addConstraint)
}
// Addition // Addition
operator fun ExpressionConvertible.plus(other: ExpressionConvertible): Expression { operator fun ExpressionConvertible.plus(other: ExpressionConvertible): Expression {
return Symbolics.add(this.toExpression(), other.toExpression()) return Symbolics.add(this.toExpression(), other.toExpression())
} }
operator fun ExpressionConvertible.plus(constant: Number): Expression { operator fun ExpressionConvertible.plus(constant: Number): Expression {
return Symbolics.add(this.toExpression(), constant.toDouble()) return Symbolics.add(this.toExpression(), constant.toDouble())
} }
// Subtraction // Subtraction
operator fun ExpressionConvertible.minus(other: ExpressionConvertible): Expression { operator fun ExpressionConvertible.minus(other: ExpressionConvertible): Expression {
return Symbolics.subtract(this.toExpression(), other.toExpression()) return Symbolics.subtract(this.toExpression(), other.toExpression())
} }
operator fun ExpressionConvertible.minus(constant: Number): Expression { operator fun ExpressionConvertible.minus(constant: Number): Expression {
return Symbolics.subtract(this.toExpression(), constant.toDouble()) return Symbolics.subtract(this.toExpression(), constant.toDouble())
} }
// Multiplication // Multiplication
operator fun ExpressionConvertible.times(other: ExpressionConvertible): Expression { operator fun ExpressionConvertible.times(other: ExpressionConvertible): Expression {
return Symbolics.multiply(this.toExpression(), other.toExpression()) return Symbolics.multiply(this.toExpression(), other.toExpression())
} }
operator fun ExpressionConvertible.times(constant: Number): Expression { operator fun ExpressionConvertible.times(constant: Number): Expression {
return Symbolics.multiply(this.toExpression(), constant.toDouble()) return Symbolics.multiply(this.toExpression(), constant.toDouble())
} }
// Division // Division
operator fun ExpressionConvertible.div(other: ExpressionConvertible): Expression { operator fun ExpressionConvertible.div(other: ExpressionConvertible): Expression {
return Symbolics.divide(this.toExpression(), other.toExpression()) return Symbolics.divide(this.toExpression(), other.toExpression())
} }
operator fun ExpressionConvertible.div(constant: Number): Expression { operator fun ExpressionConvertible.div(constant: Number): Expression {
return Symbolics.divide(this.toExpression(), constant.toDouble()) return Symbolics.divide(this.toExpression(), constant.toDouble())
} }
} }
fun Solver.dsl(init: KiwiContext.() -> Unit): Solver { fun Solver.dsl(init: KiwiContext.() -> Unit): Solver {
KiwiContext(this).init() KiwiContext(this).init()
return this return this
} }

View File

@ -9,14 +9,15 @@ import net.shadowfacts.phycon.util.SortMode
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object DefaultPlugin: PhyConPlugin { object DefaultPlugin : PhyConPlugin {
lateinit var SORT_MODE: TerminalSettingKey<SortMode> lateinit var SORT_MODE: TerminalSettingKey<SortMode>
private set private set
override fun initializePhyCon(api: PhyConAPI) { override fun initializePhyCon(api: PhyConAPI) {
SORT_MODE = api.registerTerminalSetting(Identifier(PhysicalConnectivity.MODID, "sort"), SortMode.COUNT_HIGH_FIRST) SORT_MODE =
SORT_MODE.setPriority(Int.MAX_VALUE) api.registerTerminalSetting(Identifier(PhysicalConnectivity.MODID, "sort"), SortMode.COUNT_HIGH_FIRST)
} SORT_MODE.setPriority(Int.MAX_VALUE)
}
} }

View File

@ -9,10 +9,13 @@ import net.shadowfacts.phycon.util.TerminalSettings
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhyConAPIImpl: PhyConAPI { object PhyConAPIImpl : PhyConAPI {
override fun <E> registerTerminalSetting(id: Identifier, defaultValue: E): TerminalSettingKey<E> where E: Enum<E>, E: TerminalSetting? { override fun <E> registerTerminalSetting(
return TerminalSettings.register(id, defaultValue) id: Identifier,
} defaultValue: E
): TerminalSettingKey<E> where E : Enum<E>, E : TerminalSetting? {
return TerminalSettings.register(id, defaultValue)
}
} }

View File

@ -16,32 +16,35 @@ import org.apache.logging.log4j.LogManager
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhysicalConnectivity: ModInitializer { object PhysicalConnectivity : ModInitializer {
val MODID = "phycon" val MODID = "phycon"
val NETWORK_LOGGER = LogManager.getLogger("PhyNet") val NETWORK_LOGGER = LogManager.getLogger("PhyNet")
override fun onInitialize() { override fun onInitialize() {
PhyBlocks.init() PhyBlocks.init()
PhyBlockEntities.init() PhyBlockEntities.init()
PhyItems.init() PhyItems.init()
PhyScreens.init() PhyScreens.init()
registerGlobalReceiver(C2SConfigureDevice) registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton) registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem) registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems) registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
ItemStorage.SIDED.registerForBlockEntity(P2PReceiverBlockEntity::provideItemStorage, PhyBlockEntities.P2P_RECEIVER) ItemStorage.SIDED.registerForBlockEntity(
P2PReceiverBlockEntity::provideItemStorage,
PhyBlockEntities.P2P_RECEIVER
)
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)
} }
} }
private fun registerGlobalReceiver(receiver: ServerReceiver) { private fun registerGlobalReceiver(receiver: ServerReceiver) {
ServerPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver) ServerPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
} }
} }

View File

@ -7,6 +7,7 @@ import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry
import net.fabricmc.fabric.api.renderer.v1.RendererAccess 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.netswitch.SwitchConsoleScreen
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.block.terminal.CraftingTerminalScreen
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
@ -20,36 +21,37 @@ import net.shadowfacts.phycon.util.TerminalSettings
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhysicalConnectivityClient: ClientModInitializer { object PhysicalConnectivityClient : ClientModInitializer {
val terminalSettings = TerminalSettings() val terminalSettings = TerminalSettings()
var screenMaterial: RenderMaterial? = null var screenMaterial: RenderMaterial? = null
private set private set
override fun onInitializeClient() { override fun onInitializeClient() {
ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyModelProvider) ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyModelProvider)
RendererAccess.INSTANCE.renderer?.also { renderer -> RendererAccess.INSTANCE.renderer?.also { renderer ->
screenMaterial = renderer.materialFinder() screenMaterial = renderer.materialFinder()
.emissive(0, true) .emissive(0, true)
.disableAo(0, true) .disableAo(0, true)
.disableDiffuse(0, true) .disableDiffuse(0, true)
.find() .find()
ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyExtendedModelProvider) ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyExtendedModelProvider)
} }
ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen) ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen)
ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen) 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)
ScreenRegistry.register(PhyScreens.SWITCH_CONSOLE, ::SwitchConsoleScreen)
registerGlobalReceiver(S2CTerminalUpdateDisplayedItems) registerGlobalReceiver(S2CTerminalUpdateDisplayedItems)
} }
private fun registerGlobalReceiver(receiver: ClientReceiver) { private fun registerGlobalReceiver(receiver: ClientReceiver) {
ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver) ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
} }
} }

View File

@ -13,15 +13,15 @@ import net.minecraft.world.World
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class BlockWithEntity<T: BlockEntity>(settings: Settings): Block(settings), BlockEntityProvider { abstract class BlockWithEntity<T : BlockEntity>(settings: Settings) : Block(settings), BlockEntityProvider {
abstract override fun createBlockEntity(pos: BlockPos, state: BlockState): T? abstract override fun createBlockEntity(pos: BlockPos, state: BlockState): T?
fun getBlockEntity(world: BlockView, pos: BlockPos): T? { fun getBlockEntity(world: BlockView, pos: BlockPos): T? {
val entity = world.getBlockEntity(pos) val entity = world.getBlockEntity(pos)
return if (entity != null) { return if (entity != null) {
entity as? T entity as? T
} else { } else {
null null
} }
} }
} }

View File

@ -15,27 +15,41 @@ import net.shadowfacts.phycon.api.NetworkComponentBlock
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class DeviceBlock<T: DeviceBlockEntity>(settings: Settings): BlockWithEntity<T>(settings), NetworkComponentBlock { abstract class DeviceBlock<T : DeviceBlockEntity>(settings: Settings) : BlockWithEntity<T>(settings),
NetworkComponentBlock {
abstract override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> abstract override fun getNetworkConnectedSides(
state: BlockState,
world: WorldAccess,
pos: BlockPos
): Collection<Direction>
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { override fun getNetworkInterfaceForSide(
return getBlockEntity(world, pos)!! side: Direction,
} state: BlockState,
world: WorldAccess,
pos: BlockPos
): Interface? {
return getBlockEntity(world, pos)!!
}
override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) { override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) {
super.onBreak(world, pos, state, player) super.onBreak(world, pos, state, player)
getBlockEntity(world, pos)!!.onBreak() getBlockEntity(world, pos)!!.onBreak()
} }
override fun <T: BlockEntity> getTicker(world: World, state: BlockState, type: BlockEntityType<T>): BlockEntityTicker<T>? { override fun <T : BlockEntity> getTicker(
return if (world.isClient) { world: World,
null state: BlockState,
} else { type: BlockEntityType<T>
BlockEntityTicker { world, blockPos, blockState, blockEntity -> ): BlockEntityTicker<T>? {
(blockEntity as DeviceBlockEntity).tick() return if (world.isClient) {
} null
} } else {
} BlockEntityTicker { world, blockPos, blockState, blockEntity ->
(blockEntity as DeviceBlockEntity).tick()
}
}
}
} }

View File

@ -28,205 +28,232 @@ import java.util.*
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class DeviceBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): BlockEntity(type, pos, state), abstract class DeviceBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState) :
PacketSink, BlockEntity(type, pos, state),
PacketSource, PacketSink,
Interface { PacketSource,
Interface {
companion object { companion object {
private const val ARP_RETRY_TIMEOUT = 200 private const val ARP_RETRY_TIMEOUT = 200
} }
var macAddress: MACAddress = MACAddress.random() var macAddress: MACAddress = MACAddress.random()
protected set protected set
var ipAddress: IPAddress = IPAddress.random() var ipAddress: IPAddress = IPAddress.random()
protected set protected set
private val arpTable = mutableMapOf<IPAddress, MACAddress>() private val arpTable = mutableMapOf<IPAddress, MACAddress>()
private val packetQueue = LinkedList<PendingPacket>() private val packetQueue = LinkedList<PendingPacket>()
private var cachedDestination: WeakReference<Interface>? = null private var cachedDestination: WeakReference<Interface>? = null
var counter: Long = 0 var counter: Long = 0
override fun getIPAddress() = ipAddress override fun getIPAddress() = ipAddress
override fun getMACAddress() = macAddress override fun getMACAddress() = macAddress
abstract override fun handle(packet: Packet) abstract override fun handle(packet: Packet)
private fun doHandlePacket(packet: Packet) { private fun doHandlePacket(packet: Packet) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) received packet: {}", this, ipAddress, packet) PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) received packet: {}", this, ipAddress, packet)
when (packet) { when (packet) {
is DeviceRemovedPacket -> { is DeviceRemovedPacket -> {
arpTable.remove(packet.source) arpTable.remove(packet.source)
} }
is PingPacket -> {
sendPacket(PongPacket(ipAddress, packet.source))
}
}
handle(packet)
}
override fun send(frame: EthernetFrame) { is PingPacket -> {
findDestination()?.receive(frame) sendPacket(PongPacket(ipAddress, packet.source))
} }
}
handle(packet)
}
override fun receive(frame: EthernetFrame) { override fun send(frame: EthernetFrame) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) received frame from {}: {}", this, ipAddress, macAddress, frame.source, frame) findDestination()?.receive(frame)
when (frame) { }
is ARPQueryFrame -> handleARPQuery(frame)
is ARPResponseFrame -> handleARPResponse(frame)
is NetworkSplitFrame -> handleNetworkSplit()
is PacketFrame -> {
if (frame.packet.destination.isBroadcast || frame.packet.destination == ipAddress) {
doHandlePacket(frame.packet)
}
}
}
}
private fun handleARPQuery(frame: ARPQueryFrame) { override fun receive(frame: EthernetFrame) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}), received ARP query for {}", this, ipAddress, frame.queryIP) PhysicalConnectivity.NETWORK_LOGGER.debug(
arpTable[frame.sourceIP] = frame.source "{} ({}, {}) received frame from {}: {}",
if (frame.queryIP == ipAddress) { this,
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) sending ARP response to {} with {}", this, ipAddress, frame.sourceIP, macAddress) ipAddress,
send(ARPResponseFrame(ipAddress, macAddress, frame.source)) macAddress,
} frame.source,
} frame
)
when (frame) {
is ARPQueryFrame -> handleARPQuery(frame)
is ARPResponseFrame -> handleARPResponse(frame)
is NetworkSplitFrame -> handleNetworkSplit()
is PacketFrame -> {
if (frame.packet.destination.isBroadcast || frame.packet.destination == ipAddress) {
doHandlePacket(frame.packet)
}
}
}
}
private fun handleARPResponse(frame: ARPResponseFrame) { private fun handleARPQuery(frame: ARPQueryFrame) {
arpTable[frame.query] = frame.source PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}), received ARP query for {}", this, ipAddress, frame.queryIP)
PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}) received ARP response for {} with {}", this, ipAddress, frame.query, frame.source) arpTable[frame.sourceIP] = frame.source
if (frame.queryIP == ipAddress) {
PhysicalConnectivity.NETWORK_LOGGER.debug(
"{} ({}) sending ARP response to {} with {}",
this,
ipAddress,
frame.sourceIP,
macAddress
)
send(ARPResponseFrame(ipAddress, macAddress, frame.source))
}
}
val toRemove = packetQueue.filter { (packet, _) -> private fun handleARPResponse(frame: ARPResponseFrame) {
if (packet.destination == frame.query) { arpTable[frame.query] = frame.source
send(BasePacketFrame(packet, macAddress, frame.source)) PhysicalConnectivity.NETWORK_LOGGER.debug(
true "{}, ({}) received ARP response for {} with {}",
} else { this,
false ipAddress,
} frame.query,
} frame.source
packetQueue.removeAll(toRemove) )
}
protected open fun handleNetworkSplit() { val toRemove = packetQueue.filter { (packet, _) ->
arpTable.clear() if (packet.destination == frame.query) {
cachedDestination = null send(BasePacketFrame(packet, macAddress, frame.source))
} true
} else {
false
}
}
packetQueue.removeAll(toRemove)
}
override fun sendPacket(packet: Packet) { protected open fun handleNetworkSplit() {
if (packet.destination.isBroadcast) { arpTable.clear()
send(BasePacketFrame(packet, macAddress, MACAddress.BROADCAST)) cachedDestination = null
} else if (arpTable.containsKey(packet.destination)) { }
send(BasePacketFrame(packet, macAddress, arpTable[packet.destination]!!))
} else {
packetQueue.add(PendingPacket(packet, counter))
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) sending ARP query for {}", this, ipAddress, packet.destination) override fun sendPacket(packet: Packet) {
send(ARPQueryFrame(packet.destination, ipAddress, macAddress)) if (packet.destination.isBroadcast) {
} send(BasePacketFrame(packet, macAddress, MACAddress.BROADCAST))
} } else if (arpTable.containsKey(packet.destination)) {
send(BasePacketFrame(packet, macAddress, arpTable[packet.destination]!!))
} else {
packetQueue.add(PendingPacket(packet, counter))
open fun findDestination(): Interface? { PhysicalConnectivity.NETWORK_LOGGER.debug(
val cachedDestination = this.cachedDestination?.get() "{} ({}) sending ARP query for {}",
if (cachedDestination != null) { this,
return cachedDestination ipAddress,
} packet.destination
)
send(ARPQueryFrame(packet.destination, ipAddress, macAddress))
}
}
val sides = (cachedState.block as NetworkComponentBlock).getNetworkConnectedSides(cachedState, world!!, pos) open fun findDestination(): Interface? {
return when (sides.size) { val cachedDestination = this.cachedDestination?.get()
0 -> null if (cachedDestination != null) {
1 -> { return cachedDestination
NetworkUtil.findConnectedInterface(world!!, pos, sides.first())?.also { }
this.cachedDestination = WeakReference(it)
}
}
else -> throw RuntimeException("DeviceBlockEntity.findDestination must be overridden by devices which have more than 1 network connected side")
}
}
override fun cableDisconnected() { val sides = (cachedState.block as NetworkComponentBlock).getNetworkConnectedSides(cachedState, world!!, pos)
cachedDestination = null return when (sides.size) {
handleNetworkSplit() 0 -> null
} 1 -> {
NetworkUtil.findConnectedInterface(world!!, pos, sides.first())?.also {
this.cachedDestination = WeakReference(it)
}
}
open fun tick() { else -> throw RuntimeException("DeviceBlockEntity.findDestination must be overridden by devices which have more than 1 network connected side")
counter++ }
}
if (!world!!.isClient) { override fun cableDisconnected() {
val toRemove = packetQueue.filter { entry -> cachedDestination = null
val (packet, timestamp) = entry handleNetworkSplit()
if (arpTable.containsKey(packet.destination)) { }
send(BasePacketFrame(packet, macAddress, arpTable[packet.destination]!!))
true
} else if (counter - timestamp >= ARP_RETRY_TIMEOUT) {
send(ARPQueryFrame(packet.destination, ipAddress, macAddress))
entry.timestamp = counter
// todo: should there be a retry counter?
true
} else {
false
}
}
packetQueue.removeAll(toRemove)
}
}
protected open fun toCommonTag(tag: NbtCompound) { open fun tick() {
tag.putInt("IPAddress", ipAddress.address) counter++
tag.putLong("MACAddress", macAddress.address)
}
protected open fun fromCommonTag(tag: NbtCompound) { if (!world!!.isClient) {
ipAddress = IPAddress(tag.getInt("IPAddress")) val toRemove = packetQueue.filter { entry ->
macAddress = MACAddress(tag.getLong("MACAddress")) val (packet, timestamp) = entry
} if (arpTable.containsKey(packet.destination)) {
send(BasePacketFrame(packet, macAddress, arpTable[packet.destination]!!))
true
} else if (counter - timestamp >= ARP_RETRY_TIMEOUT) {
send(ARPQueryFrame(packet.destination, ipAddress, macAddress))
entry.timestamp = counter
// todo: should there be a retry counter?
true
} else {
false
}
}
packetQueue.removeAll(toRemove)
}
}
override fun writeNbt(tag: NbtCompound) { protected open fun toCommonTag(tag: NbtCompound) {
super.writeNbt(tag) tag.putInt("IPAddress", ipAddress.address)
toCommonTag(tag) tag.putLong("MACAddress", macAddress.address)
} }
override fun readNbt(tag: NbtCompound) { protected open fun fromCommonTag(tag: NbtCompound) {
super.readNbt(tag) ipAddress = IPAddress(tag.getInt("IPAddress"))
fromCommonTag(tag) macAddress = MACAddress(tag.getLong("MACAddress"))
if (tag.getBoolean("_SyncPacket")) { }
fromClientTag(tag)
}
}
override fun toUpdatePacket(): BlockEntityUpdateS2CPacket { override fun writeNbt(tag: NbtCompound) {
return BlockEntityUpdateS2CPacket.create(this) super.writeNbt(tag)
} toCommonTag(tag)
}
override fun toInitialChunkDataNbt(): NbtCompound { override fun readNbt(tag: NbtCompound) {
val tag = NbtCompound() super.readNbt(tag)
tag.putBoolean("_SyncPacket", true) fromCommonTag(tag)
return toClientTag(tag) if (tag.getBoolean("_SyncPacket")) {
} fromClientTag(tag)
}
}
open fun toClientTag(tag: NbtCompound): NbtCompound { override fun toUpdatePacket(): BlockEntityUpdateS2CPacket {
toCommonTag(tag) return BlockEntityUpdateS2CPacket.create(this)
return tag }
}
open fun fromClientTag(tag: NbtCompound) { override fun toInitialChunkDataNbt(): NbtCompound {
} val tag = NbtCompound()
tag.putBoolean("_SyncPacket", true)
return toClientTag(tag)
}
fun markUpdate() { open fun toClientTag(tag: NbtCompound): NbtCompound {
markDirty() toCommonTag(tag)
world!!.updateListeners(pos, cachedState, cachedState, 3) return tag
} }
fun onBreak() { open fun fromClientTag(tag: NbtCompound) {
if (!world!!.isClient) { }
sendPacket(DeviceRemovedPacket(this))
}
}
data class PendingPacket( fun markUpdate() {
val packet: Packet, markDirty()
var timestamp: Long, world!!.updateListeners(pos, cachedState, cachedState, 3)
) }
fun onBreak() {
if (!world!!.isClient) {
sendPacket(DeviceRemovedPacket(this))
}
}
data class PendingPacket(
val packet: Packet,
var timestamp: Long,
)
} }

View File

@ -27,146 +27,181 @@ import java.util.*
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class FaceDeviceBlock<T: DeviceBlockEntity>(settings: Settings): DeviceBlock<T>(settings) { abstract class FaceDeviceBlock<T : DeviceBlockEntity>(settings: Settings) : DeviceBlock<T>(settings) {
companion object { companion object {
val FACING = Properties.FACING val FACING = Properties.FACING
val CABLE_CONNECTION = EnumProperty.of("cable_connection", FaceCableConnection::class.java) val CABLE_CONNECTION = EnumProperty.of("cable_connection", FaceCableConnection::class.java)
val COLOR = EnumProperty.of("color", DyeColor::class.java) val COLOR = EnumProperty.of("color", DyeColor::class.java)
} }
enum class FaceCableConnection : StringIdentifiable { enum class FaceCableConnection : StringIdentifiable {
NONE, DOWN, UP, NORTH, SOUTH, WEST, EAST; NONE, DOWN, UP, NORTH, SOUTH, WEST, EAST;
companion object { companion object {
fun from(dir: Direction?) = when (dir) { fun from(dir: Direction?) = when (dir) {
null -> NONE null -> NONE
Direction.DOWN -> DOWN Direction.DOWN -> DOWN
Direction.UP -> UP Direction.UP -> UP
Direction.NORTH -> NORTH Direction.NORTH -> NORTH
Direction.SOUTH -> SOUTH Direction.SOUTH -> SOUTH
Direction.WEST -> WEST Direction.WEST -> WEST
Direction.EAST -> EAST Direction.EAST -> EAST
} }
} }
val direction: Direction? val direction: Direction?
get() = when (this) { get() = when (this) {
NONE -> null NONE -> null
DOWN -> Direction.DOWN DOWN -> Direction.DOWN
UP -> Direction.UP UP -> Direction.UP
NORTH -> Direction.NORTH NORTH -> Direction.NORTH
SOUTH -> Direction.SOUTH SOUTH -> Direction.SOUTH
WEST -> Direction.WEST WEST -> Direction.WEST
EAST -> Direction.EAST EAST -> Direction.EAST
} }
override fun asString() = name.toLowerCase() override fun asString() = name.toLowerCase()
} }
protected abstract val faceThickness: Double protected abstract val faceThickness: Double
abstract val faceShapes: Map<Direction, VoxelShape> abstract val faceShapes: Map<Direction, VoxelShape>
private val centerShapes: Map<Direction, VoxelShape> by lazy { private val centerShapes: Map<Direction, VoxelShape> by lazy {
mapOf( mapOf(
Direction.DOWN to createCuboidShape(6.0, faceThickness, 6.0, 10.0, 10.0, 10.0), Direction.DOWN to createCuboidShape(6.0, faceThickness, 6.0, 10.0, 10.0, 10.0),
Direction.UP to createCuboidShape(6.0, 6.0, 6.0, 10.0, 16.0 - faceThickness, 10.0), Direction.UP to createCuboidShape(6.0, 6.0, 6.0, 10.0, 16.0 - faceThickness, 10.0),
Direction.NORTH to createCuboidShape(6.0, 6.0, faceThickness, 10.0, 10.0, 10.0), Direction.NORTH to createCuboidShape(6.0, 6.0, faceThickness, 10.0, 10.0, 10.0),
Direction.SOUTH to createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 16.0 - faceThickness), Direction.SOUTH to createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 16.0 - faceThickness),
Direction.WEST to createCuboidShape(faceThickness, 6.0, 6.0, 10.0, 10.0, 10.0), Direction.WEST to createCuboidShape(faceThickness, 6.0, 6.0, 10.0, 10.0, 10.0),
Direction.EAST to createCuboidShape(6.0, 6.0, 6.0, 16.0 - faceThickness, 10.0, 10.0) Direction.EAST to createCuboidShape(6.0, 6.0, 6.0, 16.0 - faceThickness, 10.0, 10.0)
) )
} }
private val shapeCache = mutableMapOf<Pair<Direction, FaceCableConnection>, VoxelShape>() private val shapeCache = mutableMapOf<Pair<Direction, FaceCableConnection>, VoxelShape>()
private fun getShape(facing: Direction, cableConnection: FaceCableConnection): VoxelShape { private fun getShape(facing: Direction, cableConnection: FaceCableConnection): VoxelShape {
return shapeCache.getOrPut(facing to cableConnection) { return shapeCache.getOrPut(facing to cableConnection) {
if (cableConnection == FaceCableConnection.NONE) { if (cableConnection == FaceCableConnection.NONE) {
VoxelShapes.union(faceShapes[facing], centerShapes[facing]) VoxelShapes.union(faceShapes[facing], centerShapes[facing])
} else { } else {
VoxelShapes.union(faceShapes[facing], centerShapes[facing], CableBlock.SIDE_SHAPES[cableConnection.direction]) VoxelShapes.union(
} faceShapes[facing],
} centerShapes[facing],
} CableBlock.SIDE_SHAPES[cableConnection.direction]
)
}
}
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> { override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val direction = state[CABLE_CONNECTION].direction val direction = state[CABLE_CONNECTION].direction
return if (direction != null) EnumSet.of(direction) else setOf() return if (direction != null) EnumSet.of(direction) else setOf()
} }
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { override fun getNetworkInterfaceForSide(
return if (side == state[FACING]) { side: Direction,
null state: BlockState,
} else { world: WorldAccess,
getBlockEntity(world, pos) pos: BlockPos
} ): Interface? {
} return if (side == state[FACING]) {
null
} else {
getBlockEntity(world, pos)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) { override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder) super.appendProperties(builder)
builder.add(FACING) builder.add(FACING)
builder.add(CABLE_CONNECTION) builder.add(CABLE_CONNECTION)
builder.add(COLOR) builder.add(COLOR)
} }
override fun getPlacementState(context: ItemPlacementContext): BlockState { override fun getPlacementState(context: ItemPlacementContext): BlockState {
val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite val facing =
// todo: this should never be called if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite
val cableConnection = FaceCableConnection.from(getCableConnectedSide(context.world, context.blockPos, facing, DyeColor.BLUE)) // todo: this should never be called
return defaultState val cableConnection =
.with(FACING, facing) FaceCableConnection.from(getCableConnectedSide(context.world, context.blockPos, facing, DyeColor.BLUE))
.with(CABLE_CONNECTION, cableConnection) return defaultState
} .with(FACING, facing)
.with(CABLE_CONNECTION, cableConnection)
}
private fun getCableConnectedSide(world: WorldAccess, pos: BlockPos, facing: Direction, color: DyeColor): Direction? { private fun getCableConnectedSide(
for (side in Direction.values()) { world: WorldAccess,
if (side == facing) { pos: BlockPos,
continue facing: Direction,
} color: DyeColor
val offsetPos = pos.offset(side) ): Direction? {
val state = world.getBlockState(offsetPos) for (side in Direction.values()) {
if (canConnectTo(world, side, state, offsetPos, color)) { if (side == facing) {
return side continue
} }
} val offsetPos = pos.offset(side)
return null val state = world.getBlockState(offsetPos)
} if (canConnectTo(world, side, state, offsetPos, color)) {
return side
}
}
return null
}
private fun canConnectTo(world: WorldAccess, side: Direction, candidateState: BlockState, candidatePos: BlockPos, myColor: DyeColor): Boolean { private fun canConnectTo(
val block = candidateState.block world: WorldAccess,
return if (block is FaceDeviceBlock<*> && candidateState[COLOR] == myColor) { side: Direction,
true candidateState: BlockState,
} else if (block is CableBlock && block.color == myColor) { candidatePos: BlockPos,
true myColor: DyeColor
} else { ): Boolean {
block is NetworkComponentBlock && block.getNetworkConnectedSides(candidateState, world, candidatePos).contains(side.opposite) val block = candidateState.block
} return if (block is FaceDeviceBlock<*> && candidateState[COLOR] == myColor) {
} true
} else if (block is CableBlock && block.color == myColor) {
true
} else {
block is NetworkComponentBlock && block.getNetworkConnectedSides(candidateState, world, candidatePos)
.contains(side.opposite)
}
}
override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, pos: BlockPos, neighborPos: BlockPos): BlockState { override fun getStateForNeighborUpdate(
val current = state[CABLE_CONNECTION] state: BlockState,
var newConnection = current side: Direction,
neighborState: BlockState,
world: WorldAccess,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val current = state[CABLE_CONNECTION]
var newConnection = current
if (current == FaceCableConnection.NONE) { if (current == FaceCableConnection.NONE) {
if (canConnectTo(world, side, neighborState, neighborPos, state[COLOR])) { if (canConnectTo(world, side, neighborState, neighborPos, state[COLOR])) {
newConnection = FaceCableConnection.from(side) newConnection = FaceCableConnection.from(side)
} }
} else { } else {
val currentConnectedPos = pos.offset(current.direction) val currentConnectedPos = pos.offset(current.direction)
if (neighborPos == currentConnectedPos && neighborState.block !is NetworkComponentBlock) { if (neighborPos == currentConnectedPos && neighborState.block !is NetworkComponentBlock) {
// the old cable connection is no longer correct, try to find another // the old cable connection is no longer correct, try to find another
newConnection = FaceCableConnection.from(getCableConnectedSide(world, pos, state[FACING], state[COLOR])) newConnection = FaceCableConnection.from(getCableConnectedSide(world, pos, state[FACING], state[COLOR]))
} }
} }
return state.with(CABLE_CONNECTION, newConnection) return state.with(CABLE_CONNECTION, newConnection)
} }
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape { override fun getOutlineShape(
return getShape(state[FACING], state[CABLE_CONNECTION]) state: BlockState,
} world: BlockView,
pos: BlockPos,
context: ShapeContext
): VoxelShape {
return getShape(state[FACING], state[CABLE_CONNECTION])
}
override fun onStacksDropped(state: BlockState, world: ServerWorld, pos: BlockPos, stack: ItemStack) { override fun onStacksDropped(state: BlockState, world: ServerWorld, pos: BlockPos, stack: ItemStack) {
super.onStacksDropped(state, world, pos, stack) super.onStacksDropped(state, world, pos, stack)
val cableStack = ItemStack(PhyItems.CABLES[state[COLOR]]) val cableStack = ItemStack(PhyItems.CABLES[state[COLOR]])
dropStack(world, pos, cableStack) dropStack(world, pos, cableStack)
} }
} }

View File

@ -37,180 +37,209 @@ import java.util.*
* @author shadowfacts * @author shadowfacts
*/ */
class CableBlock( class CableBlock(
val color: DyeColor, val color: DyeColor,
): Block( ) : Block(
FabricBlockSettings.of(CABLE_MATERIAL) FabricBlockSettings.of(CABLE_MATERIAL)
.strength(0.3f) .strength(0.3f)
.nonOpaque() .nonOpaque()
), NetworkCableBlock { ), NetworkCableBlock {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "cable") val ID = Identifier(PhysicalConnectivity.MODID, "cable")
val CABLE_MATERIAL = Material.Builder(MapColor.BLUE).build() val CABLE_MATERIAL = Material.Builder(MapColor.BLUE).build()
val CENTER_SHAPE = createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 10.0) val CENTER_SHAPE = createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 10.0)
val SIDE_SHAPES = mapOf<Direction, VoxelShape>( val SIDE_SHAPES = mapOf<Direction, VoxelShape>(
Direction.DOWN to createCuboidShape(6.0, 0.0, 6.0, 10.0, 6.0, 10.0), Direction.DOWN to createCuboidShape(6.0, 0.0, 6.0, 10.0, 6.0, 10.0),
Direction.UP to createCuboidShape(6.0, 10.0, 6.0, 10.0, 16.0, 10.0), Direction.UP to createCuboidShape(6.0, 10.0, 6.0, 10.0, 16.0, 10.0),
Direction.NORTH to createCuboidShape(6.0, 6.0, 0.0, 10.0, 10.0, 6.0), Direction.NORTH to createCuboidShape(6.0, 6.0, 0.0, 10.0, 10.0, 6.0),
Direction.SOUTH to createCuboidShape(6.0, 6.0, 10.0, 10.0, 10.0, 16.0), Direction.SOUTH to createCuboidShape(6.0, 6.0, 10.0, 10.0, 10.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 6.0, 6.0, 6.0, 10.0, 10.0), Direction.WEST to createCuboidShape(0.0, 6.0, 6.0, 6.0, 10.0, 10.0),
Direction.EAST to createCuboidShape(10.0, 6.0, 6.0, 16.0, 10.0, 10.0) Direction.EAST to createCuboidShape(10.0, 6.0, 6.0, 16.0, 10.0, 10.0)
) )
private val SHAPE_CACHE = Array<VoxelShape>(64) { key -> private val SHAPE_CACHE = Array<VoxelShape>(64) { key ->
val connectedSides = Direction.values().filterIndexed { index, _ -> val connectedSides = Direction.values().filterIndexed { index, _ ->
((key shr index) and 1) == 1 ((key shr index) and 1) == 1
} }
connectedSides.fold(CENTER_SHAPE) { acc, side -> connectedSides.fold(CENTER_SHAPE) { acc, side ->
VoxelShapes.union(acc, SIDE_SHAPES[side]) VoxelShapes.union(acc, SIDE_SHAPES[side])
} }
} }
val CONNECTIONS: Map<Direction, EnumProperty<CableConnection>> = Direction.values().associate { it to EnumProperty.of(it.name.toLowerCase(), CableConnection::class.java) } val CONNECTIONS: Map<Direction, EnumProperty<CableConnection>> =
Direction.values().associate { it to EnumProperty.of(it.name.toLowerCase(), CableConnection::class.java) }
fun getShape(state: BlockState): VoxelShape { fun getShape(state: BlockState): VoxelShape {
val key = Direction.values().foldIndexed(0) { i, acc, dir -> val key = Direction.values().foldIndexed(0) { i, acc, dir ->
if (state[CONNECTIONS[dir]] == CableConnection.ON) { if (state[CONNECTIONS[dir]] == CableConnection.ON) {
acc or (1 shl i) acc or (1 shl i)
} else { } else {
acc acc
} }
} }
return SHAPE_CACHE[key] return SHAPE_CACHE[key]
} }
} }
init { init {
defaultState = CONNECTIONS.values.fold(stateManager.defaultState) { acc, prop -> defaultState = CONNECTIONS.values.fold(stateManager.defaultState) { acc, prop ->
acc.with(prop, CableConnection.OFF) acc.with(prop, CableConnection.OFF)
} }
} }
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> { override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.noneOf(Direction::class.java) val set = EnumSet.noneOf(Direction::class.java)
for ((side, prop) in CONNECTIONS) { for ((side, prop) in CONNECTIONS) {
if (state[prop] == CableConnection.ON) { if (state[prop] == CableConnection.ON) {
set.add(side) set.add(side)
} }
} }
return set return set
} }
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) { override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder) super.appendProperties(builder)
CONNECTIONS.values.forEach { CONNECTIONS.values.forEach {
builder.add(it) builder.add(it)
} }
} }
fun getInitialState(world: World, pos: BlockPos): BlockState { fun getInitialState(world: World, pos: BlockPos): BlockState {
return CONNECTIONS.entries.fold(defaultState, { acc, (dir, prop) -> return CONNECTIONS.entries.fold(defaultState, { acc, (dir, prop) ->
acc.with(prop, getConnectionStateInDirection(world, pos, dir)) acc.with(prop, getConnectionStateInDirection(world, pos, dir))
}) })
} }
override fun getPlacementState(context: ItemPlacementContext): BlockState { override fun getPlacementState(context: ItemPlacementContext): BlockState {
return getInitialState(context.world, context.blockPos) return getInitialState(context.world, context.blockPos)
} }
override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, blockPos_1: BlockPos, blockPos_2: BlockPos): BlockState { override fun getStateForNeighborUpdate(
val prop = CONNECTIONS[side] state: BlockState,
val current = state[prop] side: Direction,
return when (current) { neighborState: BlockState,
CableConnection.DISABLED -> state world: WorldAccess,
else -> state.with(prop, getConnectionStateInDirection(world, blockPos_1, side)) blockPos_1: BlockPos,
} blockPos_2: BlockPos
} ): BlockState {
val prop = CONNECTIONS[side]
val current = state[prop]
return when (current) {
CableConnection.DISABLED -> state
else -> state.with(prop, getConnectionStateInDirection(world, blockPos_1, side))
}
}
private fun getConnectionStateInDirection(world: WorldAccess, pos: BlockPos, direction: Direction): CableConnection { private fun getConnectionStateInDirection(
val offsetPos = pos.offset(direction) world: WorldAccess,
val state = world.getBlockState(offsetPos) pos: BlockPos,
val block = state.block direction: Direction
return if (block == this) { ): CableConnection {
val prop = CONNECTIONS[direction.opposite] val offsetPos = pos.offset(direction)
when (state[prop]) { val state = world.getBlockState(offsetPos)
CableConnection.DISABLED -> CableConnection.DISABLED val block = state.block
else -> CableConnection.ON return if (block == this) {
} val prop = CONNECTIONS[direction.opposite]
} else if (block is NetworkComponentBlock && block !is CableBlock) { when (state[prop]) {
if (block.getNetworkConnectedSides(state, world, offsetPos).contains(direction.opposite)) { CableConnection.DISABLED -> CableConnection.DISABLED
CableConnection.ON else -> CableConnection.ON
} else { }
CableConnection.OFF } else if (block is NetworkComponentBlock && block !is CableBlock) {
} if (block.getNetworkConnectedSides(state, world, offsetPos).contains(direction.opposite)) {
} else { CableConnection.ON
CableConnection.OFF } else {
} CableConnection.OFF
} }
} else {
CableConnection.OFF
}
}
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { override fun getNetworkInterfaceForSide(
// cables don't have network interfaces side: Direction,
return null state: BlockState,
} world: WorldAccess,
pos: BlockPos
): Interface? {
// cables don't have network interfaces
return null
}
override fun onUse( override fun onUse(
state: BlockState, state: BlockState,
world: World, world: World,
pos: BlockPos, pos: BlockPos,
player: PlayerEntity, player: PlayerEntity,
hand: Hand, hand: Hand,
hitResult: BlockHitResult hitResult: BlockHitResult
): ActionResult { ): ActionResult {
if (player.getStackInHand(hand).item == PhyItems.SCREWDRIVER) { if (player.getStackInHand(hand).item == PhyItems.SCREWDRIVER) {
val hitPos = Vec3d(hitResult.pos.x - pos.x, hitResult.pos.y - pos.y, hitResult.pos.z - pos.z) val hitPos = Vec3d(hitResult.pos.x - pos.x, hitResult.pos.y - pos.y, hitResult.pos.z - pos.z)
val hitConnection = SIDE_SHAPES.entries.firstOrNull { (_, shape) -> val hitConnection = SIDE_SHAPES.entries.firstOrNull { (_, shape) ->
shape.boundingBox.containsInclusive(hitPos) shape.boundingBox.containsInclusive(hitPos)
} }
if (hitConnection != null) { if (hitConnection != null) {
val side = hitConnection.key val side = hitConnection.key
val prop = CONNECTIONS[side] val prop = CONNECTIONS[side]
val newState = when (state[prop]) { val newState = when (state[prop]) {
CableConnection.DISABLED -> { CableConnection.DISABLED -> {
// if the block this cable is connecting to on the side that will be re-enabled is a cable that // if the block this cable is connecting to on the side that will be re-enabled is a cable that
// is disabled on the side that connects it to us, we also re-enable that cable to make sure both // is disabled on the side that connects it to us, we also re-enable that cable to make sure both
// "halves" of the connection are in the same state // "halves" of the connection are in the same state
val connectedToPos = pos.offset(side) val connectedToPos = pos.offset(side)
val connectedTo = world.getBlockState(connectedToPos) val connectedTo = world.getBlockState(connectedToPos)
if (connectedTo.block == this && connectedTo[CONNECTIONS[side.opposite]] == CableConnection.DISABLED) { if (connectedTo.block == this && connectedTo[CONNECTIONS[side.opposite]] == CableConnection.DISABLED) {
world.setBlockState(connectedToPos, connectedTo.with(CONNECTIONS[side.opposite], CableConnection.ON)) world.setBlockState(
} connectedToPos,
connectedTo.with(CONNECTIONS[side.opposite], CableConnection.ON)
)
}
state.with(prop, if (connectedTo.block is NetworkComponentBlock) CableConnection.ON else CableConnection.OFF) state.with(
} prop,
else -> state.with(prop, CableConnection.DISABLED) if (connectedTo.block is NetworkComponentBlock) CableConnection.ON else CableConnection.OFF
} )
world.setBlockState(pos, newState) }
}
return ActionResult.SUCCESS
}
return ActionResult.PASS
}
override fun canReplace(state: BlockState, context: ItemPlacementContext): Boolean { else -> state.with(prop, CableConnection.DISABLED)
return context.stack.item is FaceDeviceBlockItem }
} world.setBlockState(pos, newState)
}
return ActionResult.SUCCESS
}
return ActionResult.PASS
}
override fun isTranslucent(blockState_1: BlockState?, blockView_1: BlockView?, blockPos_1: BlockPos?): Boolean { override fun canReplace(state: BlockState, context: ItemPlacementContext): Boolean {
return true return context.stack.item is FaceDeviceBlockItem
} }
override fun isTranslucent(blockState_1: BlockState?, blockView_1: BlockView?, blockPos_1: BlockPos?): Boolean {
return true
}
// override fun isSimpleFullBlock(blockState_1: BlockState?, blockView_1: BlockView?, blockPos_1: BlockPos?): Boolean { // override fun isSimpleFullBlock(blockState_1: BlockState?, blockView_1: BlockView?, blockPos_1: BlockPos?): Boolean {
// return false // return false
// } // }
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape { override fun getOutlineShape(
return getShape(state) state: BlockState,
} world: BlockView,
pos: BlockPos,
context: ShapeContext
): VoxelShape {
return getShape(state)
}
override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) { override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) {
super.onBreak(world, pos, state, player) super.onBreak(world, pos, state, player)
if (!world.isClient) { if (!world.isClient) {
world.server?.execute { world.server?.execute {
// notify devices on either end that the connection was broken (i.e., unset the cached receivers) // notify devices on either end that the connection was broken (i.e., unset the cached receivers)
val connectedSides = getNetworkConnectedSides(state, world, pos) val connectedSides = getNetworkConnectedSides(state, world, pos)
for (side in connectedSides) { for (side in connectedSides) {
val dest = NetworkUtil.findConnectedInterface(world, pos, side) val dest = NetworkUtil.findConnectedInterface(world, pos, side)
dest?.cableDisconnected() dest?.cableDisconnected()
} }
} }
} }
} }
} }

View File

@ -21,66 +21,61 @@ import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class ExtractorBlock: FaceDeviceBlock<ExtractorBlockEntity>( class ExtractorBlock : FaceDeviceBlock<ExtractorBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "extractor") val ID = Identifier(PhysicalConnectivity.MODID, "extractor")
private val EXTRACTOR_SHAPES = mutableMapOf<Direction, VoxelShape>() private val EXTRACTOR_SHAPES = mutableMapOf<Direction, VoxelShape>()
init { init {
val components = arrayOf( val components = arrayOf(
doubleArrayOf(0.0, 0.0, 0.0, 16.0, 2.0, 16.0), doubleArrayOf(0.0, 0.0, 0.0, 16.0, 2.0, 16.0),
doubleArrayOf(2.0, 2.0, 2.0, 14.0, 4.0, 14.0), doubleArrayOf(2.0, 2.0, 2.0, 14.0, 4.0, 14.0),
doubleArrayOf(4.0, 4.0, 4.0, 12.0, 6.0, 12.0), doubleArrayOf(4.0, 4.0, 4.0, 12.0, 6.0, 12.0),
) )
val directions = arrayOf( val directions = arrayOf(
Triple(Direction.DOWN, null, false), Triple(Direction.DOWN, null, false),
Triple(Direction.UP, null, true), Triple(Direction.UP, null, true),
Triple(Direction.NORTH, 2, false), Triple(Direction.NORTH, 2, false),
Triple(Direction.SOUTH, 2, true), Triple(Direction.SOUTH, 2, true),
Triple(Direction.WEST, 1, false), Triple(Direction.WEST, 1, false),
Triple(Direction.EAST, 1, true), Triple(Direction.EAST, 1, true),
) )
for ((dir, rotate, flip) in directions) { for ((dir, rotate, flip) in directions) {
val shapes = components.map { it -> val shapes = components.map { it ->
val arr = it.copyOf() val arr = it.copyOf()
if (rotate != null) { if (rotate != null) {
for (i in 0 until 3) { for (i in 0 until 3) {
arr[i] = it[(i + rotate) % 3] arr[i] = it[(i + rotate) % 3]
arr[3 + i] = it[3 + ((i + rotate) % 3)] arr[3 + i] = it[3 + ((i + rotate) % 3)]
} }
} }
if (flip) { if (flip) {
for (i in arr.indices) { for (i in arr.indices) {
arr[i] = 16.0 - arr[i] arr[i] = 16.0 - arr[i]
} }
} }
createCuboidShape(min(arr[0], arr[3]), min(arr[1], arr[4]), min(arr[2], arr[5]), max(arr[0], arr[3]), max(arr[1], arr[4]), max(arr[2], arr[5])) createCuboidShape(
} min(arr[0], arr[3]),
EXTRACTOR_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) } min(arr[1], arr[4]),
} min(arr[2], arr[5]),
} max(arr[0], arr[3]),
} max(arr[1], arr[4]),
max(arr[2], arr[5])
)
}
EXTRACTOR_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) }
}
}
}
override val faceThickness = 6.0 override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = EXTRACTOR_SHAPES override val faceShapes: Map<Direction, VoxelShape> = EXTRACTOR_SHAPES
override fun createBlockEntity(pos: BlockPos, state: BlockState) = ExtractorBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = ExtractorBlockEntity(pos, state)
override fun onPlaced(world: World, pos: BlockPos, state: BlockState, entity: LivingEntity?, stack: ItemStack) {
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighbor: Block, neighborPos: BlockPos, bl: Boolean) {
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
} }

View File

@ -1,13 +1,14 @@
package net.shadowfacts.phycon.block.extractor package net.shadowfacts.phycon.block.extractor
import alexiil.mc.lib.attributes.SearchOptions import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache
import alexiil.mc.lib.attributes.Simulation import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import alexiil.mc.lib.attributes.item.FixedItemInv import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import alexiil.mc.lib.attributes.item.ItemAttributes import net.fabricmc.fabric.api.transfer.v1.storage.Storage
import alexiil.mc.lib.attributes.item.filter.ExactItemStackFilter import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.api.packet.Packet
@ -23,111 +24,120 @@ import net.shadowfacts.phycon.packet.ItemStackPacket
import net.shadowfacts.phycon.packet.RemoteActivationPacket import net.shadowfacts.phycon.packet.RemoteActivationPacket
import net.shadowfacts.phycon.util.ActivationMode import net.shadowfacts.phycon.util.ActivationMode
import net.shadowfacts.phycon.util.ClientConfigurableDevice import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.copyWithCount
import kotlin.math.min
import kotlin.properties.Delegates import kotlin.properties.Delegates
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class ExtractorBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.EXTRACTOR, pos, state), class ExtractorBlockEntity(pos: BlockPos, state: BlockState) :
NetworkStackDispatcher<ExtractorBlockEntity.PendingInsertion>, DeviceBlockEntity(PhyBlockEntities.EXTRACTOR, pos, state),
ActivationController.ActivatableDevice, NetworkStackDispatcher<ExtractorBlockEntity.PendingInsertion>,
ClientConfigurableDevice { ActivationController.ActivatableDevice,
ClientConfigurableDevice {
companion object { companion object {
val SLEEP_TIME = 40L val SLEEP_TIME = 40L
} }
private val facing: Direction private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING] get() = cachedState[FaceDeviceBlock.FACING]
private var inventory: FixedItemInv? = null private var inventory: Pair<BlockState, BlockApiCache<Storage<ItemVariant>, Direction>>? = null
override val pendingInsertions = mutableListOf<PendingInsertion>() override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = 1L override val dispatchStackTimeout = 1L
override val controller = ActivationController(SLEEP_TIME, this) override val controller = ActivationController(SLEEP_TIME, this)
fun updateInventory() { private fun getInventory(): Storage<ItemVariant>? {
val offsetPos = pos.offset(facing) if (inventory == null) {
val option = SearchOptions.inDirection(facing) val offsetPos = pos.offset(facing)
inventory = ItemAttributes.FIXED_INV.getFirstOrNull(world, offsetPos, option) val cachedFacedBlock = world!!.getBlockState(offsetPos)
} val inventory = BlockApiCache.create(ItemStorage.SIDED, world!! as ServerWorld, offsetPos)
this.inventory = Pair(cachedFacedBlock, inventory)
}
return inventory!!.second.find(inventory!!.first, facing.opposite)
}
private fun getInventory(): FixedItemInv? { override fun handle(packet: Packet) {
if (inventory == null) updateInventory() when (packet) {
return inventory is CapacityPacket -> handleCapacity(packet)
} is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
override fun handle(packet: Packet) { override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
when (packet) { // we can't insert things back into the inventory, so just let them spawn
is CapacityPacket -> handleCapacity(packet) return packet.stack
is ItemStackPacket -> handleItemStack(packet) }
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
// we can't insert things back into the inventory, so just let them spawn
return packet.stack
}
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter) override fun finishInsertion(insertion: PendingInsertion): ItemStack {
val inventory = getInventory() ?: return insertion.stack
// if the inventory has changed, the old slot index is meaningless
if (inventory !== insertion.inventory) return insertion.stack
val transaction = Transaction.openOuter()
val extractedAmount = inventory.extract(ItemVariant.of(insertion.stack), insertion.stack.count.toLong(), transaction).toInt()
transaction.commit()
if (extractedAmount == 0) {
return insertion.stack
} else if (extractedAmount != insertion.stack.count) {
// if we extracted less than expected, make sure super.finishInsertion doesn't send more than we actually have
insertion.stack = insertion.stack.copyWithCount(extractedAmount)
}
return super.finishInsertion(insertion)
}
override fun finishInsertion(insertion: PendingInsertion): ItemStack { override fun tick() {
val inventory = getInventory() ?: return insertion.stack super.tick()
// if the inventory has changed, the old slot index is meaningless
if (inventory !== insertion.inventory) return insertion.stack
val extracted = inventory.extractStack(insertion.inventorySlot, ExactItemStackFilter(insertion.stack), ItemStack.EMPTY, insertion.totalCapacity, Simulation.ACTION)
if (extracted.isEmpty) return insertion.stack
// if we extracted less than expected, make sure super.finishInsertion doesn't send more than we actually have
insertion.stack = extracted
return super.finishInsertion(insertion)
}
override fun tick() { if (!world!!.isClient) {
super.tick() controller.tick()
if (!world!!.isClient) { finishTimedOutPendingInsertions()
controller.tick() }
}
finishTimedOutPendingInsertions() override fun activate(): Boolean {
} val inventory = getInventory() ?: return false
} for (view in inventory.iterator(null)) {
if (view.amount <= 0) continue
val transaction = Transaction.openOuter()
var extractableAmount = inventory.simulateExtract(view.resource, view.amount, transaction).toInt()
transaction.close()
if (extractableAmount <= 0) continue
extractableAmount = min(extractableAmount, view.resource.item.maxCount)
dispatchItemStack(view.resource.toStack(extractableAmount)) { insertion ->
insertion.inventory = inventory
}
return true
}
return false
}
override fun activate(): Boolean { override fun toCommonTag(tag: NbtCompound) {
val inventory = getInventory() ?: return false super.toCommonTag(tag)
for (slot in 0 until inventory.slotCount) { writeDeviceConfiguration(tag)
val slotStack = inventory.getInvStack(slot) }
if (slotStack.isEmpty) continue
val extractable = inventory.extractStack(slot, ExactItemStackFilter(slotStack), ItemStack.EMPTY, slotStack.count, Simulation.SIMULATE)
if (extractable.isEmpty) continue
dispatchItemStack(extractable) { insertion ->
insertion.inventory = inventory
insertion.inventorySlot = slot
}
return true
}
return false
}
override fun toCommonTag(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
super.toCommonTag(tag) super.fromCommonTag(tag)
writeDeviceConfiguration(tag) loadDeviceConfiguration(tag)
} }
override fun fromCommonTag(tag: NbtCompound) { override fun writeDeviceConfiguration(tag: NbtCompound) {
super.fromCommonTag(tag) tag.putString("ActivationMode", controller.activationMode.name)
loadDeviceConfiguration(tag) }
}
override fun writeDeviceConfiguration(tag: NbtCompound) { override fun loadDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name) controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
} }
override fun loadDeviceConfiguration(tag: NbtCompound) { class PendingInsertion(stack: ItemStack, timestamp: Long) :
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
} lateinit var inventory: Storage<ItemVariant>
var inventorySlot by Delegates.notNull<Int>()
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) { }
lateinit var inventory: FixedItemInv
var inventorySlot by Delegates.notNull<Int>()
}
} }

View File

@ -31,87 +31,89 @@ import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class InserterBlock: FaceDeviceBlock<InserterBlockEntity>( class InserterBlock : FaceDeviceBlock<InserterBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "inserter") val ID = Identifier(PhysicalConnectivity.MODID, "inserter")
private val INSERTER_SHAPES = mutableMapOf<Direction, VoxelShape>() private val INSERTER_SHAPES = mutableMapOf<Direction, VoxelShape>()
init { init {
val components = arrayOf( val components = arrayOf(
doubleArrayOf(4.0, 0.0, 4.0, 12.0, 2.0, 12.0), doubleArrayOf(4.0, 0.0, 4.0, 12.0, 2.0, 12.0),
doubleArrayOf(2.0, 2.0, 2.0, 14.0, 4.0, 14.0), doubleArrayOf(2.0, 2.0, 2.0, 14.0, 4.0, 14.0),
doubleArrayOf(0.0, 4.0, 0.0, 16.0, 6.0, 16.0), doubleArrayOf(0.0, 4.0, 0.0, 16.0, 6.0, 16.0),
) )
val directions = arrayOf( val directions = arrayOf(
Triple(Direction.DOWN, null, false), Triple(Direction.DOWN, null, false),
Triple(Direction.UP, null, true), Triple(Direction.UP, null, true),
Triple(Direction.NORTH, 2, false), Triple(Direction.NORTH, 2, false),
Triple(Direction.SOUTH, 2, true), Triple(Direction.SOUTH, 2, true),
Triple(Direction.WEST, 1, false), Triple(Direction.WEST, 1, false),
Triple(Direction.EAST, 1, true), Triple(Direction.EAST, 1, true),
) )
for ((dir, rotate, flip) in directions) { for ((dir, rotate, flip) in directions) {
val shapes = components.map { it -> val shapes = components.map { it ->
val arr = it.copyOf() val arr = it.copyOf()
if (rotate != null) { if (rotate != null) {
for (i in 0 until 3) { for (i in 0 until 3) {
arr[i] = it[(i + rotate) % 3] arr[i] = it[(i + rotate) % 3]
arr[3 + i] = it[3 + ((i + rotate) % 3)] arr[3 + i] = it[3 + ((i + rotate) % 3)]
} }
} }
if (flip) { if (flip) {
for (i in arr.indices) { for (i in arr.indices) {
arr[i] = 16.0 - arr[i] arr[i] = 16.0 - arr[i]
} }
} }
createCuboidShape(min(arr[0], arr[3]), min(arr[1], arr[4]), min(arr[2], arr[5]), max(arr[0], arr[3]), max(arr[1], arr[4]), max(arr[2], arr[5])) createCuboidShape(
} min(arr[0], arr[3]),
INSERTER_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) } min(arr[1], arr[4]),
} min(arr[2], arr[5]),
} max(arr[0], arr[3]),
} max(arr[1], arr[4]),
max(arr[2], arr[5])
)
}
INSERTER_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) }
}
}
}
override val faceThickness = 6.0 override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = INSERTER_SHAPES override val faceShapes: Map<Direction, VoxelShape> = INSERTER_SHAPES
override fun createBlockEntity(pos: BlockPos, state: BlockState) = InserterBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = InserterBlockEntity(pos, state)
override fun onPlaced(world: World, pos: BlockPos, state: BlockState, entity: LivingEntity?, stack: ItemStack) { override fun onUse(
if (!world.isClient) { state: BlockState,
getBlockEntity(world, pos)!!.updateInventory() world: World,
} pos: BlockPos,
} player: PlayerEntity,
hand: Hand,
hitResult: BlockHitResult
): ActionResult {
if (!world.isClient) {
val be = getBlockEntity(world, pos)!!
override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighborBlock: Block, neighborPos: BlockPos, bl: Boolean) { be.markUpdate()
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult { val factory = object : ExtendedScreenHandlerFactory {
if (!world.isClient) { override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler {
val be = getBlockEntity(world, pos)!! return InserterScreenHandler(syncId, playerInv, be)
}
be.markUpdate() override fun getDisplayName() = this@InserterBlock.name
val factory = object: ExtendedScreenHandlerFactory { override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler { buf.writeBlockPos(be.pos)
return InserterScreenHandler(syncId, playerInv, be) }
} }
player.openHandledScreen(factory)
override fun getDisplayName() = this@InserterBlock.name }
return ActionResult.SUCCESS
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { }
buf.writeBlockPos(be.pos)
}
}
player.openHandledScreen(factory)
}
return ActionResult.SUCCESS
}
} }

View File

@ -1,13 +1,14 @@
package net.shadowfacts.phycon.block.inserter package net.shadowfacts.phycon.block.inserter
import alexiil.mc.lib.attributes.SearchOptions import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache
import alexiil.mc.lib.attributes.Simulation import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import alexiil.mc.lib.attributes.item.ItemAttributes import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import alexiil.mc.lib.attributes.item.ItemInsertable import net.fabricmc.fabric.api.transfer.v1.storage.Storage
import alexiil.mc.lib.attributes.item.ItemStackUtil import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.api.packet.Packet
@ -19,153 +20,161 @@ import net.shadowfacts.phycon.component.NetworkStackProvider
import net.shadowfacts.phycon.component.handleItemStack import net.shadowfacts.phycon.component.handleItemStack
import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.* import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode import net.shadowfacts.phycon.util.*
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.GhostInv
import kotlin.math.min import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class InserterBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.INSERTER, pos, state), class InserterBlockEntity(pos: BlockPos, state: BlockState) : DeviceBlockEntity(PhyBlockEntities.INSERTER, pos, state),
ItemStackPacketHandler, ItemStackPacketHandler,
ActivationController.ActivatableDevice, ActivationController.ActivatableDevice,
ClientConfigurableDevice, ClientConfigurableDevice,
GhostInv { GhostInv {
companion object { companion object {
val SLEEP_TIME = 40L val SLEEP_TIME = 40L
val REQUEST_TIMEOUT = 40 val REQUEST_TIMEOUT = 40
} }
private val facing: Direction private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING] get() = cachedState[FaceDeviceBlock.FACING]
private var inventory: ItemInsertable? = null private var inventory: Pair<BlockState, BlockApiCache<Storage<ItemVariant>, Direction>>? = null
private var currentRequest: PendingExtractRequest? = null private var currentRequest: PendingExtractRequest? = null
var stackToExtract: ItemStack = ItemStack.EMPTY var stackToExtract: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack override var ghostSlotStack: ItemStack
get() = stackToExtract get() = stackToExtract
set(value) { stackToExtract = value } set(value) {
var amountToExtract = 1 stackToExtract = value
override val controller = ActivationController(SLEEP_TIME, this) }
var amountToExtract = 1
override val controller = ActivationController(SLEEP_TIME, this)
fun updateInventory() { private fun getInventory(): Storage<ItemVariant>? {
val offsetPos = pos.offset(facing) if (inventory == null) {
val option = SearchOptions.inDirection(facing) val offsetPos = pos.offset(facing)
inventory = ItemAttributes.INSERTABLE.getFirstOrNull(world, offsetPos, option) val cachedFacedBlock = world!!.getBlockState(offsetPos)
} val inventory = BlockApiCache.create(ItemStorage.SIDED, world!! as ServerWorld, offsetPos)
this.inventory = Pair(cachedFacedBlock, inventory)
}
return inventory!!.second.find(inventory!!.first, facing.opposite)
}
private fun getInventory(): ItemInsertable? { override fun handle(packet: Packet) {
if (inventory == null) updateInventory() when (packet) {
return inventory is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
} is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
override fun handle(packet: Packet) { override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
when (packet) { val inventory = getInventory()
is RemoteActivationPacket -> controller.handleRemoteActivation(packet) if (inventory != null) {
is StackLocationPacket -> handleStackLocation(packet) val transaction = Transaction.openOuter()
is ItemStackPacket -> handleItemStack(packet) val inserted = inventory.insert(ItemVariant.of(packet.stack), packet.stack.count.toLong(), transaction).toInt()
} transaction.commit()
} return if (inserted == 0) {
packet.stack
} else if (inserted == packet.stack.count) {
ItemStack.EMPTY
} else {
packet.stack.copyWithCount(packet.stack.count - inserted)
}
} else {
// no inventory, entire stack remains
return packet.stack
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { private fun handleStackLocation(packet: StackLocationPacket) {
val inventory = getInventory() val request = currentRequest
return if (inventory != null) { if (request != null && request.stack.equalsIgnoringAmount(packet.stack)) {
inventory.attemptInsertion(packet.stack, Simulation.ACTION) request.results.add(packet.amount to packet.stackProvider)
} else { if (request.isFinishable(counter)) {
// no inventory, entire stack remains finishRequest()
packet.stack }
} }
} }
private fun handleStackLocation(packet: StackLocationPacket) { override fun tick() {
val request = currentRequest super.tick()
if (request != null && ItemStackUtil.areEqualIgnoreAmounts(request.stack, packet.stack)) {
request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) {
finishRequest()
}
}
}
override fun tick() { if (!world!!.isClient) {
super.tick() controller.tick()
if (!world!!.isClient) { val request = currentRequest
controller.tick() if (request != null) {
if (request.isFinishable(counter)) {
finishRequest()
} else if (counter - request.timestamp >= REQUEST_TIMEOUT && request.totalAmount == 0) {
currentRequest = null
}
}
}
}
val request = currentRequest override fun activate(): Boolean {
if (request != null) { if (currentRequest != null || stackToExtract.isEmpty) {
if (request.isFinishable(counter)) { return false
finishRequest() }
} else if (counter - request.timestamp >= REQUEST_TIMEOUT && request.totalAmount == 0) {
currentRequest = null
}
}
}
}
override fun activate(): Boolean { // todo: configure me
if (currentRequest != null || stackToExtract.isEmpty) { currentRequest = PendingExtractRequest(stackToExtract, counter)
return false sendPacket(LocateStackPacket(stackToExtract, ipAddress))
} return true
}
// todo: configure me private fun finishRequest() {
currentRequest = PendingExtractRequest(stackToExtract, counter) val request = currentRequest ?: return
sendPacket(LocateStackPacket(stackToExtract, ipAddress))
return true
}
private fun finishRequest() { // todo: dedup with TerminalBlockEntity.stackLocateRequestCompleted
val request = currentRequest ?: return val actualAmount = min(min(request.stack.maxCount, request.totalAmount), amountToExtract)
val sortedResults = request.results.sortedByDescending { it.first }.toMutableList()
var amountRequested = 0
while (amountRequested < actualAmount && sortedResults.isNotEmpty()) {
val (sourceAmount, source) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, actualAmount - amountRequested)
amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, source.ipAddress))
}
// todo: dedup with TerminalBlockEntity.stackLocateRequestCompleted currentRequest = null
val actualAmount = min(min(request.stack.maxCount, request.totalAmount), amountToExtract) }
val sortedResults = request.results.sortedByDescending { it.first }.toMutableList()
var amountRequested = 0
while (amountRequested < actualAmount && sortedResults.isNotEmpty()) {
val (sourceAmount, source) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, actualAmount - amountRequested)
amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, source.ipAddress))
}
currentRequest = null override fun toCommonTag(tag: NbtCompound) {
} super.toCommonTag(tag)
writeDeviceConfiguration(tag)
tag.put("StackToExtract", stackToExtract.writeNbt(NbtCompound()))
}
override fun toCommonTag(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
super.toCommonTag(tag) super.fromCommonTag(tag)
writeDeviceConfiguration(tag) loadDeviceConfiguration(tag)
tag.put("StackToExtract", stackToExtract.writeNbt(NbtCompound())) stackToExtract = ItemStack.fromNbt(tag.getCompound("StackToExtract"))
} }
override fun fromCommonTag(tag: NbtCompound) { override fun writeDeviceConfiguration(tag: NbtCompound) {
super.fromCommonTag(tag) tag.putString("ActivationMode", controller.activationMode.name)
loadDeviceConfiguration(tag) tag.putInt("AmountToExtract", amountToExtract)
stackToExtract = ItemStack.fromNbt(tag.getCompound("StackToExtract")) }
}
override fun writeDeviceConfiguration(tag: NbtCompound) { override fun loadDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name) controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
tag.putInt("AmountToExtract", amountToExtract) amountToExtract = tag.getInt("AmountToExtract")
} }
override fun loadDeviceConfiguration(tag: NbtCompound) { class PendingExtractRequest(
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) val stack: ItemStack,
amountToExtract = tag.getInt("AmountToExtract") val timestamp: Long,
} var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
) {
val totalAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
class PendingExtractRequest( fun isFinishable(currentTimestamp: Long): Boolean {
val stack: ItemStack, return totalAmount >= stack.maxCount || (currentTimestamp - timestamp >= REQUEST_TIMEOUT && totalAmount > 0)
val timestamp: Long, }
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf() }
) {
val totalAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
fun isFinishable(currentTimestamp: Long): Boolean {
return totalAmount >= stack.maxCount || (currentTimestamp - timestamp >= REQUEST_TIMEOUT && totalAmount > 0)
}
}
} }

View File

@ -19,107 +19,107 @@ import java.lang.NumberFormatException
* @author shadowfacts * @author shadowfacts
*/ */
class InserterScreen( class InserterScreen(
handler: InserterScreenHandler, handler: InserterScreenHandler,
playerInv: PlayerInventory, playerInv: PlayerInventory,
title: Text, title: Text,
): HandledScreen<InserterScreenHandler>( ) : HandledScreen<InserterScreenHandler>(
handler, handler,
playerInv, playerInv,
title title
) { ) {
companion object { companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/inserter.png") val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/inserter.png")
} }
private lateinit var amountField: TextFieldWidget private lateinit var amountField: TextFieldWidget
init { init {
backgroundWidth = 176 backgroundWidth = 176
backgroundHeight = 133 backgroundHeight = 133
playerInventoryTitleY = backgroundHeight - 94 playerInventoryTitleY = backgroundHeight - 94
} }
override fun init() { override fun init() {
super.init() super.init()
amountField = TextFieldWidget(textRenderer, x + 57, y + 24, 80, 9, LiteralText("Amount")) amountField = TextFieldWidget(textRenderer, x + 57, y + 24, 80, 9, LiteralText("Amount"))
amountField.text = handler.inserter.amountToExtract.toString() amountField.text = handler.inserter.amountToExtract.toString()
amountField.setDrawsBackground(false) amountField.setDrawsBackground(false)
amountField.isVisible = true amountField.isVisible = true
amountField.setTextFieldFocused(true) amountField.setTextFieldFocused(true)
amountField.setEditableColor(0xffffff) amountField.setEditableColor(0xffffff)
amountField.setTextPredicate { amountField.setTextPredicate {
if (it.isEmpty()) { if (it.isEmpty()) {
true true
} else { } else {
try { try {
val value = Integer.parseInt(it) val value = Integer.parseInt(it)
value in 1..64 value in 1..64
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
false false
} }
} }
} }
addDrawableChild(amountField) addDrawableChild(amountField)
} }
fun amountUpdated() { fun amountUpdated() {
if (amountField.text.isNotEmpty()) { if (amountField.text.isNotEmpty()) {
handler.inserter.amountToExtract = Integer.parseInt(amountField.text) handler.inserter.amountToExtract = Integer.parseInt(amountField.text)
client!!.player!!.networkHandler.sendPacket(C2SConfigureDevice(handler.inserter)) client!!.player!!.networkHandler.sendPacket(C2SConfigureDevice(handler.inserter))
} }
} }
override fun handledScreenTick() { override fun handledScreenTick() {
super.handledScreenTick() super.handledScreenTick()
amountField.tick() amountField.tick()
} }
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
renderBackground(matrixStack) renderBackground(matrixStack)
RenderSystem.setShader(GameRenderer::getPositionTexShader) RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND) RenderSystem.setShaderTexture(0, BACKGROUND)
val x = (width - backgroundWidth) / 2 val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2 val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight) drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
} }
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
super.render(matrixStack, mouseX, mouseY, delta) super.render(matrixStack, mouseX, mouseY, delta)
amountField.render(matrixStack, mouseX, mouseY, delta) amountField.render(matrixStack, mouseX, mouseY, delta)
drawMouseoverTooltip(matrixStack, mouseX, mouseY) drawMouseoverTooltip(matrixStack, mouseX, mouseY)
} }
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, slotActionType: SlotActionType?) { override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, slotActionType: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, slotActionType) super.onMouseClick(slot, invSlot, clickData, slotActionType)
amountField.setTextFieldFocused(true) amountField.setTextFieldFocused(true)
} }
override fun charTyped(c: Char, i: Int): Boolean { override fun charTyped(c: Char, i: Int): Boolean {
val oldText = amountField.text val oldText = amountField.text
if (amountField.charTyped(c, i)) { if (amountField.charTyped(c, i)) {
if (oldText != amountField.text) { if (oldText != amountField.text) {
amountUpdated() amountUpdated()
} }
return true return true
} }
return super.charTyped(c, i) return super.charTyped(c, i)
} }
override fun keyPressed(i: Int, j: Int, k: Int): Boolean { override fun keyPressed(i: Int, j: Int, k: Int): Boolean {
val oldText = amountField.text val oldText = amountField.text
if (amountField.keyPressed(i, j, k)) { if (amountField.keyPressed(i, j, k)) {
if (oldText != amountField.text) { if (oldText != amountField.text) {
amountUpdated() amountUpdated()
} }
return true return true
} }
return super.keyPressed(i, j, k) return super.keyPressed(i, j, k)
} }
} }

View File

@ -19,65 +19,65 @@ import kotlin.math.min
* @author shadowfacts * @author shadowfacts
*/ */
class InserterScreenHandler( class InserterScreenHandler(
syncId: Int, syncId: Int,
playerInv: PlayerInventory, playerInv: PlayerInventory,
val inserter: InserterBlockEntity, val inserter: InserterBlockEntity,
): ScreenHandler(PhyScreens.INSERTER, syncId) { ) : ScreenHandler(PhyScreens.INSERTER, syncId) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "inserter") val ID = Identifier(PhysicalConnectivity.MODID, "inserter")
} }
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf) :
this( this(
syncId, syncId,
playerInv, playerInv,
PhyBlocks.INSERTER.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! PhyBlocks.INSERTER.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
) )
init { init {
// fake slot // fake slot
addSlot(GhostSlot(inserter, 31, 20)) addSlot(GhostSlot(inserter, 31, 20))
// player inv // player inv
for (y in 0 until 3) { for (y in 0 until 3) {
for (x in 0 until 9) { for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, 8 + x * 18, 51 + y * 18)) addSlot(Slot(playerInv, x + y * 9 + 9, 8 + x * 18, 51 + y * 18))
} }
} }
// hotbar // hotbar
for (x in 0 until 9) { for (x in 0 until 9) {
addSlot(Slot(playerInv, x, 8 + x * 18, 109)) addSlot(Slot(playerInv, x, 8 + x * 18, 109))
} }
} }
private fun stackToExtractChanged() { private fun stackToExtractChanged() {
inserter.amountToExtract = min(inserter.stackToExtract.maxCount, inserter.amountToExtract) inserter.amountToExtract = min(inserter.stackToExtract.maxCount, inserter.amountToExtract)
} }
override fun canUse(player: PlayerEntity): Boolean { override fun canUse(player: PlayerEntity): Boolean {
return true return true
} }
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity) { override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity) {
// fake slot // fake slot
if (slotId == 0) { if (slotId == 0) {
if (cursorStack.isEmpty) { if (cursorStack.isEmpty) {
inserter.stackToExtract = ItemStack.EMPTY inserter.stackToExtract = ItemStack.EMPTY
} else { } else {
inserter.stackToExtract = cursorStack.copyWithCount(1) inserter.stackToExtract = cursorStack.copyWithCount(1)
} }
stackToExtractChanged() stackToExtractChanged()
} }
super.onSlotClick(slotId, clickData, actionType, player) super.onSlotClick(slotId, clickData, actionType, player)
} }
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
val slot = slots[slotId] val slot = slots[slotId]
inserter.stackToExtract = slot.stack.copyWithCount(1) inserter.stackToExtract = slot.stack.copyWithCount(1)
stackToExtractChanged() stackToExtractChanged()
return ItemStack.EMPTY return ItemStack.EMPTY
} }
} }

View File

@ -23,51 +23,63 @@ import java.util.*
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class MinerBlock: DeviceBlock<MinerBlockEntity>( class MinerBlock : DeviceBlock<MinerBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "miner") val ID = Identifier(PhysicalConnectivity.MODID, "miner")
val FACING = Properties.FACING val FACING = Properties.FACING
} }
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> { override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.of(state[FACING].opposite) return EnumSet.of(state[FACING].opposite)
} }
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { override fun getNetworkInterfaceForSide(
return if (side == state[FACING]) { side: Direction,
null state: BlockState,
} else { world: WorldAccess,
getBlockEntity(world, pos) pos: BlockPos
} ): Interface? {
} return if (side == state[FACING]) {
null
} else {
getBlockEntity(world, pos)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) { override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder) super.appendProperties(builder)
builder.add(FACING) builder.add(FACING)
} }
override fun createBlockEntity(pos: BlockPos, state: BlockState) = MinerBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = MinerBlockEntity(pos, state)
override fun getPlacementState(context: ItemPlacementContext): BlockState? { override fun getPlacementState(context: ItemPlacementContext): BlockState? {
val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite
return defaultState.with(FACING, facing) return defaultState.with(FACING, facing)
} }
override fun onPlaced(world: World, pos: BlockPos, state: BlockState, entity: LivingEntity?, itemStack: ItemStack) { override fun onPlaced(world: World, pos: BlockPos, state: BlockState, entity: LivingEntity?, itemStack: ItemStack) {
if (!world.isClient) { if (!world.isClient) {
// getBlockEntity(world, pos)!!.updateBlockToMine() // getBlockEntity(world, pos)!!.updateBlockToMine()
} }
} }
override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighbor: Block, neighborPos: BlockPos, bl: Boolean) { override fun neighborUpdate(
if (!world.isClient) { state: BlockState,
world: World,
pos: BlockPos,
neighbor: Block,
neighborPos: BlockPos,
bl: Boolean
) {
if (!world.isClient) {
// getBlockEntity(world, pos)!!.updateBlockToMine() // getBlockEntity(world, pos)!!.updateBlockToMine()
} }
} }
} }

View File

@ -1,8 +1,10 @@
package net.shadowfacts.phycon.block.miner package net.shadowfacts.phycon.block.miner
import alexiil.mc.lib.attributes.item.GroupedItemInvView import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import alexiil.mc.lib.attributes.item.ItemStackUtil import net.fabricmc.fabric.api.transfer.v1.storage.StorageView
import alexiil.mc.lib.attributes.item.filter.ItemFilter import net.fabricmc.fabric.api.transfer.v1.storage.base.ExtractionOnlyStorage
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant
import net.minecraft.block.Block import net.minecraft.block.Block
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
@ -22,219 +24,262 @@ import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode import net.shadowfacts.phycon.util.ActivationMode
import net.shadowfacts.phycon.util.ClientConfigurableDevice import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.copyWithCount import net.shadowfacts.phycon.util.copyWithCount
import net.shadowfacts.phycon.util.equalsIgnoringAmount
import kotlin.math.min import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class MinerBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.MINER, pos, state), class MinerBlockEntity(pos: BlockPos, state: BlockState) : DeviceBlockEntity(PhyBlockEntities.MINER, pos, state),
NetworkStackProvider, NetworkStackProvider,
NetworkStackDispatcher<MinerBlockEntity.PendingInsertion>, NetworkStackDispatcher<MinerBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice, ActivationController.ActivatableDevice,
ClientConfigurableDevice { ClientConfigurableDevice {
private val facing: Direction private val facing: Direction
get() = cachedState[MinerBlock.FACING] get() = cachedState[MinerBlock.FACING]
private val invProxy = MinerInvProxy(this) private val invProxy = MinerStorageProxy(this)
override val pendingInsertions = mutableListOf<PendingInsertion>() override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = AbstractTerminalBlockEntity.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
var minerMode = MinerMode.ON_DEMAND var minerMode = MinerMode.ON_DEMAND
override fun handle(packet: Packet) { override fun handle(packet: Packet) {
when (packet) { when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet) is RequestInventoryPacket -> handleRequestInventory(packet)
is LocateStackPacket -> handleLocateStack(packet) is LocateStackPacket -> handleLocateStack(packet)
is ExtractStackPacket -> handleExtractStack(packet) is ExtractStackPacket -> handleExtractStack(packet)
is CapacityPacket -> handleCapacity(packet) is CapacityPacket -> handleCapacity(packet)
is ItemStackPacket -> handleItemStack(packet) is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet) is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
} }
} }
private fun handleRequestInventory(packet: RequestInventoryPacket) { private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (minerMode != MinerMode.ON_DEMAND || packet.kind != RequestInventoryPacket.Kind.GROUPED) { if (minerMode != MinerMode.ON_DEMAND || packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return return
} }
sendPacket(ReadGroupedInventoryPacket(invProxy, ipAddress, packet.source)) sendPacket(ReadItemStoragePacket(invProxy, ipAddress, packet.source))
} }
private fun handleLocateStack(packet: LocateStackPacket) { private fun handleLocateStack(packet: LocateStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) { if (minerMode != MinerMode.ON_DEMAND) {
return return
} }
val amount = invProxy.getAmount(packet.stack) val amount = invProxy.simulateExtract(ItemVariant.of(packet.stack), Long.MAX_VALUE, null)
if (amount > 0) { if (amount > 0) {
sendPacket(StackLocationPacket(packet.stack, amount, this, ipAddress, packet.source)) sendPacket(StackLocationPacket(packet.stack, amount.toInt(), this, ipAddress, packet.source))
} }
} }
private fun handleExtractStack(packet: ExtractStackPacket) { private fun handleExtractStack(packet: ExtractStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) { if (minerMode != MinerMode.ON_DEMAND) {
return return
} }
// always recalculate immediately before breaking // always recalculate immediately before breaking
val drops = invProxy.getDrops(recalculate = true) val drops = invProxy.getDrops(recalculate = true)
if (invProxy.getAmount(packet.stack) > 0) { val amount = invProxy.simulateExtract(ItemVariant.of(packet.stack), packet.amount.toLong(), null)
world!!.breakBlock(pos.offset(facing), false) if (amount > 0) {
world!!.breakBlock(pos.offset(facing), false)
// send the requested amount back to the requester // send the requested amount back to the requester
var remaining = packet.amount var remaining = packet.amount
for (droppedStack in drops) { for (droppedStack in drops) {
if (remaining <= 0) { if (remaining <= 0) {
break break
} }
if (!ItemStackUtil.areEqualIgnoreAmounts(droppedStack, packet.stack)) { if (!droppedStack.equalsIgnoringAmount(packet.stack)) {
continue continue
} }
val toDecr = min(droppedStack.count, remaining) val toDecr = min(droppedStack.count, remaining)
val copy = droppedStack.copyWithCount(toDecr) val copy = droppedStack.copyWithCount(toDecr)
droppedStack.decrement(toDecr) droppedStack.decrement(toDecr)
remaining -= toDecr remaining -= toDecr
// todo: should this try to combine stacks and send as few packets as possible? // todo: should this try to combine stacks and send as few packets as possible?
sendPacket(ItemStackPacket(copy, ipAddress, packet.source)) sendPacket(ItemStackPacket(copy, ipAddress, packet.source))
} }
// dump any remaining drops into the network // dump any remaining drops into the network
for (droppedStack in drops) { for (droppedStack in drops) {
if (droppedStack.isEmpty) continue if (droppedStack.isEmpty) continue
dispatchItemStack(droppedStack) dispatchItemStack(droppedStack)
} }
} }
} }
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
// miner can't receive stacks, so remaining is the entire packet stack // miner can't receive stacks, so remaining is the entire packet stack
return packet.stack return packet.stack
} }
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter) override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun tick() { override fun tick() {
super.tick() super.tick()
if (!world!!.isClient) { if (!world!!.isClient) {
if (minerMode == MinerMode.AUTOMATIC) { if (minerMode == MinerMode.AUTOMATIC) {
controller.tick() controller.tick()
} }
finishTimedOutPendingInsertions() finishTimedOutPendingInsertions()
} }
} }
override fun activate(): Boolean { override fun activate(): Boolean {
if (minerMode == MinerMode.ON_DEMAND) { if (minerMode == MinerMode.ON_DEMAND) {
return false return false
} }
val drops = invProxy.getDrops(recalculate = true) val drops = invProxy.getDrops(recalculate = true)
if (!world!!.getBlockState(pos.offset(facing)).isAir) { if (!world!!.getBlockState(pos.offset(facing)).isAir) {
world!!.breakBlock(pos.offset(facing), false) world!!.breakBlock(pos.offset(facing), false)
for (stack in drops) { for (stack in drops) {
if (stack.isEmpty) continue if (stack.isEmpty) continue
dispatchItemStack(stack) dispatchItemStack(stack)
} }
return true return true
} else { } else {
return false return false
} }
} }
override fun canConfigureActivationController(): Boolean { override fun canConfigureActivationController(): Boolean {
return minerMode == MinerMode.AUTOMATIC return minerMode == MinerMode.AUTOMATIC
} }
override fun canConfigureProviderPriority(): Boolean { override fun canConfigureProviderPriority(): Boolean {
return minerMode == MinerMode.ON_DEMAND return minerMode == MinerMode.ON_DEMAND
} }
override fun toCommonTag(tag: NbtCompound) { override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag) super.toCommonTag(tag)
writeDeviceConfiguration(tag) writeDeviceConfiguration(tag)
} }
override fun fromCommonTag(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag) super.fromCommonTag(tag)
loadDeviceConfiguration(tag) loadDeviceConfiguration(tag)
} }
override fun writeDeviceConfiguration(tag: NbtCompound) { override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("MinerMode", minerMode.name) tag.putString("MinerMode", minerMode.name)
tag.putString("ActivationMode", controller.activationMode.name) tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("ProviderPriority", providerPriority) tag.putInt("ProviderPriority", providerPriority)
} }
override fun loadDeviceConfiguration(tag: NbtCompound) { override fun loadDeviceConfiguration(tag: NbtCompound) {
minerMode = MinerMode.valueOf(tag.getString("MinerMode")) minerMode = MinerMode.valueOf(tag.getString("MinerMode"))
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
providerPriority = tag.getInt("ProviderPriority") providerPriority = tag.getInt("ProviderPriority")
} }
enum class MinerMode { enum class MinerMode {
ON_DEMAND, AUTOMATIC; ON_DEMAND, AUTOMATIC;
val friendlyName = TranslatableText("gui.phycon.miner_mode.${name.lowercase()}") val friendlyName = TranslatableText("gui.phycon.miner_mode.${name.lowercase()}")
} }
class MinerInvProxy(val miner: MinerBlockEntity): GroupedItemInvView { class MinerStorageProxy(val miner: MinerBlockEntity) :
companion object { SnapshotParticipant<Pair<BlockState, List<ItemStack>>?>(),
val TOOL = ItemStack(Items.DIAMOND_PICKAXE) ExtractionOnlyStorage<ItemVariant>
} {
companion object {
val TOOL = ItemStack(Items.DIAMOND_PICKAXE)
}
private var cachedState: BlockState? = null private var cache: Pair<BlockState, List<ItemStack>>? = null
private var cachedDrops: List<ItemStack>? = null
private val world: World private val world: World
get() = miner.world!! get() = miner.world!!
private val pos: BlockPos private val pos: BlockPos
get() = miner.pos!! get() = miner.pos!!
private val facing: Direction private val facing: Direction
get() = miner.facing get() = miner.facing
fun getDrops(recalculate: Boolean = false): List<ItemStack> { fun getDrops(recalculate: Boolean = false): List<ItemStack> {
val targetPos = pos.offset(facing) val targetPos = pos.offset(facing)
val realState = world.getBlockState(targetPos) val realState = world.getBlockState(targetPos)
// todo: does BlockState.equals actually work or is reference equality fine for BlockStates? val cache = this.cache
if (cachedDrops == null || realState != cachedState || recalculate) { // todo: does BlockState.equals actually work or is reference equality fine for BlockStates?
cachedState = realState if (cache == null || realState != cache.first || recalculate) {
val be = if (realState.hasBlockEntity()) world.getBlockEntity(targetPos) else null
val drops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be, null, TOOL)
this.cache = Pair(realState, drops)
return drops
}
return cache.second
}
val be = if (realState.hasBlockEntity()) world.getBlockEntity(targetPos) else null override fun createSnapshot(): Pair<BlockState, List<ItemStack>>? {
cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be, null, TOOL) return cache
} }
return cachedDrops!!
}
override fun getStoredStacks(): Set<ItemStack> { override fun readSnapshot(snapshot: Pair<BlockState, List<ItemStack>>?) {
if (miner.minerMode != MinerMode.ON_DEMAND) { cache = snapshot
return setOf() }
}
return getDrops().toSet()
}
override fun getTotalCapacity(): Int { override fun extract(resource: ItemVariant, maxAmount: Long, transaction: TransactionContext): Long {
return Int.MAX_VALUE val drops = getDrops()
} val (matched, unmatched) = drops.partition { resource.matches(it) }
if (matched.isEmpty()) {
return 0
}
val matchedCount = matched.sumOf { it.count }.toLong()
val extracted = min(maxAmount, matchedCount)
transaction.addCloseCallback { context, result ->
if (result.wasCommitted()) {
world.breakBlock(pos.offset(facing), false)
// send any un-extracted drops to the network
if (matchedCount > extracted) {
miner.dispatchItemStack(resource.toStack().copyWithCount((matchedCount - extracted).toInt()))
}
for (stack in unmatched) {
miner.dispatchItemStack(stack)
}
}
}
return extracted
}
override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic { override fun iterator(transaction: TransactionContext): Iterator<StorageView<ItemVariant>> {
var totalCount = 0 return getDrops().map { View(it, this) }.iterator()
for (s in storedStacks) { }
if (filter.matches(s)) {
totalCount += s.count
}
}
return GroupedItemInvView.ItemInvStatistic(filter, totalCount, 0, Int.MAX_VALUE)
}
}
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) { class View(val stack: ItemStack, val proxy: MinerStorageProxy) : StorageView<ItemVariant> {
} override fun extract(resource: ItemVariant, maxAmount: Long, transaction: TransactionContext): Long {
return proxy.extract(resource, maxAmount, transaction)
}
override fun isResourceBlank(): Boolean {
return false
}
override fun getResource(): ItemVariant {
return ItemVariant.of(stack)
}
override fun getAmount(): Long {
return stack.count.toLong()
}
override fun getCapacity(): Long {
// TODO: ehh? we have stuff stored, but nothing can be inserted, so...
return 0
}
}
}
class PendingInsertion(stack: ItemStack, timestamp: Long) :
NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
}
} }

View File

@ -1,7 +1,5 @@
package net.shadowfacts.phycon.block.netinterface package net.shadowfacts.phycon.block.netinterface
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.* import net.minecraft.block.*
import net.minecraft.entity.LivingEntity import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
@ -18,44 +16,27 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>( class InterfaceBlock : FaceDeviceBlock<InterfaceBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
), ),
NetworkComponentBlock, NetworkComponentBlock {
AttributeProvider {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "network_interface") val ID = Identifier(PhysicalConnectivity.MODID, "network_interface")
} }
override val faceThickness = 2.0 override val faceThickness = 2.0
override val faceShapes = mapOf( override val faceShapes = mapOf(
Direction.DOWN to createCuboidShape(2.0, 0.0, 2.0, 14.0, 2.0, 14.0), Direction.DOWN to createCuboidShape(2.0, 0.0, 2.0, 14.0, 2.0, 14.0),
Direction.UP to createCuboidShape(2.0, 14.0, 2.0, 14.0, 16.0, 14.0), Direction.UP to createCuboidShape(2.0, 14.0, 2.0, 14.0, 16.0, 14.0),
Direction.NORTH to createCuboidShape(2.0, 2.0, 0.0, 14.0, 14.0, 2.0), Direction.NORTH to createCuboidShape(2.0, 2.0, 0.0, 14.0, 14.0, 2.0),
Direction.SOUTH to createCuboidShape(2.0, 2.0, 14.0, 14.0, 14.0, 16.0), Direction.SOUTH to createCuboidShape(2.0, 2.0, 14.0, 14.0, 14.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 2.0, 2.0, 2.0, 14.0, 14.0), Direction.WEST to createCuboidShape(0.0, 2.0, 2.0, 2.0, 14.0, 14.0),
Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0) Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0)
) )
override fun createBlockEntity(pos: BlockPos, state: BlockState) = InterfaceBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = InterfaceBlockEntity(pos, state)
override fun onPlaced(world: World, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighborBlock: Block, neighborPos: BlockPos, boolean_1: Boolean) {
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) {
to.offer(getBlockEntity(world, pos))
}
} }

View File

@ -1,12 +1,14 @@
package net.shadowfacts.phycon.block.netinterface package net.shadowfacts.phycon.block.netinterface
import alexiil.mc.lib.attributes.SearchOptions import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache
import alexiil.mc.lib.attributes.Simulation import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import alexiil.mc.lib.attributes.item.GroupedItemInv import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import alexiil.mc.lib.attributes.item.ItemAttributes import net.fabricmc.fabric.api.transfer.v1.storage.Storage
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.api.packet.Packet
@ -19,122 +21,123 @@ import net.shadowfacts.phycon.component.NetworkStackReceiver
import net.shadowfacts.phycon.component.handleItemStack import net.shadowfacts.phycon.component.handleItemStack
import net.shadowfacts.phycon.packet.* import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ClientConfigurableDevice import net.shadowfacts.phycon.util.ClientConfigurableDevice
import java.lang.ref.WeakReference import net.shadowfacts.phycon.util.copyWithCount
import kotlin.math.min import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class InterfaceBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.INTERFACE, pos, state), class InterfaceBlockEntity(pos: BlockPos, state: BlockState) :
ItemStackPacketHandler, DeviceBlockEntity(PhyBlockEntities.INTERFACE, pos, state),
NetworkStackProvider, ItemStackPacketHandler,
NetworkStackReceiver, NetworkStackProvider,
ClientConfigurableDevice { NetworkStackReceiver,
ClientConfigurableDevice {
private val facing: Direction private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING] get() = cachedState[FaceDeviceBlock.FACING]
override var providerPriority = 0 override var providerPriority = 0
override var receiverPriority = 0 override var receiverPriority = 0
var syncPriorities = true var syncPriorities = true
private var inventory: WeakReference<GroupedItemInv>? = null private var cachedFacedBlock: BlockState? = null
private var inventoryCache: BlockApiCache<Storage<ItemVariant>, Direction>? = null
fun updateInventory() { private fun getInventory(): Storage<ItemVariant>? {
val offsetPos = pos.offset(facing) if (cachedFacedBlock == null) {
val option = SearchOptions.inDirection(facing) cachedFacedBlock = world!!.getBlockState(pos.offset(facing))
inventory = ItemAttributes.GROUPED_INV.getFirstOrNull(world, offsetPos, option).let { }
WeakReference(it) if (inventoryCache == null) {
} inventoryCache = BlockApiCache.create(ItemStorage.SIDED, world!! as ServerWorld, pos.offset(facing))
} }
return inventoryCache!!.find(cachedFacedBlock!!, facing.opposite)
}
private fun getInventory(): GroupedItemInv? { override fun handle(packet: Packet) {
// if we don't have an inventory, try to get one when (packet) {
// this happens when readAll is called before a neighbor state changes, such as immediately after world load is RequestInventoryPacket -> handleRequestInventory(packet)
if (inventory?.get() == null) updateInventory() is LocateStackPacket -> handleLocateStack(packet)
return inventory?.get() is ExtractStackPacket -> handleExtractStack(packet)
} is CheckCapacityPacket -> handleCheckCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
override fun handle(packet: Packet) { private fun handleRequestInventory(packet: RequestInventoryPacket) {
when (packet) { if (packet.kind != RequestInventoryPacket.Kind.GROUPED) {
is RequestInventoryPacket -> handleRequestInventory(packet) return
is LocateStackPacket -> handleLocateStack(packet) }
is ExtractStackPacket -> handleExtractStack(packet) getInventory()?.also { inv ->
is CheckCapacityPacket -> handleCheckCapacity(packet) sendPacket(ReadItemStoragePacket(inv, ipAddress, packet.source))
is ItemStackPacket -> handleItemStack(packet) }
} }
}
private fun handleRequestInventory(packet: RequestInventoryPacket) { private fun handleLocateStack(packet: LocateStackPacket) {
if (packet.kind != RequestInventoryPacket.Kind.GROUPED) { getInventory()?.also { inv ->
return val transaction = Transaction.openOuter()
} val amount = inv.simulateExtract(ItemVariant.of(packet.stack), Long.MAX_VALUE, transaction);
getInventory()?.also { inv -> transaction.close()
sendPacket(ReadGroupedInventoryPacket(inv, ipAddress, packet.source)) sendPacket(StackLocationPacket(packet.stack, amount.toInt(), this, ipAddress, packet.source))
} }
} }
private fun handleLocateStack(packet: LocateStackPacket) { private fun handleExtractStack(packet: ExtractStackPacket) {
getInventory()?.also { inv -> getInventory()?.also { inv ->
val amount = inv.getAmount(packet.stack) var remaining = packet.amount
sendPacket(StackLocationPacket(packet.stack, amount, this, ipAddress, packet.source)) while (remaining > 0) {
} val toExtract = min(remaining, packet.stack.maxCount)
} val transaction = Transaction.openOuter()
val extracted = inv.extract(ItemVariant.of(packet.stack), toExtract.toLong(), transaction)
transaction.commit()
remaining -= extracted.toInt()
sendPacket(ItemStackPacket(packet.stack.copyWithCount(extracted.toInt()), ipAddress, packet.source))
}
}
}
private fun handleExtractStack(packet: ExtractStackPacket) { private fun handleCheckCapacity(packet: CheckCapacityPacket) {
getInventory()?.also { inv -> getInventory()?.also { inv ->
var amount = packet.amount val transaction = Transaction.openOuter()
while (amount > 0) { val inserted = inv.simulateInsert(ItemVariant.of(packet.stack), packet.stack.count.toLong(), transaction)
val extracted = inv.extract(packet.stack, min(amount, packet.stack.maxCount)) transaction.close()
if (extracted.isEmpty) { sendPacket(CapacityPacket(packet.stack, inserted.toInt(), this, ipAddress, packet.source))
break }
} else { }
amount -= extracted.count
sendPacket(ItemStackPacket(extracted, ipAddress, packet.source))
}
}
}
}
private fun handleCheckCapacity(packet: CheckCapacityPacket) { override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
getInventory()?.also { inv -> val inventory = getInventory()
val remaining = inv.attemptInsertion(packet.stack, Simulation.SIMULATE) if (inventory != null) {
val couldAccept = packet.stack.count - remaining.count val transaction = Transaction.openOuter()
sendPacket(CapacityPacket(packet.stack, couldAccept, this, ipAddress, packet.source)) val inserted = inventory.insert(ItemVariant.of(packet.stack), packet.stack.count.toLong(), transaction)
} transaction.commit()
} val remaining = packet.stack.count - inserted.toInt()
return packet.stack.copyWithCount(remaining)
} else {
return packet.stack
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { override fun toCommonTag(tag: NbtCompound) {
val inventory = getInventory() super.toCommonTag(tag)
if (inventory != null) { writeDeviceConfiguration(tag)
val remaining = inventory.insert(packet.stack) }
// whatever could not be inserted will be sent back to the packet's source
return remaining
} else {
return packet.stack
}
}
override fun toCommonTag(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
super.toCommonTag(tag) super.fromCommonTag(tag)
writeDeviceConfiguration(tag) loadDeviceConfiguration(tag)
} }
override fun fromCommonTag(tag: NbtCompound) { override fun writeDeviceConfiguration(tag: NbtCompound) {
super.fromCommonTag(tag) tag.putInt("ProviderPriority", providerPriority)
loadDeviceConfiguration(tag) tag.putInt("ReceiverPriority", receiverPriority)
} tag.putBoolean("SyncPriorities", syncPriorities)
}
override fun writeDeviceConfiguration(tag: NbtCompound) { override fun loadDeviceConfiguration(tag: NbtCompound) {
tag.putInt("ProviderPriority", providerPriority) providerPriority = tag.getInt("ProviderPriority")
tag.putInt("ReceiverPriority", receiverPriority) receiverPriority = tag.getInt("ReceiverPriority")
tag.putBoolean("SyncPriorities", syncPriorities) syncPriorities = tag.getBoolean("SyncPriorities")
} }
override fun loadDeviceConfiguration(tag: NbtCompound) {
providerPriority = tag.getInt("ProviderPriority")
receiverPriority = tag.getInt("ReceiverPriority")
syncPriorities = tag.getBoolean("SyncPriorities")
}
} }

View File

@ -1,7 +1,5 @@
package net.shadowfacts.phycon.block.netswitch package net.shadowfacts.phycon.block.netswitch
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.block.Material import net.minecraft.block.Material
import net.minecraft.block.entity.BlockEntity import net.minecraft.block.entity.BlockEntity
@ -11,7 +9,6 @@ import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World import net.minecraft.world.World
import net.minecraft.world.WorldAccess import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
@ -23,39 +20,43 @@ import java.util.*
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class SwitchBlock: BlockWithEntity<SwitchBlockEntity>( class SwitchBlock : BlockWithEntity<SwitchBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
), ),
NetworkComponentBlock, NetworkComponentBlock {
AttributeProvider {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "switch") val ID = Identifier(PhysicalConnectivity.MODID, "switch")
} }
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> { override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.allOf(Direction::class.java) return EnumSet.allOf(Direction::class.java)
} }
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { override fun getNetworkInterfaceForSide(
return getBlockEntity(world, pos)?.interfaces?.find { it.side == side } side: Direction,
} state: BlockState,
world: WorldAccess,
pos: BlockPos
): Interface? {
return getBlockEntity(world, pos)?.interfaces?.find { it.side == side }
}
override fun createBlockEntity(pos: BlockPos, state: BlockState) = SwitchBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = SwitchBlockEntity(pos, state)
override fun <T: BlockEntity> getTicker(world: World, state: BlockState, type: BlockEntityType<T>): BlockEntityTicker<T>? { override fun <T : BlockEntity> getTicker(
return if (world.isClient) { world: World,
null state: BlockState,
} else { type: BlockEntityType<T>
BlockEntityTicker { world, blockPos, blockState, blockEntity -> ): BlockEntityTicker<T>? {
(blockEntity as SwitchBlockEntity).tick() return if (world.isClient) {
} null
} } else {
} BlockEntityTicker { world, blockPos, blockState, blockEntity ->
(blockEntity as SwitchBlockEntity).tick()
override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) { }
to.offer(getBlockEntity(world, pos)) }
} }
} }

View File

@ -20,6 +20,7 @@ import net.shadowfacts.phycon.frame.BasePacketFrame
import net.shadowfacts.phycon.frame.NetworkSplitFrame import net.shadowfacts.phycon.frame.NetworkSplitFrame
import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.ItemStackPacket import net.shadowfacts.phycon.packet.ItemStackPacket
import net.shadowfacts.phycon.util.IntRingBuffer
import net.shadowfacts.phycon.util.NetworkUtil import net.shadowfacts.phycon.util.NetworkUtil
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Deque import java.util.Deque
@ -28,155 +29,188 @@ import java.util.LinkedList
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockEntities.SWITCH, pos, state) { class SwitchBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(PhyBlockEntities.SWITCH, pos, state) {
companion object { companion object {
var SWITCHING_CAPACITY = 256 // 256 packets/tick var SWITCHING_CAPACITY = 256 // 256 packets/tick
} }
val interfaces = Direction.values().map { SwitchInterface(it, WeakReference(this), MACAddress.random()) } val interfaces = Direction.values().map { SwitchInterface(it, WeakReference(this), MACAddress.random()) }
private val macTable = mutableMapOf<MACAddress, Direction>() private val macTable = mutableMapOf<MACAddress, Direction>()
private val destinationCache = Array<WeakReference<Interface>?>(6) { null } private val destinationCache = Array<WeakReference<Interface>?>(6) { null }
private var packetsHandledThisTick = 0 val packetStatistics = IntRingBuffer(60) // 1 minute's worth
private var delayedPackets: Deque<Pair<PacketFrame, SwitchInterface>> = LinkedList() private val currentSecondPacketStatistics = IntRingBuffer(20)
private var packetsHandledThisTick = 0
private var delayedPackets: Deque<Pair<PacketFrame, SwitchInterface>> = LinkedList()
fun interfaceForSide(side: Direction): SwitchInterface { var statisticsObserver: (() -> Unit)? = null
return interfaces.find { it.side == side }!!
}
private fun handle(frame: EthernetFrame, fromItf: SwitchInterface) { fun interfaceForSide(side: Direction): SwitchInterface {
macTable[frame.source] = fromItf.side return interfaces.find { it.side == side }!!
}
if (frame is PacketFrame) { private fun handle(frame: EthernetFrame, fromItf: SwitchInterface) {
if (packetsHandledThisTick > SWITCHING_CAPACITY) { macTable[frame.source] = fromItf.side
PhysicalConnectivity.NETWORK_LOGGER.debug("{} reached capacity, delaying forwarding {}", this, frame)
delayedPackets.addLast(frame to fromItf)
return
} else {
packetsHandledThisTick++
}
}
resend(frame, fromItf) if (frame is PacketFrame) {
} if (packetsHandledThisTick > SWITCHING_CAPACITY) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} reached capacity, delaying forwarding {}", this, frame)
delayedPackets.addLast(frame to fromItf)
return
} else {
packetsHandledThisTick++
}
}
private fun resend(frame: EthernetFrame, fromItf: SwitchInterface) { resend(frame, fromItf)
if (frame.destination.type != MACAddress.Type.BROADCAST && macTable.containsKey(frame.destination)) { }
val dir = macTable[frame.destination]!!
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) forwarding {} to side {}", this, fromItf.side, fromItf.macAddress, frame, dir)
interfaceForSide(dir).send(frame)
} else {
flood(frame, fromItf)
}
}
private fun flood(frame: EthernetFrame, source: SwitchInterface) { private fun resend(frame: EthernetFrame, fromItf: SwitchInterface) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) flooding {}", this, source.side, source.macAddress, frame) if (frame.destination.type != MACAddress.Type.BROADCAST && macTable.containsKey(frame.destination)) {
for (itf in interfaces) { val dir = macTable[frame.destination]!!
if (source == itf) continue PhysicalConnectivity.NETWORK_LOGGER.debug(
itf.send(frame) "{} ({}, {}) forwarding {} to side {}",
} this,
} fromItf.side,
fromItf.macAddress,
frame,
dir
)
interfaceForSide(dir).send(frame)
} else {
flood(frame, fromItf)
}
}
private fun findDestination(fromItf: Interface): Interface? { private fun flood(frame: EthernetFrame, source: SwitchInterface) {
val side = (fromItf as SwitchInterface).side PhysicalConnectivity.NETWORK_LOGGER.debug(
return destinationCache[side.ordinal]?.get() "{} ({}, {}) flooding {}",
?: NetworkUtil.findConnectedInterface(world!!, pos, side)?.also { this,
destinationCache[side.ordinal] = WeakReference(it) source.side,
} source.macAddress,
} frame
)
for (itf in interfaces) {
if (source == itf) continue
itf.send(frame)
}
}
private fun cableDisconnected(itf: SwitchInterface) { private fun findDestination(fromItf: Interface): Interface? {
macTable.entries.filter { val side = (fromItf as SwitchInterface).side
it.value == itf.side return destinationCache[side.ordinal]?.get()
}.forEach { ?: NetworkUtil.findConnectedInterface(world!!, pos, side)?.also {
macTable.remove(it.key) destinationCache[side.ordinal] = WeakReference(it)
} }
destinationCache[itf.side.ordinal] = null }
flood(NetworkSplitFrame(itf.macAddress), itf)
}
fun tick() { private fun cableDisconnected(itf: SwitchInterface) {
packetsHandledThisTick = 0 macTable.entries.filter {
it.value == itf.side
}.forEach {
macTable.remove(it.key)
}
destinationCache[itf.side.ordinal] = null
flood(NetworkSplitFrame(itf.macAddress), itf)
}
while (delayedPackets.isNotEmpty() && packetsHandledThisTick <= SWITCHING_CAPACITY) { fun tick() {
val (frame, fromItf) = delayedPackets.pop() if (statisticsObserver != null) {
resend(frame, fromItf) if (currentSecondPacketStatistics.size == 20) {
} packetStatistics.add(currentSecondPacketStatistics.sum())
} currentSecondPacketStatistics.clear()
statisticsObserver?.invoke()
} else {
currentSecondPacketStatistics.add(packetsHandledThisTick)
}
}
override fun writeNbt(tag: NbtCompound) { packetsHandledThisTick = 0
super.writeNbt(tag)
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address }) while (delayedPackets.isNotEmpty() && packetsHandledThisTick <= SWITCHING_CAPACITY) {
val list = NbtList() val (frame, fromItf) = delayedPackets.pop()
for ((frame, fromItf) in delayedPackets) { resend(frame, fromItf)
val packet = frame.packet }
if (packet !is ItemStackPacket) continue }
val compound = NbtCompound()
compound.putInt("FromItfSide", fromItf.side.ordinal)
compound.putInt("SourceIP", packet.source.address)
compound.putInt("DestinationIP", packet.destination.address)
compound.putLong("SourceMAC", frame.source.address)
compound.putLong("DestinationMAC", frame.destination.address)
compound.put("Stack", packet.stack.writeNbt(NbtCompound()))
list.add(compound)
}
tag.put("DelayedStackPackets", list)
}
override fun readNbt(tag: NbtCompound) { override fun writeNbt(tag: NbtCompound) {
super.readNbt(tag) super.writeNbt(tag)
tag.getLongArray("InterfaceAddresses")?.forEachIndexed { i, l -> tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
interfaces[i].macAddress = MACAddress(l) val list = NbtList()
} for ((frame, fromItf) in delayedPackets) {
tag.getList("DelayedStackPackets", 10).forEach { it -> val packet = frame.packet
val compound = it as NbtCompound if (packet !is ItemStackPacket) continue
val fromItfSide = Direction.values()[compound.getInt("FromItfSide")] val compound = NbtCompound()
val fromItf = interfaces.find { it.side == fromItfSide }!! compound.putInt("FromItfSide", fromItf.side.ordinal)
val sourceIP = IPAddress(compound.getInt("SourceIP")) compound.putInt("SourceIP", packet.source.address)
val destinationIP = IPAddress(compound.getInt("DestinationIP")) compound.putInt("DestinationIP", packet.destination.address)
val sourceMAC = MACAddress(compound.getLong("SourceMAC")) compound.putLong("SourceMAC", frame.source.address)
val destinationMAC = MACAddress(compound.getLong("DestinationMAC")) compound.putLong("DestinationMAC", frame.destination.address)
val stack = ItemStack.fromNbt(compound.getCompound("Stack")) compound.put("Stack", packet.stack.writeNbt(NbtCompound()))
if (!stack.isEmpty) { list.add(compound)
val packet = ItemStackPacket(stack, sourceIP, destinationIP) }
val frame = BasePacketFrame(packet, sourceMAC, destinationMAC) tag.put("DelayedStackPackets", list)
delayedPackets.addLast(frame to fromItf) }
}
}
}
override fun toUpdatePacket(): Packet<ClientPlayPacketListener>? { override fun readNbt(tag: NbtCompound) {
return BlockEntityUpdateS2CPacket.create(this) super.readNbt(tag)
}
override fun toInitialChunkDataNbt(): NbtCompound { tag.getLongArray("InterfaceAddresses")?.forEachIndexed { i, l ->
val tag = NbtCompound() interfaces[i].macAddress = MACAddress(l)
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address }) }
return tag tag.getList("DelayedStackPackets", 10).forEach { it ->
} val compound = it as NbtCompound
val fromItfSide = Direction.values()[compound.getInt("FromItfSide")]
val fromItf = interfaces.find { it.side == fromItfSide }!!
val sourceIP = IPAddress(compound.getInt("SourceIP"))
val destinationIP = IPAddress(compound.getInt("DestinationIP"))
val sourceMAC = MACAddress(compound.getLong("SourceMAC"))
val destinationMAC = MACAddress(compound.getLong("DestinationMAC"))
val stack = ItemStack.fromNbt(compound.getCompound("Stack"))
if (!stack.isEmpty) {
val packet = ItemStackPacket(stack, sourceIP, destinationIP)
val frame = BasePacketFrame(packet, sourceMAC, destinationMAC)
delayedPackets.addLast(frame to fromItf)
}
}
tag.getIntArray("PacketStatistics")?.also { statistics ->
if (statistics.isNotEmpty()) {
packetStatistics.replace(statistics)
}
}
}
class SwitchInterface( override fun toUpdatePacket(): Packet<ClientPlayPacketListener>? {
val side: Direction, return BlockEntityUpdateS2CPacket.create(this)
val switch: WeakReference<SwitchBlockEntity>, }
@JvmField var macAddress: MACAddress,
): Interface {
override fun getMACAddress() = macAddress
override fun receive(frame: EthernetFrame) { override fun toInitialChunkDataNbt(): NbtCompound {
switch.get()?.handle(frame, this) val tag = NbtCompound()
} tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
tag.putIntArray("PacketStatistics", packetStatistics.asContiguousArray())
return tag
}
override fun send(frame: EthernetFrame) { class SwitchInterface(
switch.get()?.findDestination(this)?.receive(frame) val side: Direction,
} val switch: WeakReference<SwitchBlockEntity>,
@JvmField var macAddress: MACAddress,
) : Interface {
override fun getMACAddress() = macAddress
override fun cableDisconnected() { override fun receive(frame: EthernetFrame) {
switch.get()?.cableDisconnected(this) switch.get()?.handle(frame, this)
} }
}
override fun send(frame: EthernetFrame) {
switch.get()?.findDestination(this)?.receive(frame)
}
override fun cableDisconnected() {
switch.get()?.cableDisconnected(this)
}
}
} }

View File

@ -0,0 +1,100 @@
package net.shadowfacts.phycon.block.netswitch
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.shadowfacts.cacao.CacaoHandledScreen
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.kiwidsl.dsl
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class SwitchConsoleScreen(
handler: SwitchConsoleScreenHandler,
playerInventory: PlayerInventory,
title: Text,
) : CacaoHandledScreen<SwitchConsoleScreenHandler>(
handler,
playerInventory,
title,
) {
val root = SwitchConsoleViewController(handler.switch)
init {
addWindow(ScreenHandlerWindow(handler, root))
}
override fun shouldPause() = false
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW.GLFW_KEY_E) {
close()
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
}
class SwitchConsoleViewController(val switch: SwitchBlockEntity) : ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val stats = SwitchPacketStatisticsView(switch)
view.addSubview(stats)
view.solver.dsl {
stats.centerXAnchor equalTo (view.centerXAnchor + 50)
stats.centerYAnchor equalTo (view.centerYAnchor + 50)
}
}
}
class SwitchPacketStatisticsView(val switch: SwitchBlockEntity) : View() {
init {
intrinsicContentSize = Size(180.0, 90.0)
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.fill(matrixStack, bounds, Color.BLACK)
if (switch.packetStatistics.size == 0) {
return
}
// TODO: drawLine isn't working for some reason
RenderHelper.drawLine(
Point(bounds.left, bounds.top),
Point(bounds.right, bounds.bottom),
1.0,
2f,
Color.MAGENTA
)
return
val maxPackets = switch.packetStatistics.maxOf { it }
val maxDataPointsCount = 60
var lastPoint: Point? = null
val size = Size(3.0, 3.0)
for ((index, packets) in switch.packetStatistics.withIndex()) {
val x = (1 - (switch.packetStatistics.size - index).toDouble() / maxDataPointsCount) * bounds.width
val y = (1 - (packets.toDouble() / maxPackets)) * (bounds.height)
val point = Point(x, y)
if (lastPoint != null) {
// RenderHelper.fill(matrixStack, Rect(lastPoint, 3.0, 3.0), Color.RED)
RenderHelper.drawLine(lastPoint, point, 1.0, 2f, Color.RED)
}
lastPoint = point
}
}
}

View File

@ -0,0 +1,46 @@
package net.shadowfacts.phycon.block.netswitch
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
/**
* @author shadowfacts
*/
class SwitchConsoleScreenHandler(
syncId: Int,
val switch: SwitchBlockEntity,
) : ScreenHandler(PhyScreens.SWITCH_CONSOLE, syncId) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "switch_console")
}
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf) :
this(
syncId,
PhyBlocks.SWITCH.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
)
init {
switch.statisticsObserver = {
switch.world!!.updateListeners(switch.pos, switch.cachedState, switch.cachedState, 3)
}
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
switch.statisticsObserver = null
}
}

View File

@ -12,10 +12,10 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class P2PInterfaceBlock: FaceDeviceBlock<P2PInterfaceBlockEntity>( class P2PInterfaceBlock : FaceDeviceBlock<P2PInterfaceBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
@ -24,12 +24,12 @@ class P2PInterfaceBlock: FaceDeviceBlock<P2PInterfaceBlockEntity>(
override val faceThickness = 4.0 override val faceThickness = 4.0
override val faceShapes = mapOf( override val faceShapes = mapOf(
Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 4.0, 16.0), Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 4.0, 16.0),
Direction.UP to createCuboidShape(0.0, 12.0, 0.0, 16.0, 16.0, 16.0), Direction.UP to createCuboidShape(0.0, 12.0, 0.0, 16.0, 16.0, 16.0),
Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 4.0), Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 4.0),
Direction.SOUTH to createCuboidShape(0.0, 0.0, 12.0, 16.0, 16.0, 16.0), Direction.SOUTH to createCuboidShape(0.0, 0.0, 12.0, 16.0, 16.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 4.0, 16.0, 16.0), Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 4.0, 16.0, 16.0),
Direction.EAST to createCuboidShape(12.0, 0.0, 0.0, 16.0, 16.0, 16.0) Direction.EAST to createCuboidShape(12.0, 0.0, 0.0, 16.0, 16.0, 16.0)
) )
override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PInterfaceBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PInterfaceBlockEntity(pos, state)

View File

@ -15,7 +15,8 @@ import net.shadowfacts.phycon.packet.RequestInventoryPacket
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class P2PInterfaceBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.P2P_INTERFACE, pos, state) { class P2PInterfaceBlockEntity(pos: BlockPos, state: BlockState) :
DeviceBlockEntity(PhyBlockEntities.P2P_INTERFACE, pos, state) {
private var inventory: Storage<ItemVariant>? = null private var inventory: Storage<ItemVariant>? = null

View File

@ -16,10 +16,10 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class P2PReceiverBlock: FaceDeviceBlock<P2PReceiverBlockEntity>( class P2PReceiverBlock : FaceDeviceBlock<P2PReceiverBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
@ -28,12 +28,12 @@ class P2PReceiverBlock: FaceDeviceBlock<P2PReceiverBlockEntity>(
override val faceThickness = 4.0 override val faceThickness = 4.0
override val faceShapes = mapOf( override val faceShapes = mapOf(
Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 4.0, 16.0), Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 4.0, 16.0),
Direction.UP to createCuboidShape(0.0, 12.0, 0.0, 16.0, 16.0, 16.0), Direction.UP to createCuboidShape(0.0, 12.0, 0.0, 16.0, 16.0, 16.0),
Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 4.0), Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 4.0),
Direction.SOUTH to createCuboidShape(0.0, 0.0, 12.0, 16.0, 16.0, 16.0), Direction.SOUTH to createCuboidShape(0.0, 0.0, 12.0, 16.0, 16.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 4.0, 16.0, 16.0), Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 4.0, 16.0, 16.0),
Direction.EAST to createCuboidShape(12.0, 0.0, 0.0, 16.0, 16.0, 16.0) Direction.EAST to createCuboidShape(12.0, 0.0, 0.0, 16.0, 16.0, 16.0)
) )
override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PReceiverBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PReceiverBlockEntity(pos, state)

View File

@ -21,7 +21,8 @@ import java.lang.ref.WeakReference
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class P2PReceiverBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.P2P_RECEIVER, pos, state), class P2PReceiverBlockEntity(pos: BlockPos, state: BlockState) :
DeviceBlockEntity(PhyBlockEntities.P2P_RECEIVER, pos, state),
ClientConfigurableDevice { ClientConfigurableDevice {
enum class Status { enum class Status {

View File

@ -18,58 +18,65 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>( class RedstoneControllerBlock : FaceDeviceBlock<RedstoneControllerBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "redstone_controller") val ID = Identifier(PhysicalConnectivity.MODID, "redstone_controller")
val POWERED = Properties.POWERED val POWERED = Properties.POWERED
} }
override val faceThickness = 3.0 override val faceThickness = 3.0
override val faceShapes = mapOf( override val faceShapes = mapOf(
Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 3.0, 16.0), Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 3.0, 16.0),
Direction.UP to createCuboidShape(0.0, 13.0, 0.0, 16.0, 16.0, 16.0), Direction.UP to createCuboidShape(0.0, 13.0, 0.0, 16.0, 16.0, 16.0),
Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 3.0), Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 3.0),
Direction.SOUTH to createCuboidShape(0.0, 0.0, 13.0, 16.0, 16.0, 16.0), Direction.SOUTH to createCuboidShape(0.0, 0.0, 13.0, 16.0, 16.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 3.0, 16.0, 16.0), Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 3.0, 16.0, 16.0),
Direction.EAST to createCuboidShape(13.0, 0.0, 0.0, 16.0, 16.0, 16.0) Direction.EAST to createCuboidShape(13.0, 0.0, 0.0, 16.0, 16.0, 16.0)
) )
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) { override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder) super.appendProperties(builder)
builder.add(POWERED) builder.add(POWERED)
} }
override fun createBlockEntity(pos: BlockPos, state: BlockState) = RedstoneControllerBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = RedstoneControllerBlockEntity(pos, state)
override fun getPlacementState(context: ItemPlacementContext): BlockState { override fun getPlacementState(context: ItemPlacementContext): BlockState {
val state = super.getPlacementState(context) val state = super.getPlacementState(context)
return state.with(POWERED, isPowered(context.world, context.blockPos, state[FACING])) return state.with(POWERED, isPowered(context.world, context.blockPos, state[FACING]))
} }
override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighborBlock: Block, neighborPos: BlockPos, bl: Boolean) { override fun neighborUpdate(
// this can't be done in getStateForNeighborUpdate because getEmittedRedstonePower is defined in World not WorldAccess state: BlockState,
if (!world.isClient) { world: World,
val wasLit = state[POWERED] pos: BlockPos,
val isLit = isPowered(world, pos, state[FACING]) neighborBlock: Block,
if (wasLit != isLit) { neighborPos: BlockPos,
toggleLit(state, world, pos) bl: Boolean
} ) {
} // this can't be done in getStateForNeighborUpdate because getEmittedRedstonePower is defined in World not WorldAccess
} if (!world.isClient) {
val wasLit = state[POWERED]
val isLit = isPowered(world, pos, state[FACING])
if (wasLit != isLit) {
toggleLit(state, world, pos)
}
}
}
private fun isPowered(world: World, pos: BlockPos, facing: Direction): Boolean { private fun isPowered(world: World, pos: BlockPos, facing: Direction): Boolean {
val offset = pos.offset(facing) val offset = pos.offset(facing)
return world.getEmittedRedstonePower(offset, facing) > 0 return world.getEmittedRedstonePower(offset, facing) > 0
} }
private fun toggleLit(state: BlockState, world: World, pos: BlockPos) { private fun toggleLit(state: BlockState, world: World, pos: BlockPos) {
world.setBlockState(pos, state.cycle(POWERED), 2) world.setBlockState(pos, state.cycle(POWERED), 2)
getBlockEntity(world, pos)!!.redstoneStateChanged() getBlockEntity(world, pos)!!.redstoneStateChanged()
} }
} }

View File

@ -14,66 +14,67 @@ import net.shadowfacts.phycon.util.RedstoneMode
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class RedstoneControllerBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.REDSTONE_CONTROLLER, pos, state), class RedstoneControllerBlockEntity(pos: BlockPos, state: BlockState) :
ClientConfigurableDevice { DeviceBlockEntity(PhyBlockEntities.REDSTONE_CONTROLLER, pos, state),
ClientConfigurableDevice {
var managedDevices = Array<IPAddress?>(5) { null } var managedDevices = Array<IPAddress?>(5) { null }
var redstoneMode = RedstoneMode.HIGH var redstoneMode = RedstoneMode.HIGH
set(value) { set(value) {
field = value field = value
redstoneStateChanged() redstoneStateChanged()
} }
private var redstonePowered = false private var redstonePowered = false
override fun handle(packet: Packet) { override fun handle(packet: Packet) {
} }
fun redstoneStateChanged() { fun redstoneStateChanged() {
if (world == null || world!!.isClient) return if (world == null || world!!.isClient) return
val oldPowered = redstonePowered val oldPowered = redstonePowered
redstonePowered = cachedState[RedstoneControllerBlock.POWERED] redstonePowered = cachedState[RedstoneControllerBlock.POWERED]
val mode: RemoteActivationPacket.Mode? = when (redstoneMode) { val mode: RemoteActivationPacket.Mode? = when (redstoneMode) {
RedstoneMode.TOGGLE -> if (oldPowered != redstonePowered) RemoteActivationPacket.Mode.SINGLE else null RedstoneMode.TOGGLE -> if (oldPowered != redstonePowered) RemoteActivationPacket.Mode.SINGLE else null
RedstoneMode.RISING_EDGE -> if (!oldPowered && redstonePowered) RemoteActivationPacket.Mode.SINGLE else null RedstoneMode.RISING_EDGE -> if (!oldPowered && redstonePowered) RemoteActivationPacket.Mode.SINGLE else null
RedstoneMode.FALLING_EDGE -> if (oldPowered && !redstonePowered) RemoteActivationPacket.Mode.SINGLE else null RedstoneMode.FALLING_EDGE -> if (oldPowered && !redstonePowered) RemoteActivationPacket.Mode.SINGLE else null
RedstoneMode.HIGH -> if (redstonePowered) RemoteActivationPacket.Mode.ENABLE else RemoteActivationPacket.Mode.DISABLE RedstoneMode.HIGH -> if (redstonePowered) RemoteActivationPacket.Mode.ENABLE else RemoteActivationPacket.Mode.DISABLE
RedstoneMode.LOW -> if (redstonePowered) RemoteActivationPacket.Mode.DISABLE else RemoteActivationPacket.Mode.ENABLE RedstoneMode.LOW -> if (redstonePowered) RemoteActivationPacket.Mode.DISABLE else RemoteActivationPacket.Mode.ENABLE
} }
if (mode != null) { if (mode != null) {
sendActivatePacket(mode) sendActivatePacket(mode)
} }
} }
private fun sendActivatePacket(mode: RemoteActivationPacket.Mode) { private fun sendActivatePacket(mode: RemoteActivationPacket.Mode) {
for (ip in managedDevices) { for (ip in managedDevices) {
if (ip == null) continue if (ip == null) continue
sendPacket(RemoteActivationPacket(mode, ipAddress, ip)) sendPacket(RemoteActivationPacket(mode, ipAddress, ip))
} }
} }
override fun toCommonTag(tag: NbtCompound) { override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag) super.toCommonTag(tag)
writeDeviceConfiguration(tag) writeDeviceConfiguration(tag)
} }
override fun fromCommonTag(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag) super.fromCommonTag(tag)
loadDeviceConfiguration(tag) loadDeviceConfiguration(tag)
} }
override fun writeDeviceConfiguration(tag: NbtCompound) { override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address }) tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address })
tag.putString("RedstoneMode", redstoneMode.name) tag.putString("RedstoneMode", redstoneMode.name)
} }
override fun loadDeviceConfiguration(tag: NbtCompound) { override fun loadDeviceConfiguration(tag: NbtCompound) {
val addresses = tag.getIntArray("ManagedDevices") val addresses = tag.getIntArray("ManagedDevices")
managedDevices = (0..4).map { if (it >= addresses.size) null else IPAddress(addresses[it]) }.toTypedArray() managedDevices = (0..4).map { if (it >= addresses.size) null else IPAddress(addresses[it]) }.toTypedArray()
redstoneMode = RedstoneMode.valueOf(tag.getString("RedstoneMode")) redstoneMode = RedstoneMode.valueOf(tag.getString("RedstoneMode"))
} }
} }

View File

@ -24,65 +24,82 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class RedstoneEmitterBlock: FaceDeviceBlock<RedstoneEmitterBlockEntity>( class RedstoneEmitterBlock : FaceDeviceBlock<RedstoneEmitterBlockEntity>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
) { ) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "redstone_emitter") val ID = Identifier(PhysicalConnectivity.MODID, "redstone_emitter")
} }
// todo: don't just copy the redstone controller // todo: don't just copy the redstone controller
override val faceThickness = 3.0 override val faceThickness = 3.0
override val faceShapes = mapOf( override val faceShapes = mapOf(
Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 3.0, 16.0), Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 3.0, 16.0),
Direction.UP to createCuboidShape(0.0, 13.0, 0.0, 16.0, 16.0, 16.0), Direction.UP to createCuboidShape(0.0, 13.0, 0.0, 16.0, 16.0, 16.0),
Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 3.0), Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 3.0),
Direction.SOUTH to createCuboidShape(0.0, 0.0, 13.0, 16.0, 16.0, 16.0), Direction.SOUTH to createCuboidShape(0.0, 0.0, 13.0, 16.0, 16.0, 16.0),
Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 3.0, 16.0, 16.0), Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 3.0, 16.0, 16.0),
Direction.EAST to createCuboidShape(13.0, 0.0, 0.0, 16.0, 16.0, 16.0) Direction.EAST to createCuboidShape(13.0, 0.0, 0.0, 16.0, 16.0, 16.0)
) )
override fun createBlockEntity(pos: BlockPos, state: BlockState) = RedstoneEmitterBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = RedstoneEmitterBlockEntity(pos, state)
override fun emitsRedstonePower(state: BlockState): Boolean { override fun emitsRedstonePower(state: BlockState): Boolean {
return true return true
} }
override fun getStrongRedstonePower(state: BlockState, world: BlockView, pos: BlockPos, receivingSide: Direction): Int { override fun getStrongRedstonePower(
return if (receivingSide.opposite == state[FACING]) { state: BlockState,
getBlockEntity(world, pos)!!.cachedEmittedPower world: BlockView,
} else { pos: BlockPos,
0 receivingSide: Direction
} ): Int {
} return if (receivingSide.opposite == state[FACING]) {
getBlockEntity(world, pos)!!.cachedEmittedPower
} else {
0
}
}
override fun getWeakRedstonePower(state: BlockState, world: BlockView, pos: BlockPos, receivingSide: Direction): Int { override fun getWeakRedstonePower(
return getStrongRedstonePower(state, world, pos, receivingSide) state: BlockState,
} world: BlockView,
pos: BlockPos,
receivingSide: Direction
): Int {
return getStrongRedstonePower(state, world, pos, receivingSide)
}
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, result: BlockHitResult): ActionResult { override fun onUse(
if (!world.isClient) { state: BlockState,
val be = getBlockEntity(world, pos)!! world: World,
pos: BlockPos,
player: PlayerEntity,
hand: Hand,
result: BlockHitResult
): ActionResult {
if (!world.isClient) {
val be = getBlockEntity(world, pos)!!
be.markUpdate() be.markUpdate()
val factory = object: ExtendedScreenHandlerFactory { val factory = object : ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler { override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler {
return RedstoneEmitterScreenHandler(syncId, playerInv, be) return RedstoneEmitterScreenHandler(syncId, playerInv, be)
} }
override fun getDisplayName() = this@RedstoneEmitterBlock.name override fun getDisplayName() = this@RedstoneEmitterBlock.name
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(be.pos) buf.writeBlockPos(be.pos)
} }
} }
player.openHandledScreen(factory) player.openHandledScreen(factory)
} }
return ActionResult.SUCCESS return ActionResult.SUCCESS
} }
} }

View File

@ -1,6 +1,7 @@
package net.shadowfacts.phycon.block.redstone_emitter package net.shadowfacts.phycon.block.redstone_emitter
import alexiil.mc.lib.attributes.item.GroupedItemInvView import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import net.fabricmc.fabric.api.transfer.v1.storage.Storage
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
@ -12,124 +13,127 @@ import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.FaceDeviceBlock import net.shadowfacts.phycon.block.FaceDeviceBlock
import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.DeviceRemovedPacket import net.shadowfacts.phycon.packet.DeviceRemovedPacket
import net.shadowfacts.phycon.packet.ReadGroupedInventoryPacket import net.shadowfacts.phycon.packet.ReadItemStoragePacket
import net.shadowfacts.phycon.packet.RequestInventoryPacket import net.shadowfacts.phycon.packet.RequestInventoryPacket
import net.shadowfacts.phycon.util.ClientConfigurableDevice import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.GhostInv import net.shadowfacts.phycon.util.GhostInv
import kotlin.math.round
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class RedstoneEmitterBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.REDSTONE_EMITTER, pos, state), class RedstoneEmitterBlockEntity(pos: BlockPos, state: BlockState) :
ClientConfigurableDevice, DeviceBlockEntity(PhyBlockEntities.REDSTONE_EMITTER, pos, state),
GhostInv { ClientConfigurableDevice,
GhostInv {
private val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>() private val inventoryCache = mutableMapOf<IPAddress, Storage<ItemVariant>>()
var cachedEmittedPower: Int = 0 var cachedEmittedPower: Int = 0
private set private set
var stackToMonitor: ItemStack = ItemStack.EMPTY var stackToMonitor: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack override var ghostSlotStack: ItemStack
get() = stackToMonitor get() = stackToMonitor
set(value) { stackToMonitor = value } set(value) {
var maxAmount = 64 stackToMonitor = value
var mode = Mode.ANALOG }
set(value) { var maxAmount = 64
field = value var mode = Mode.ANALOG
recalculateRedstone() set(value) {
} field = value
recalculateRedstone()
}
override fun handle(packet: Packet) { override fun handle(packet: Packet) {
when (packet) { when (packet) {
is ReadGroupedInventoryPacket -> handleReadInventory(packet) is ReadItemStoragePacket -> handleReadItemStorage(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet) is DeviceRemovedPacket -> handleDeviceRemoved(packet)
} }
} }
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) { private fun handleReadItemStorage(packet: ReadItemStoragePacket) {
inventoryCache[packet.source] = packet.inventory inventoryCache[packet.source] = packet.inventory
recalculateRedstone() recalculateRedstone()
} }
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source) inventoryCache.remove(packet.source)
recalculateRedstone() recalculateRedstone()
} }
override fun tick() { override fun tick() {
super.tick() super.tick()
if (!world!!.isClient && counter % 20 == 0L) { if (!world!!.isClient && counter % 20 == 0L) {
if (counter % 80 == 0L) { if (counter % 80 == 0L) {
updateInventories() updateInventories()
} else if (counter % 20 == 0L) { }
recalculateRedstone() recalculateRedstone()
} }
} }
}
private fun updateInventories() { private fun updateInventories() {
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress)) sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
} }
private fun recalculateRedstone() { private fun recalculateRedstone() {
if (world == null || world!!.isClient) return if (world == null || world!!.isClient) return
if (stackToMonitor.isEmpty) { if (stackToMonitor.isEmpty) {
cachedEmittedPower = 0 cachedEmittedPower = 0
updateWorld() updateWorld()
return return
} }
val networkAmount = inventoryCache.values.fold(0) { acc, inv -> val variant = ItemVariant.of(stackToMonitor)
acc + inv.getAmount(stackToMonitor) val networkAmount = inventoryCache.values.fold(0) { acc, inv ->
} acc + inv.simulateExtract(variant, Long.MAX_VALUE, null).toInt()
cachedEmittedPower = }
when (mode) { cachedEmittedPower =
Mode.ANALOG -> if (networkAmount == 0) { when (mode) {
0 Mode.ANALOG -> if (networkAmount == 0) {
} else { 0
1 + round(networkAmount / maxAmount.toDouble() * 14).toInt() } else {
} 1 + (networkAmount / maxAmount.toDouble() * 14).toInt()
Mode.DIGITAL -> if (networkAmount >= maxAmount) 15 else 0 }
}
updateWorld() Mode.DIGITAL -> if (networkAmount >= maxAmount) 15 else 0
} }
private fun updateWorld() { updateWorld()
world!!.updateNeighborsAlways(pos, cachedState.block) }
world!!.updateNeighborsAlways(pos.offset(cachedState[FaceDeviceBlock.FACING]), cachedState.block)
}
override fun toCommonTag(tag: NbtCompound) { private fun updateWorld() {
super.toCommonTag(tag) world!!.updateNeighborsAlways(pos, cachedState.block)
tag.putInt("CachedEmittedPower", cachedEmittedPower) world!!.updateNeighborsAlways(pos.offset(cachedState[FaceDeviceBlock.FACING]), cachedState.block)
tag.put("StackToMonitor", stackToMonitor.writeNbt(NbtCompound())) }
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) { override fun toCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag) super.toCommonTag(tag)
cachedEmittedPower = tag.getInt("CachedEmittedPower") tag.putInt("CachedEmittedPower", cachedEmittedPower)
stackToMonitor = ItemStack.fromNbt(tag.getCompound("StackToMonitor")) tag.put("StackToMonitor", stackToMonitor.writeNbt(NbtCompound()))
loadDeviceConfiguration(tag) writeDeviceConfiguration(tag)
} }
override fun writeDeviceConfiguration(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
tag.putInt("MaxAmount", maxAmount) super.fromCommonTag(tag)
tag.putString("Mode", mode.name) cachedEmittedPower = tag.getInt("CachedEmittedPower")
} stackToMonitor = ItemStack.fromNbt(tag.getCompound("StackToMonitor"))
loadDeviceConfiguration(tag)
}
override fun loadDeviceConfiguration(tag: NbtCompound) { override fun writeDeviceConfiguration(tag: NbtCompound) {
maxAmount = tag.getInt("MaxAmount") tag.putInt("MaxAmount", maxAmount)
mode = Mode.valueOf(tag.getString("Mode")) tag.putString("Mode", mode.name)
} }
enum class Mode { override fun loadDeviceConfiguration(tag: NbtCompound) {
ANALOG, DIGITAL; maxAmount = tag.getInt("MaxAmount")
mode = Mode.valueOf(tag.getString("Mode"))
}
val friendlyName = TranslatableText("gui.phycon.redstone_emitter_mode.${name.lowercase()}") enum class Mode {
} ANALOG, DIGITAL;
val friendlyName = TranslatableText("gui.phycon.redstone_emitter_mode.${name.lowercase()}")
}
} }

View File

@ -27,129 +27,134 @@ import kotlin.math.ceil
* @author shadowfacts * @author shadowfacts
*/ */
class RedstoneEmitterScreen( class RedstoneEmitterScreen(
handler: RedstoneEmitterScreenHandler, handler: RedstoneEmitterScreenHandler,
playerInv: PlayerInventory, playerInv: PlayerInventory,
title: Text title: Text
): CacaoHandledScreen<RedstoneEmitterScreenHandler>( ) : CacaoHandledScreen<RedstoneEmitterScreenHandler>(
handler, handler,
playerInv, playerInv,
title title
) { ) {
companion object { companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/redstone_emitter.png") val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/redstone_emitter.png")
} }
init { init {
backgroundWidth = 176 backgroundWidth = 176
backgroundHeight = 166 backgroundHeight = 166
addWindow(ScreenHandlerWindow(handler, ViewController(handler.emitter))) addWindow(ScreenHandlerWindow(handler, ViewController(handler.emitter)))
} }
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY) super.drawBackground(matrixStack, delta, mouseX, mouseY)
RenderSystem.setShader(GameRenderer::getPositionTexShader) RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND) RenderSystem.setShaderTexture(0, BACKGROUND)
val x = (width - backgroundWidth) / 2 val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2 val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight) drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
} }
class ViewController( class ViewController(
private val emitter: RedstoneEmitterBlockEntity, private val emitter: RedstoneEmitterBlockEntity,
): net.shadowfacts.cacao.viewcontroller.ViewController() { ) : net.shadowfacts.cacao.viewcontroller.ViewController() {
lateinit var halfLabel: Label lateinit var halfLabel: Label
lateinit var fullLabel: Label lateinit var fullLabel: Label
override fun viewDidLoad() { override fun viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
val title = Label(PhyBlocks.REDSTONE_EMITTER.name) val title = Label(PhyBlocks.REDSTONE_EMITTER.name)
title.textColor = Color.TEXT title.textColor = Color.TEXT
view.addSubview(title) view.addSubview(title)
val inv = Label(MinecraftClient.getInstance().player!!.inventory.displayName) val inv = Label(MinecraftClient.getInstance().player!!.inventory.displayName)
inv.textColor = Color.TEXT inv.textColor = Color.TEXT
view.addSubview(inv) view.addSubview(inv)
val field = NumberField(emitter.maxAmount) { val field = NumberField(emitter.maxAmount) {
if (it.number != null) { if (it.number != null) {
emitter.maxAmount = it.number!! emitter.maxAmount = it.number!!
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureDevice(emitter)) MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureDevice(emitter))
} }
updateLabelTexts() updateLabelTexts()
} }
field.validator = { it >= 0 } field.validator = { it >= 0 }
field.drawBackground = false field.drawBackground = false
view.addSubview(field) view.addSubview(field)
val hStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL, spacing = 2.0) val hStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL, spacing = 2.0)
view.addSubview(hStack) view.addSubview(hStack)
val zeroStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL)) val zeroStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
zeroStack.addArrangedSubview(Label(TranslatableText("gui.phycon.emitter.count", 0), textAlignment = Label.TextAlignment.CENTER)).apply { zeroStack.addArrangedSubview(
textColor = Color.TEXT Label(
} TranslatableText("gui.phycon.emitter.count", 0),
zeroStack.addArrangedSubview(View()) textAlignment = Label.TextAlignment.CENTER
zeroStack.addArrangedSubview(Label("0", textAlignment = Label.TextAlignment.CENTER)).apply { )
textColor = Color.RED ).apply {
} textColor = Color.TEXT
}
zeroStack.addArrangedSubview(View())
zeroStack.addArrangedSubview(Label("0", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED
}
val halfStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL)) val halfStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
halfLabel = halfStack.addArrangedSubview(Label("half", textAlignment = Label.TextAlignment.CENTER)).apply { halfLabel = halfStack.addArrangedSubview(Label("half", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT textColor = Color.TEXT
} }
halfStack.addArrangedSubview(View()) halfStack.addArrangedSubview(View())
halfStack.addArrangedSubview(Label("8", textAlignment = Label.TextAlignment.CENTER)).apply { halfStack.addArrangedSubview(Label("8", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED textColor = Color.RED
} }
val fullStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL)) val fullStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
fullLabel = fullStack.addArrangedSubview(Label("full", textAlignment = Label.TextAlignment.CENTER)).apply { fullLabel = fullStack.addArrangedSubview(Label("full", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT textColor = Color.TEXT
} }
fullStack.addArrangedSubview(View()) fullStack.addArrangedSubview(View())
fullStack.addArrangedSubview(Label("15", textAlignment = Label.TextAlignment.CENTER)).apply { fullStack.addArrangedSubview(Label("15", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED textColor = Color.RED
} }
updateLabelTexts() updateLabelTexts()
view.solver.dsl { view.solver.dsl {
val minX = Variable("minX") val minX = Variable("minX")
val minY = Variable("minY") val minY = Variable("minY")
minX equalTo ((view.widthAnchor - 176) / 2) minX equalTo ((view.widthAnchor - 176) / 2)
minY equalTo ((view.heightAnchor - 166) / 2) minY equalTo ((view.heightAnchor - 166) / 2)
title.leftAnchor equalTo (minX + 8) title.leftAnchor equalTo (minX + 8)
title.topAnchor equalTo (minY + 6) title.topAnchor equalTo (minY + 6)
inv.leftAnchor equalTo (minX + 8) inv.leftAnchor equalTo (minX + 8)
inv.topAnchor equalTo (minY + 72) inv.topAnchor equalTo (minY + 72)
field.widthAnchor equalTo 82 field.widthAnchor equalTo 82
field.heightAnchor equalTo 11 field.heightAnchor equalTo 11
field.leftAnchor equalTo (minX + 57) field.leftAnchor equalTo (minX + 57)
field.topAnchor equalTo (minY + 23) field.topAnchor equalTo (minY + 23)
hStack.centerXAnchor equalTo view.centerXAnchor hStack.centerXAnchor equalTo view.centerXAnchor
hStack.widthAnchor equalTo (176 - 4) hStack.widthAnchor equalTo (176 - 4)
hStack.topAnchor equalTo (field.bottomAnchor + 8) hStack.topAnchor equalTo (field.bottomAnchor + 8)
hStack.bottomAnchor equalTo inv.topAnchor hStack.bottomAnchor equalTo inv.topAnchor
zeroStack.widthAnchor equalTo halfStack.widthAnchor zeroStack.widthAnchor equalTo halfStack.widthAnchor
halfStack.widthAnchor equalTo fullStack.widthAnchor halfStack.widthAnchor equalTo fullStack.widthAnchor
} }
} }
private fun updateLabelTexts() { private fun updateLabelTexts() {
halfLabel.text = TranslatableText("gui.phycon.emitter.count", ceil(emitter.maxAmount / 2.0).toInt()) halfLabel.text = TranslatableText("gui.phycon.emitter.count", ceil(emitter.maxAmount / 2.0).toInt())
fullLabel.text = TranslatableText("gui.phycon.emitter.count", emitter.maxAmount) fullLabel.text = TranslatableText("gui.phycon.emitter.count", emitter.maxAmount)
window!!.layout() window!!.layout()
} }
} }
} }

View File

@ -18,59 +18,59 @@ import net.shadowfacts.phycon.util.copyWithCount
* @author shadowfacts * @author shadowfacts
*/ */
class RedstoneEmitterScreenHandler( class RedstoneEmitterScreenHandler(
syncId: Int, syncId: Int,
playerInv: PlayerInventory, playerInv: PlayerInventory,
val emitter: RedstoneEmitterBlockEntity, val emitter: RedstoneEmitterBlockEntity,
): ScreenHandler(PhyScreens.REDSTONE_EMITTER, syncId) { ) : ScreenHandler(PhyScreens.REDSTONE_EMITTER, syncId) {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "redstone_emitter") val ID = Identifier(PhysicalConnectivity.MODID, "redstone_emitter")
} }
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf) :
this( this(
syncId, syncId,
playerInv, playerInv,
PhyBlocks.REDSTONE_EMITTER.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! PhyBlocks.REDSTONE_EMITTER.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
) )
init { init {
// fake slot // fake slot
addSlot(GhostSlot(emitter, 31, 20)) addSlot(GhostSlot(emitter, 31, 20))
// player inv // player inv
for (y in 0 until 3) { for (y in 0 until 3) {
for (x in 0 until 9) { for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, 8 + x * 18, 84 + y * 18)) addSlot(Slot(playerInv, x + y * 9 + 9, 8 + x * 18, 84 + y * 18))
} }
} }
// hotbar // hotbar
for (x in 0 until 9) { for (x in 0 until 9) {
addSlot(Slot(playerInv, x, 8 + x * 18, 142)) addSlot(Slot(playerInv, x, 8 + x * 18, 142))
} }
} }
override fun canUse(player: PlayerEntity): Boolean { override fun canUse(player: PlayerEntity): Boolean {
return true return true
} }
override fun onSlotClick(slotId: Int, clickData: Int, slotActionType: SlotActionType, player: PlayerEntity) { override fun onSlotClick(slotId: Int, clickData: Int, slotActionType: SlotActionType, player: PlayerEntity) {
// fake slot // fake slot
if (slotId == 0) { if (slotId == 0) {
if (cursorStack.isEmpty) { if (cursorStack.isEmpty) {
emitter.stackToMonitor = ItemStack.EMPTY emitter.stackToMonitor = ItemStack.EMPTY
} else { } else {
emitter.stackToMonitor = cursorStack.copyWithCount(1) emitter.stackToMonitor = cursorStack.copyWithCount(1)
} }
} }
super.onSlotClick(slotId, clickData, slotActionType, player) super.onSlotClick(slotId, clickData, slotActionType, player)
} }
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
val slot = slots[slotId] val slot = slots[slotId]
emitter.stackToMonitor = slot.stack.copyWithCount(1) emitter.stackToMonitor = slot.stack.copyWithCount(1)
return ItemStack.EMPTY return ItemStack.EMPTY
} }
} }

View File

@ -1,7 +1,5 @@
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.Block
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.block.Material import net.minecraft.block.Material
@ -12,15 +10,11 @@ import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties import net.minecraft.state.property.Properties
import net.minecraft.util.ActionResult import net.minecraft.util.ActionResult
import net.minecraft.util.Hand 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.hit.BlockHitResult
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World import net.minecraft.world.World
import net.minecraft.world.WorldAccess import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlock import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet import java.util.EnumSet
@ -28,48 +22,50 @@ import java.util.EnumSet
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class AbstractTerminalBlock<T: AbstractTerminalBlockEntity>: DeviceBlock<T>( abstract class AbstractTerminalBlock<T : AbstractTerminalBlockEntity> : DeviceBlock<T>(
Settings.of(Material.METAL) Settings.of(Material.METAL)
.strength(1.5f) .strength(1.5f)
.sounds(BlockSoundGroup.METAL) .sounds(BlockSoundGroup.METAL)
), ),
NetworkComponentBlock, NetworkComponentBlock {
AttributeProvider {
companion object { companion object {
val FACING = Properties.FACING val FACING = Properties.FACING
} }
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> { override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.allOf(Direction::class.java) val set = EnumSet.allOf(Direction::class.java)
set.remove(state[FACING]) set.remove(state[FACING])
return set return set
} }
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) { override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder) super.appendProperties(builder)
builder.add(FACING) builder.add(FACING)
} }
override fun getPlacementState(context: ItemPlacementContext): BlockState { override fun getPlacementState(context: ItemPlacementContext): BlockState {
return defaultState.with(FACING, context.playerLookDirection.opposite) return defaultState.with(FACING, context.playerLookDirection.opposite)
} }
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult { override fun onUse(
getBlockEntity(world, pos)!!.onActivate(player) state: BlockState,
return ActionResult.SUCCESS 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) { override fun onStateReplaced(state: BlockState, world: World, pos: BlockPos, newState: BlockState, moved: Boolean) {
if (!state.isOf(newState.block)) { if (!state.isOf(newState.block)) {
val be = getBlockEntity(world, pos)!! val be = getBlockEntity(world, pos)!!
be.dropItems() be.dropItems()
super.onStateReplaced(state, world, pos, newState, moved) 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,8 +1,9 @@
package net.shadowfacts.phycon.block.terminal package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInvView import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import alexiil.mc.lib.attributes.item.ItemStackCollections import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import alexiil.mc.lib.attributes.item.ItemStackUtil import net.fabricmc.fabric.api.transfer.v1.storage.Storage
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntityType import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerEntity
@ -18,9 +19,9 @@ import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.block.DeviceBlockEntity import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.component.* import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.frame.NetworkSplitFrame
import net.shadowfacts.phycon.packet.* import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.NetworkUtil import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.util.equalsIgnoringAmount
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
import java.util.function.IntBinaryOperator import java.util.function.IntBinaryOperator
@ -30,254 +31,258 @@ import kotlin.properties.Delegates
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): DeviceBlockEntity(type, pos, state), abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState) :
InventoryChangedListener, DeviceBlockEntity(type, pos, state),
ItemStackPacketHandler, InventoryChangedListener,
NetworkStackDispatcher<AbstractTerminalBlockEntity.PendingInsertion> { ItemStackPacketHandler,
NetworkStackDispatcher<AbstractTerminalBlockEntity.PendingInsertion> {
companion object { companion object {
// the locate/insertion timeouts are only 1 tick because that's long enough to hear from every device on the network // 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 // 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 LOCATE_REQUEST_TIMEOUT: Long = 1 // ticks
val INSERTION_TIMEOUT: Long = 1 // ticks val INSERTION_TIMEOUT: Long = 1 // ticks
val REQUEST_INVENTORY_TIMEOUT: Long = 1 // ticks val REQUEST_INVENTORY_TIMEOUT: Long = 1 // ticks
} }
protected val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>() protected val inventoryCache = mutableMapOf<IPAddress, Storage<ItemVariant>>()
val internalBuffer = TerminalBufferInventory(18) val internalBuffer = TerminalBufferInventory(18)
protected val pendingRequests = LinkedList<StackLocateRequest>() protected val pendingRequests = LinkedList<StackLocateRequest>()
override val pendingInsertions = mutableListOf<PendingInsertion>() override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = INSERTION_TIMEOUT override val dispatchStackTimeout = INSERTION_TIMEOUT
val cachedNetItems = ItemStackCollections.intMap() val cachedNetItems = Object2IntOpenHashMap<ItemVariant>()
private var requestInventoryTimestamp: Long? = null private var requestInventoryTimestamp: Long? = null
// todo: multiple players could have the terminal open simultaneously // todo: multiple players could have the terminal open simultaneously
var netItemObserver: WeakReference<NetItemObserver>? = null var netItemObserver: WeakReference<NetItemObserver>? = null
init { init {
internalBuffer.addListener(this) internalBuffer.addListener(this)
} }
override fun findDestination(): Interface? { override fun findDestination(): Interface? {
for (dir in Direction.values()) { for (dir in Direction.values()) {
val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir) val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir)
if (itf != null) { if (itf != null) {
return itf return itf
} }
} }
return null return null
} }
override fun handleNetworkSplit() { override fun handleNetworkSplit() {
super.handleNetworkSplit() super.handleNetworkSplit()
inventoryCache.clear() inventoryCache.clear()
} }
override fun handle(packet: Packet) { override fun handle(packet: Packet) {
when (packet) { when (packet) {
is ReadGroupedInventoryPacket -> handleReadInventory(packet) is ReadItemStoragePacket -> handleReadItemStorage(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet) is DeviceRemovedPacket -> handleDeviceRemoved(packet)
is StackLocationPacket -> handleStackLocation(packet) is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet) is ItemStackPacket -> handleItemStack(packet)
is CapacityPacket -> handleCapacity(packet) is CapacityPacket -> handleCapacity(packet)
} }
} }
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) { private fun handleReadItemStorage(packet: ReadItemStoragePacket) {
inventoryCache[packet.source] = packet.inventory inventoryCache[packet.source] = packet.inventory
updateAndSync() updateAndSync()
} }
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source) inventoryCache.remove(packet.source)
updateAndSync() updateAndSync()
} }
private fun handleStackLocation(packet: StackLocationPacket) { private fun handleStackLocation(packet: StackLocationPacket) {
val request = pendingRequests.firstOrNull { val request = pendingRequests.firstOrNull {
ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) it.stack.equalsIgnoringAmount(packet.stack)
} }
if (request != null) { if (request != null) {
request.results.add(packet.amount to packet.stackProvider) request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) { if (request.isFinishable(counter)) {
stackLocateRequestCompleted(request) stackLocateRequestCompleted(request)
} }
} }
} }
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val mode = val mode =
if (packet.bounceCount > 0) { if (packet.bounceCount > 0) {
// if this stack bounced from an inventory, that means we previously tried to send it to the network, so retry // if this stack bounced from an inventory, that means we previously tried to send it to the network, so retry
TerminalBufferInventory.Mode.TO_NETWORK TerminalBufferInventory.Mode.TO_NETWORK
} else { } else {
TerminalBufferInventory.Mode.FROM_NETWORK TerminalBufferInventory.Mode.FROM_NETWORK
} }
val remaining = internalBuffer.insert(packet.stack, mode) val remaining = internalBuffer.insert(packet.stack, mode)
// this happens outside the normal update loop because by receiving the item stack packet // 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 // we "know" how much the count in the source inventory has changed
updateAndSync() updateAndSync()
return remaining return remaining
} }
protected fun updateAndSync() { protected fun updateAndSync() {
updateNetItems() updateNetItems()
// syncs the internal buffer to the client // syncs the internal buffer to the client
markUpdate() markUpdate()
// syncs the open container (if any) to the client // syncs the open container (if any) to the client
netItemObserver?.get()?.netItemsChanged() netItemObserver?.get()?.netItemsChanged()
} }
private fun updateNetItems() { private fun updateNetItems() {
cachedNetItems.clear() cachedNetItems.clear()
for (inventory in inventoryCache.values) { for (inventory in inventoryCache.values) {
for (stack in inventory.storedStacks) { val transaction = Transaction.openOuter()
val amount = inventory.getAmount(stack) for (view in inventory.iterator(transaction)) {
cachedNetItems.mergeInt(stack, amount, IntBinaryOperator { a, b -> a + b }) val amount = view.amount.toInt()
} cachedNetItems.mergeInt(view.resource, amount, IntBinaryOperator { a, b -> a + b })
} }
} transaction.close()
}
}
private fun beginInsertions() { private fun beginInsertions() {
if (world!!.isClient) return if (world!!.isClient) return
for (slot in 0 until internalBuffer.size()) { for (slot in 0 until internalBuffer.size()) {
if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue
if (pendingInsertions.any { it.bufferSlot == slot }) continue if (pendingInsertions.any { it.bufferSlot == slot }) continue
val stack = internalBuffer.getStack(slot) val stack = internalBuffer.getStack(slot)
dispatchItemStack(stack) { insertion -> dispatchItemStack(stack) { insertion ->
insertion.bufferSlot = slot insertion.bufferSlot = slot
} }
} }
} }
private fun finishPendingRequests() { private fun finishPendingRequests() {
if (world!!.isClient) return if (world!!.isClient) return
if (pendingRequests.isEmpty()) return if (pendingRequests.isEmpty()) return
val finishable = pendingRequests.filter { it.isFinishable(counter) } val finishable = pendingRequests.filter { it.isFinishable(counter) }
// stackLocateRequestCompleted removes the object from pendingRequests // stackLocateRequestCompleted removes the object from pendingRequests
finishable.forEach(::stackLocateRequestCompleted) finishable.forEach(::stackLocateRequestCompleted)
} }
override fun tick() { override fun tick() {
super.tick() super.tick()
if (!world!!.isClient) { if (!world!!.isClient) {
finishPendingRequests() finishPendingRequests()
finishTimedOutPendingInsertions() finishTimedOutPendingInsertions()
if (counter % 20 == 0L) { if (counter % 20 == 0L) {
beginInsertions() beginInsertions()
} }
if (requestInventoryTimestamp != null && (counter - requestInventoryTimestamp!!) >= REQUEST_INVENTORY_TIMEOUT) { if (requestInventoryTimestamp != null && (counter - requestInventoryTimestamp!!) >= REQUEST_INVENTORY_TIMEOUT) {
updateAndSync() updateAndSync()
requestInventoryTimestamp = null requestInventoryTimestamp = null
} }
} }
} }
open fun onActivate(player: PlayerEntity) { open fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) { if (!world!!.isClient) {
updateAndSync() updateAndSync()
inventoryCache.clear() inventoryCache.clear()
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress)) sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
requestInventoryTimestamp = counter requestInventoryTimestamp = counter
} }
} }
fun requestItem(stack: ItemStack, amount: Int = stack.count) { fun requestItem(stack: ItemStack, amount: Int = stack.count) {
val request = StackLocateRequest(stack, amount, counter) val request = StackLocateRequest(stack, amount, counter)
pendingRequests.add(request) pendingRequests.add(request)
// locate packets are sent immediately instead of being added to a queue // locate packets are sent immediately instead of being added to a queue
// otherwise the terminal UI feels sluggish and unresponsive // otherwise the terminal UI feels sluggish and unresponsive
sendPacket(LocateStackPacket(stack, ipAddress)) sendPacket(LocateStackPacket(stack, ipAddress))
} }
protected open fun stackLocateRequestCompleted(request: StackLocateRequest) { protected open fun stackLocateRequestCompleted(request: StackLocateRequest) {
pendingRequests.remove(request) pendingRequests.remove(request)
val sortedResults = request.results.toMutableList() val sortedResults = request.results.toMutableList()
sortedResults.sortWith { a, b -> sortedResults.sortWith { a, b ->
// sort results first by provider priority, and then by the count that it can provide // sort results first by provider priority, and then by the count that it can provide
if (a.second.providerPriority == b.second.providerPriority) { if (a.second.providerPriority == b.second.providerPriority) {
b.first - a.first b.first - a.first
} else { } else {
b.second.providerPriority - a.second.providerPriority b.second.providerPriority - a.second.providerPriority
} }
} }
var amountRequested = 0 var amountRequested = 0
while (amountRequested < request.amount && sortedResults.isNotEmpty()) { while (amountRequested < request.amount && sortedResults.isNotEmpty()) {
val (sourceAmount, sourceInterface) = sortedResults.removeAt(0) val (sourceAmount, sourceInterface) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, request.amount - amountRequested) val amountToRequest = min(sourceAmount, request.amount - amountRequested)
amountRequested += amountToRequest amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress)) sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress))
} }
} }
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter) override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun finishInsertion(insertion: PendingInsertion): ItemStack { override fun finishInsertion(insertion: PendingInsertion): ItemStack {
val remaining = super.finishInsertion(insertion) val remaining = super.finishInsertion(insertion)
internalBuffer.setStack(insertion.bufferSlot, remaining) internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets // as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateAndSync() updateAndSync()
// don't start a second insertion, since remaining will be dispatched at the next beginInsertions // don't start a second insertion, since remaining will be dispatched at the next beginInsertions
return ItemStack.EMPTY return ItemStack.EMPTY
} }
override fun onInventoryChanged(inv: Inventory) { override fun onInventoryChanged(inv: Inventory) {
if (inv == internalBuffer && world != null && !world!!.isClient) { if (inv == internalBuffer && world != null && !world!!.isClient) {
markUpdate() markUpdate()
} }
} }
open fun dropItems() { open fun dropItems() {
ItemScatterer.spawn(world, pos, internalBuffer) ItemScatterer.spawn(world, pos, internalBuffer)
} }
override fun toCommonTag(tag: NbtCompound) { override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag) super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag()) tag.put("InternalBuffer", internalBuffer.toTag())
} }
override fun fromCommonTag(tag: NbtCompound) { override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag) super.fromCommonTag(tag)
internalBuffer.fromTag(tag.getCompound("InternalBuffer")) internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
} }
interface NetItemObserver { interface NetItemObserver {
fun netItemsChanged() fun netItemsChanged()
} }
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) { class PendingInsertion(stack: ItemStack, timestamp: Long) :
var bufferSlot by Delegates.notNull<Int>() NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
} var bufferSlot by Delegates.notNull<Int>()
}
open class StackLocateRequest( open class StackLocateRequest(
val stack: ItemStack, val stack: ItemStack,
val amount: Int, val amount: Int,
val timestamp: Long, val timestamp: Long,
) { ) {
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf() var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
val totalResultAmount: Int val totalResultAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount } get() = results.fold(0) { acc, (amount, _) -> acc + amount }
fun isFinishable(currentTimestamp: Long): Boolean { fun isFinishable(currentTimestamp: Long): Boolean {
// we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to // we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to
// correctly sort by priority // correctly sort by priority
return currentTimestamp - timestamp >= LOCATE_REQUEST_TIMEOUT return currentTimestamp - timestamp >= LOCATE_REQUEST_TIMEOUT
} }
} }
} }

View File

@ -27,178 +27,197 @@ import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
abstract class AbstractTerminalScreen<BE: AbstractTerminalBlockEntity, T: AbstractTerminalScreenHandler<BE>>( abstract class AbstractTerminalScreen<BE : AbstractTerminalBlockEntity, T : AbstractTerminalScreenHandler<BE>>(
handler: T, handler: T,
playerInv: PlayerInventory, playerInv: PlayerInventory,
title: Text, title: Text,
val terminalBackgroundWidth: Int, val terminalBackgroundWidth: Int,
val terminalBackgroundHeight: Int, val terminalBackgroundHeight: Int,
): CacaoHandledScreen<T>(handler, playerInv, title) { ) : CacaoHandledScreen<T>(handler, playerInv, title) {
interface SearchQueryListener { interface SearchQueryListener {
fun terminalSearchQueryChanged(newValue: String) fun terminalSearchQueryChanged(newValue: String)
fun requestTerminalSearchFieldUpdate(): String? fun requestTerminalSearchFieldUpdate(): String?
} }
companion object { companion object {
var searchQueryListener: SearchQueryListener? = null var searchQueryListener: SearchQueryListener? = null
} }
abstract val backgroundTexture: Identifier abstract val backgroundTexture: Identifier
val terminalVC: AbstractTerminalViewController<*, *, *> val terminalVC: AbstractTerminalViewController<*, *, *>
var amountVC: TerminalRequestAmountViewController? = null var amountVC: TerminalRequestAmountViewController? = null
private var prevSearchQuery = "" private var prevSearchQuery = ""
var searchQuery = "" var searchQuery = ""
set(value) { set(value) {
field = value field = value
if (prevSearchQuery != value) { if (prevSearchQuery != value) {
searchQueryListener?.terminalSearchQueryChanged(value) searchQueryListener?.terminalSearchQueryChanged(value)
} }
prevSearchQuery = value prevSearchQuery = value
} }
var scrollPosition = 0.0 var scrollPosition = 0.0
init { init {
backgroundWidth = terminalBackgroundWidth backgroundWidth = terminalBackgroundWidth
backgroundHeight = terminalBackgroundHeight backgroundHeight = terminalBackgroundHeight
terminalVC = createViewController() terminalVC = createViewController()
addWindow(ScreenHandlerWindow(handler, terminalVC)) addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems() requestUpdatedItems()
} }
abstract fun createViewController(): AbstractTerminalViewController<*, *, *> abstract fun createViewController(): AbstractTerminalViewController<*, *, *>
fun requestItem(stack: ItemStack, amount: Int) { fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount) val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet) netHandler.sendPacket(packet)
} }
fun requestUpdatedItems() { fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!! val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat())) player.networkHandler.sendPacket(
} C2STerminalUpdateDisplayedItems(
handler.terminal,
searchQuery,
scrollPosition.toFloat()
)
)
}
private fun showRequestAmountDialog(stack: ItemStack) { private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack) val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc)) addWindow(Window(vc))
amountVC = vc amountVC = vc
} }
@ExperimentalUnsignedTypes @ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) { fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) { if (!handler.isBufferSlot(slot.id)) {
return return
} }
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart) val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) { val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return else -> return
} }
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt()) 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 DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP } private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int) { fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int) {
val amount = stack.count val amount = stack.count
val s = when { val s = when {
amount < 1_000 -> amount.toString() amount < 1_000 -> amount.toString()
amount < 1_000_000 -> { amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K" 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 amount < 1_000_000_000 -> {
// empty string for label because vanilla renders the count behind the damage bar val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "") format.format(amount / 1_000_000.0) + "M"
}
// ItemRenderer.renderGuiItemOverlay creates a new MatrixStack specifically for drawing the overlay else -> {
val matrixStack = MatrixStack() DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
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().positionMatrix, immediate, false, 0, 0xF000F0)
RenderSystem.enableDepthTest()
immediate.draw()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { // draw damage bar
super.drawBackground(matrixStack, delta, mouseX, mouseY) // empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
drawBackgroundTexture(matrixStack) // ItemRenderer.renderGuiItemOverlay creates a new MatrixStack specifically for drawing the overlay
} val matrixStack = MatrixStack()
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().positionMatrix,
immediate,
false,
0,
0xF000F0
)
RenderSystem.enableDepthTest()
immediate.draw()
}
open fun drawBackgroundTexture(matrixStack: MatrixStack) { override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
RenderSystem.setShader(GameRenderer::getPositionTexColorShader) super.drawBackground(matrixStack, delta, mouseX, mouseY)
RenderSystem.setShaderTexture(0, backgroundTexture)
RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
override fun handledScreenTick() { drawBackgroundTexture(matrixStack)
super.handledScreenTick() }
if (amountVC != null) { open fun drawBackgroundTexture(matrixStack: MatrixStack) {
amountVC!!.field.tick() RenderSystem.setShader(GameRenderer::getPositionTexColorShader)
} else { RenderSystem.setShaderTexture(0, backgroundTexture)
terminalVC.searchField.tick() RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
} val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
val newSearchQuery = searchQueryListener?.requestTerminalSearchFieldUpdate() override fun handledScreenTick() {
if (newSearchQuery != null && searchQuery != newSearchQuery) { super.handledScreenTick()
searchQuery = newSearchQuery
terminalVC.searchField.text = newSearchQuery
requestUpdatedItems()
}
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) { if (amountVC != null) {
super.onMouseClick(slot, invSlot, clickData, type) amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id) && handler.cursorStack.isEmpty) { val newSearchQuery = searchQueryListener?.requestTerminalSearchFieldUpdate()
val stack = slot.stack if (newSearchQuery != null && searchQuery != newSearchQuery) {
searchQuery = newSearchQuery
terminalVC.searchField.text = newSearchQuery
requestUpdatedItems()
}
}
if (type == SlotActionType.QUICK_MOVE) { override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
// shift click, request full stack super.onMouseClick(slot, invSlot, clickData, type)
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)
}
}
}
}
override fun setFocused(element: Element?) { if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id) && handler.cursorStack.isEmpty) {
super.setFocused(element) val stack = slot.stack
// so that when something else (e.g., REI) steals focus and calls setFocused(null) on us, any first responder resigns
if (element == null) { if (type == SlotActionType.QUICK_MOVE) {
windows.last().firstResponder?.resignFirstResponder() // 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)
}
}
}
}
override fun setFocused(element: Element?) {
super.setFocused(element)
// so that when something else (e.g., REI) steals focus and calls setFocused(null) on us, any first responder resigns
if (element == null) {
windows.last().firstResponder?.resignFirstResponder()
}
}
} }

View File

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

View File

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

View File

@ -9,12 +9,12 @@ import net.shadowfacts.phycon.PhysicalConnectivity
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class CraftingTerminalBlock: AbstractTerminalBlock<CraftingTerminalBlockEntity>() { class CraftingTerminalBlock : AbstractTerminalBlock<CraftingTerminalBlockEntity>() {
companion object { companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "crafting_terminal") val ID = Identifier(PhysicalConnectivity.MODID, "crafting_terminal")
} }
override fun createBlockEntity(pos: BlockPos, state: BlockState) = CraftingTerminalBlockEntity(pos, state) override fun createBlockEntity(pos: BlockPos, state: BlockState) = CraftingTerminalBlockEntity(pos, state)
} }

View File

@ -1,8 +1,9 @@
package net.shadowfacts.phycon.block.terminal package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.ItemStackCollections import it.unimi.dsi.fastutil.ints.IntBinaryOperator
import alexiil.mc.lib.attributes.item.ItemStackUtil import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import net.minecraft.block.BlockState import net.minecraft.block.BlockState
import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory import net.minecraft.entity.player.PlayerInventory
@ -17,122 +18,130 @@ import net.minecraft.util.math.BlockPos
import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.ItemStackPacket import net.shadowfacts.phycon.packet.ItemStackPacket
import net.shadowfacts.phycon.packet.LocateStackPacket import net.shadowfacts.phycon.packet.LocateStackPacket
import net.shadowfacts.phycon.packet.RequestInventoryPacket import net.shadowfacts.phycon.util.equalsIgnoringAmount
import net.shadowfacts.phycon.util.fromTag import net.shadowfacts.phycon.util.fromTag
import net.shadowfacts.phycon.util.toTag import net.shadowfacts.phycon.util.toTag
import java.util.LinkedList import java.util.LinkedList
import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class CraftingTerminalBlockEntity(pos: BlockPos, state: BlockState): AbstractTerminalBlockEntity(PhyBlockEntities.CRAFTING_TERMINAL, pos, state) { class CraftingTerminalBlockEntity(pos: BlockPos, state: BlockState) :
AbstractTerminalBlockEntity(PhyBlockEntities.CRAFTING_TERMINAL, pos, state) {
val craftingInv = SimpleInventory(9) val craftingInv = SimpleInventory(9)
private val completedCraftingStackRequests = LinkedList<CraftingStackLocateRequest>() private val completedCraftingStackRequests = LinkedList<CraftingStackLocateRequest>()
override fun onActivate(player: PlayerEntity) { override fun onActivate(player: PlayerEntity) {
super.onActivate(player) super.onActivate(player)
if (!world!!.isClient) { if (!world!!.isClient) {
val factory = object: ExtendedScreenHandlerFactory { val factory = object : ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? { override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? {
return CraftingTerminalScreenHandler(syncId, playerInv, this@CraftingTerminalBlockEntity) return CraftingTerminalScreenHandler(syncId, playerInv, this@CraftingTerminalBlockEntity)
} }
override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal") override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal")
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos) buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos)
} }
} }
player.openHandledScreen(factory) player.openHandledScreen(factory)
} }
} }
fun requestItemsForCrafting(maxAmount: Int) { fun requestItemsForCrafting(maxAmount: Int) {
val amounts = ItemStackCollections.map<IntArray>() // use an array map because we have at most 9 items
// values are bitfields of which slots contain the item
val stackToSlotsMap = Object2IntArrayMap<ItemVariant>()
for (i in 0 until craftingInv.size()) { val or = IntBinaryOperator { a, b -> a or b }
val craftingInvStack = craftingInv.getStack(i) for (i in 0 until craftingInv.size()) {
if (craftingInvStack.isEmpty) continue val craftingInvStack = craftingInv.getStack(i)
if (craftingInvStack.count >= craftingInvStack.maxCount) continue if (craftingInvStack.isEmpty) continue
if (craftingInvStack.count >= craftingInvStack.maxCount) continue
if (craftingInvStack !in amounts) amounts[craftingInvStack] = IntArray(9) { 0 } stackToSlotsMap.mergeInt(ItemVariant.of(craftingInvStack), 1 shl i, or)
amounts[craftingInvStack]!![i] = min(maxAmount, craftingInvStack.maxCount - craftingInvStack.count) }
}
for ((stack, amountPerSlot) in amounts) { for ((variant, slots) in stackToSlotsMap) {
val total = amountPerSlot.sum() val total = slots.countOneBits()
val request = CraftingStackLocateRequest(stack, total, counter, amountPerSlot) val stack = variant.toStack()
pendingRequests.add(request) val request = CraftingStackLocateRequest(stack, total, counter, slots)
sendPacket(LocateStackPacket(stack, ipAddress)) pendingRequests.add(request)
} sendPacket(LocateStackPacket(stack, ipAddress))
} }
}
override fun stackLocateRequestCompleted(request: StackLocateRequest) { override fun stackLocateRequestCompleted(request: StackLocateRequest) {
if (request is CraftingStackLocateRequest) { if (request is CraftingStackLocateRequest) {
completedCraftingStackRequests.add(request) completedCraftingStackRequests.add(request)
} }
super.stackLocateRequestCompleted(request) super.stackLocateRequestCompleted(request)
} }
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val craftingReq = completedCraftingStackRequests.find { ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) } val craftingReq =
if (craftingReq != null) { completedCraftingStackRequests.find { it.stack.equalsIgnoringAmount(packet.stack) }
var remaining = packet.stack.copy() if (craftingReq != null) {
var remaining = packet.stack.copy()
for (i in 0 until craftingInv.size()) { for (i in 0 until craftingInv.size()) {
val currentStack = craftingInv.getStack(i) val currentStack = craftingInv.getStack(i)
if (currentStack.count >= currentStack.maxCount) continue if (currentStack.count >= currentStack.maxCount) continue
if (!ItemStackUtil.areEqualIgnoreAmounts(currentStack, remaining)) continue if (!currentStack.equalsIgnoringAmount(remaining)) continue
val toInsert = minOf(remaining.count, currentStack.maxCount - currentStack.count, craftingReq.amountPerSlot[i]) currentStack.count += 1
currentStack.count += toInsert remaining.count -= 1
remaining.count -= toInsert
craftingReq.amountPerSlot[i] -= toInsert
craftingReq.received += toInsert
if (remaining.isEmpty) { craftingReq.slots = craftingReq.slots and (1 shl i).inv()
break craftingReq.received += 1
}
}
if (craftingReq.amountPerSlot.sum() == 0 || craftingReq.received >= craftingReq.totalResultAmount) { if (remaining.isEmpty) {
completedCraftingStackRequests.remove(craftingReq) break
} }
}
if (!remaining.isEmpty) { // if slots == 0, there are no more slots needing items for this request
remaining = internalBuffer.insert(remaining, TerminalBufferInventory.Mode.FROM_NETWORK) if (craftingReq.slots == 0 || craftingReq.received >= craftingReq.totalResultAmount) {
} completedCraftingStackRequests.remove(craftingReq)
}
updateAndSync() if (!remaining.isEmpty) {
remaining = internalBuffer.insert(remaining, TerminalBufferInventory.Mode.FROM_NETWORK)
}
return remaining updateAndSync()
} else {
return super.doHandleItemStack(packet)
}
}
override fun toCommonTag(tag: NbtCompound) { return remaining
super.toCommonTag(tag) } else {
tag.put("CraftingInv", craftingInv.toTag()) return super.doHandleItemStack(packet)
} }
}
override fun fromCommonTag(tag: NbtCompound) { override fun toCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag) super.toCommonTag(tag)
craftingInv.fromTag(tag.getList("CraftingInv", 10)) tag.put("CraftingInv", craftingInv.toTag())
} }
class CraftingStackLocateRequest( override fun fromCommonTag(tag: NbtCompound) {
stack: ItemStack, super.fromCommonTag(tag)
amount: Int, craftingInv.fromTag(tag.getList("CraftingInv", 10))
timestamp: Long, }
val amountPerSlot: IntArray,
): StackLocateRequest(stack, amount, timestamp) { class CraftingStackLocateRequest(
var received: Int = 0 stack: ItemStack,
} amount: Int,
timestamp: Long,
/**
* Values are bitfields of which slots in the crafting inventory this request is for.
*/
var slots: Int,
) : StackLocateRequest(stack, amount, timestamp) {
var received: Int = 0
}
} }

View File

@ -12,38 +12,38 @@ import net.shadowfacts.phycon.PhysicalConnectivity
* @author shadowfacts * @author shadowfacts
*/ */
class CraftingTerminalScreen( class CraftingTerminalScreen(
handler: CraftingTerminalScreenHandler, handler: CraftingTerminalScreenHandler,
playerInv: PlayerInventory, playerInv: PlayerInventory,
title: Text, title: Text,
): AbstractTerminalScreen<CraftingTerminalBlockEntity, CraftingTerminalScreenHandler>( ) : AbstractTerminalScreen<CraftingTerminalBlockEntity, CraftingTerminalScreenHandler>(
handler, handler,
playerInv, playerInv,
title, title,
259, 259,
252, 252,
) { ) {
companion object { companion object {
private val BACKGROUND_1 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_1.png") 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") private val BACKGROUND_2 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_2.png")
} }
override val backgroundTexture = BACKGROUND_1 override val backgroundTexture = BACKGROUND_1
override fun createViewController(): AbstractTerminalViewController<*, *, *> { override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return CraftingTerminalViewController(this, handler) return CraftingTerminalViewController(this, handler)
} }
override fun drawBackgroundTexture(matrixStack: MatrixStack) { override fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.setShader(GameRenderer::getPositionTexShader) RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND_1) RenderSystem.setShaderTexture(0, BACKGROUND_1)
val x = (width - backgroundWidth) / 2 val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2 val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, 256, 252) drawTexture(matrixStack, x, y, 0, 0, 256, 252)
RenderSystem.setShaderTexture(0, BACKGROUND_2) RenderSystem.setShaderTexture(0, BACKGROUND_2)
drawTexture(matrixStack, x + 256, y, 0, 0, 3, 252) drawTexture(matrixStack, x + 256, y, 0, 0, 3, 252)
} }
} }

View File

@ -20,146 +20,158 @@ import net.shadowfacts.phycon.init.PhyScreens
* @author shadowfacts * @author shadowfacts
*/ */
class CraftingTerminalScreenHandler( class CraftingTerminalScreenHandler(
syncId: Int, syncId: Int,
playerInv: PlayerInventory, playerInv: PlayerInventory,
terminal: CraftingTerminalBlockEntity, terminal: CraftingTerminalBlockEntity,
): AbstractTerminalScreenHandler<CraftingTerminalBlockEntity>(PhyScreens.CRAFTING_TERMINAL, syncId, playerInv, terminal) { ) : AbstractTerminalScreenHandler<CraftingTerminalBlockEntity>(
PhyScreens.CRAFTING_TERMINAL,
syncId,
playerInv,
terminal
) {
val craftingInv = CraftingInv(this) val craftingInv = CraftingInv(this)
val result = CraftingResultInventory() val result = CraftingResultInventory()
val resultSlot: CraftingResultSlot val resultSlot: CraftingResultSlot
val craftingSlotsStart: Int val craftingSlotsStart: Int
val craftingSlotsEnd: Int val craftingSlotsEnd: Int
get() = craftingSlotsStart + 9 get() = craftingSlotsStart + 9
override val xOffset: Int override val xOffset: Int
get() = 5 get() = 5
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf) :
this( this(
syncId, syncId,
playerInv, playerInv,
PhyBlocks.CRAFTING_TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! PhyBlocks.CRAFTING_TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
) )
init { init {
craftingSlotsStart = slots.size craftingSlotsStart = slots.size
for (y in 0 until 3) { for (y in 0 until 3) {
for (x in 0 until 3) { for (x in 0 until 3) {
this.addSlot(Slot(craftingInv, x + y * 3, 13 + x * 18, 140 + y * 18)) this.addSlot(Slot(craftingInv, x + y * 3, 13 + x * 18, 140 + y * 18))
} }
} }
resultSlot = CraftingResultSlot(playerInv.player, craftingInv, result, 0, 31, 224) resultSlot = CraftingResultSlot(playerInv.player, craftingInv, result, 0, 31, 224)
addSlot(resultSlot) addSlot(resultSlot)
updateCraftingResult() updateCraftingResult()
} }
override fun onContentChanged(inventory: Inventory?) { override fun onContentChanged(inventory: Inventory?) {
updateCraftingResult() updateCraftingResult()
} }
private fun updateCraftingResult() { private fun updateCraftingResult() {
val world = playerInv.player.world val world = playerInv.player.world
if (!world.isClient) { if (!world.isClient) {
val player = playerInv.player as ServerPlayerEntity val player = playerInv.player as ServerPlayerEntity
val recipe = world.server!!.recipeManager.getFirstMatch(RecipeType.CRAFTING, craftingInv, world) val recipe = world.server!!.recipeManager.getFirstMatch(RecipeType.CRAFTING, craftingInv, world)
val resultStack = val resultStack =
if (recipe.isPresent && result.shouldCraftRecipe(world, player, recipe.get())) { if (recipe.isPresent && result.shouldCraftRecipe(world, player, recipe.get())) {
recipe.get().craft(craftingInv) recipe.get().craft(craftingInv)
} else { } else {
ItemStack.EMPTY ItemStack.EMPTY
} }
result.setStack(0, resultStack) result.setStack(0, resultStack)
player.networkHandler.sendPacket(ScreenHandlerSlotUpdateS2CPacket(syncId, nextRevision(), resultSlot.id, resultStack)) player.networkHandler.sendPacket(
} ScreenHandlerSlotUpdateS2CPacket(
} syncId,
nextRevision(),
resultSlot.id,
resultStack
)
)
}
}
fun clearCraftingGrid() { fun clearCraftingGrid() {
assert(!playerInv.player.world.isClient) assert(!playerInv.player.world.isClient)
for (i in 0 until terminal.craftingInv.size()) { for (i in 0 until terminal.craftingInv.size()) {
val craftingInvStack = terminal.craftingInv.getStack(i) val craftingInvStack = terminal.craftingInv.getStack(i)
if (craftingInvStack.isEmpty) continue if (craftingInvStack.isEmpty) continue
val remainder = terminal.internalBuffer.insert(craftingInvStack, TerminalBufferInventory.Mode.TO_NETWORK) val remainder = terminal.internalBuffer.insert(craftingInvStack, TerminalBufferInventory.Mode.TO_NETWORK)
terminal.craftingInv.setStack(i, remainder) terminal.craftingInv.setStack(i, remainder)
} }
updateCraftingResult() updateCraftingResult()
sendContentUpdates() sendContentUpdates()
} }
fun requestMoreCraftingIngredients(maxAmount: Int) { fun requestMoreCraftingIngredients(maxAmount: Int) {
assert(!playerInv.player.world.isClient) assert(!playerInv.player.world.isClient)
terminal.requestItemsForCrafting(maxAmount) terminal.requestItemsForCrafting(maxAmount)
} }
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (slotId == resultSlot.id && resultSlot.hasStack()) { if (slotId == resultSlot.id && resultSlot.hasStack()) {
val craftingResult = resultSlot.stack val craftingResult = resultSlot.stack
val originalResult = craftingResult.copy() val originalResult = craftingResult.copy()
// todo: CraftingScreenHandler calls onCraft, but I don't think that's necessary because onStackChanged should handle it // todo: CraftingScreenHandler calls onCraft, but I don't think that's necessary because onStackChanged should handle it
craftingResult.item.onCraft(craftingResult, player.world, player) craftingResult.item.onCraft(craftingResult, player.world, player)
if (!insertItem(craftingResult, playerSlotsStart, playerSlotsEnd, true)) { if (!insertItem(craftingResult, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY return ItemStack.EMPTY
} }
resultSlot.onQuickTransfer(craftingResult, originalResult) resultSlot.onQuickTransfer(craftingResult, originalResult)
if (craftingResult.isEmpty) { if (craftingResult.isEmpty) {
resultSlot.stack = ItemStack.EMPTY resultSlot.stack = ItemStack.EMPTY
} }
if (craftingResult.count == originalResult.count) { if (craftingResult.count == originalResult.count) {
return ItemStack.EMPTY return ItemStack.EMPTY
} }
resultSlot.onTakeItem(player, craftingResult) resultSlot.onTakeItem(player, craftingResult)
player.dropItem(craftingResult, false) player.dropItem(craftingResult, false)
return originalResult return originalResult
} else { } else {
return super.transferSlot(player, slotId) return super.transferSlot(player, slotId)
} }
} }
// RecipeType.CRAFTING wants a CraftingInventory, but we can't store a CraftingInventory on the BE without a screen handler, so... // 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) { class CraftingInv(val handler: CraftingTerminalScreenHandler) : CraftingInventory(handler, 3, 3) {
private val backing = handler.terminal.craftingInv private val backing = handler.terminal.craftingInv
override fun isEmpty(): Boolean { override fun isEmpty(): Boolean {
return backing.isEmpty return backing.isEmpty
} }
override fun getStack(i: Int): ItemStack { override fun getStack(i: Int): ItemStack {
return backing.getStack(i) return backing.getStack(i)
} }
override fun removeStack(i: Int): ItemStack { override fun removeStack(i: Int): ItemStack {
return backing.removeStack(i) return backing.removeStack(i)
} }
override fun removeStack(i: Int, j: Int): ItemStack { override fun removeStack(i: Int, j: Int): ItemStack {
val res = backing.removeStack(i, j) val res = backing.removeStack(i, j)
if (!res.isEmpty) { if (!res.isEmpty) {
handler.onContentChanged(this) handler.onContentChanged(this)
} }
return res return res
} }
override fun setStack(i: Int, itemStack: ItemStack?) { override fun setStack(i: Int, itemStack: ItemStack?) {
backing.setStack(i, itemStack) backing.setStack(i, itemStack)
handler.onContentChanged(this) handler.onContentChanged(this)
} }
override fun clear() { override fun clear() {
backing.clear() backing.clear()
} }
override fun provideRecipeInputs(finder: RecipeMatcher) { override fun provideRecipeInputs(finder: RecipeMatcher) {
TODO() TODO()
} }
} }
} }

View File

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

Some files were not shown because too many files have changed in this diff Show More