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 {
id "fabric-loom" version "0.12.9"
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
@ -79,9 +79,9 @@ repositories {
dependencies {
// PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
// You may need to force-disable transitiveness on them.
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-items:${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-items:${project.libblockattributes_version}"
implementation project(":kiwi-java")
include project(":kiwi-java")

View File

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

View File

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

View File

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

View File

@ -14,18 +14,27 @@ import techreborn.init.TRContent
/**
* @author shadowfacts
*/
object PhyConTR: ModInitializer {
object PhyConTR : ModInitializer {
override fun onInitialize() {
TRContent.StorageUnit.values().forEach {
ItemAttributes.GROUPED_INV.setBlockAdder(AttributeSourceType.COMPAT_WRAPPER, it.block, ::addStorageUnitGroupedInv)
}
}
override fun onInitialize() {
TRContent.StorageUnit.values().forEach {
ItemAttributes.GROUPED_INV.setBlockAdder(
AttributeSourceType.COMPAT_WRAPPER,
it.block,
::addStorageUnitGroupedInv
)
}
}
private fun addStorageUnitGroupedInv(world: World, pos: BlockPos, state: BlockState, to: AttributeList<GroupedItemInv>) {
(world.getBlockEntity(pos) as? StorageUnitBaseBlockEntity)?.also { su ->
to.offer(StorageUnitWrapper(su))
}
}
private fun addStorageUnitGroupedInv(
world: World,
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
*/
class StorageUnitWrapper(
val be: StorageUnitBaseBlockEntity,
): GroupedItemInv {
val be: StorageUnitBaseBlockEntity,
) : GroupedItemInv {
override fun getStoredStacks(): Set<ItemStack> {
val set = ItemStackCollections.set()
if (!be.storedStack.isEmpty) {
set.add(be.storedStack)
}
return set
}
override fun getStoredStacks(): Set<ItemStack> {
val set = ItemStackCollections.set()
if (!be.storedStack.isEmpty) {
set.add(be.storedStack)
}
return set
}
override fun getTotalCapacity(): Int {
return be.maxCapacity
}
override fun getTotalCapacity(): Int {
return be.maxCapacity
}
override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic {
// todo: should spaceAddable really be zero? that's what SimpleGroupedItemInv does
return if (be.storedStack.isEmpty) {
GroupedItemInvView.ItemInvStatistic(filter, 0, 0, totalCapacity)
} else if (filter.matches(be.storedStack)) {
// don't use the storedAmount field, it's only used on the client for rendering
val amount = be.getStoredAmount()
GroupedItemInvView.ItemInvStatistic(filter, amount, 0, totalCapacity - amount)
} else {
GroupedItemInvView.ItemInvStatistic(filter, 0, 0, 0)
}
}
override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic {
// todo: should spaceAddable really be zero? that's what SimpleGroupedItemInv does
return if (be.storedStack.isEmpty) {
GroupedItemInvView.ItemInvStatistic(filter, 0, 0, totalCapacity)
} else if (filter.matches(be.storedStack)) {
// don't use the storedAmount field, it's only used on the client for rendering
val amount = be.getStoredAmount()
GroupedItemInvView.ItemInvStatistic(filter, amount, 0, totalCapacity - amount)
} else {
GroupedItemInvView.ItemInvStatistic(filter, 0, 0, 0)
}
}
override fun attemptInsertion(filter: ItemStack, simulation: Simulation): ItemStack {
if (simulation.isAction) {
return be.processInput(filter)
}
override fun attemptInsertion(filter: ItemStack, simulation: Simulation): ItemStack {
if (simulation.isAction) {
return be.processInput(filter)
}
if (be.storedStack.isEmpty) {
return ItemStack.EMPTY
}
if (be.storedStack.isEmpty) {
return ItemStack.EMPTY
}
if (!ItemStackUtil.areEqualIgnoreAmounts(be.storedStack, filter)) {
return filter
}
if (!ItemStackUtil.areEqualIgnoreAmounts(be.storedStack, filter)) {
return filter
}
val availableCapacity = totalCapacity - be.getStoredAmount()
return if (availableCapacity >= filter.count) {
ItemStack.EMPTY
} else {
filter.copyWithCount(filter.count - availableCapacity)
}
}
val availableCapacity = totalCapacity - be.getStoredAmount()
return if (availableCapacity >= filter.count) {
ItemStack.EMPTY
} else {
filter.copyWithCount(filter.count - availableCapacity)
}
}
override fun attemptExtraction(filter: ItemFilter, maxAmount: Int, simulation: Simulation): ItemStack {
if (be.storedStack.isEmpty || !filter.matches(be.storedStack)) {
return ItemStack.EMPTY
}
override fun attemptExtraction(filter: ItemFilter, maxAmount: Int, simulation: Simulation): ItemStack {
if (be.storedStack.isEmpty || !filter.matches(be.storedStack)) {
return ItemStack.EMPTY
}
val extracted = min(maxAmount, be.getStoredAmount())
val extracted = min(maxAmount, be.getStoredAmount())
if (simulation.isAction) {
be.storedStack.decrement(extracted)
}
if (simulation.isAction) {
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);
default void cableDisconnected() {}
default void cableDisconnected() {
}
}

View File

@ -10,9 +10,9 @@ public interface NetworkDevice {
/**
* 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.
*
* <p>
* The address of a network device should never be the broadcast address.
*
* @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();
@Nullable Text getTooltip();
@Nullable
Text getTooltip();
}

View File

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

View File

@ -22,6 +22,7 @@ public final class MACAddress {
}
private static final Random macAddressRandom = new Random();
@NotNull
public static MACAddress random() {
return random(macAddressRandom);
@ -61,7 +62,7 @@ public final class MACAddress {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MACAddress that = (MACAddress)o;
MACAddress that = (MACAddress) o;
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")
)
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);
}
}
@ -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")
)
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();
if (slot.id < handler.getNetworkSlotsEnd() && stack.getCount() > 1) {
self.drawNetworkSlotAmount(stack, x, y);

View File

@ -7,13 +7,13 @@ import net.shadowfacts.cacao.window.Window
*/
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): T
fun <T : Window> addWindow(window: T, index: Int): 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
*/
open class CacaoHandledScreen<Handler: ScreenHandler>(
handler: Handler,
playerInv: PlayerInventory,
title: Text,
): HandledScreen<Handler>(handler, playerInv, title), AbstractCacaoScreen {
open class CacaoHandledScreen<Handler : ScreenHandler>(
handler: Handler,
playerInv: PlayerInventory,
title: Text,
) : 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 {
if (window is ScreenHandlerWindow && window.screenHandler != handler) {
throw RuntimeException("Adding ScreenHandlerWindow to CacaoHandledScreen with different screen handler is not supported")
}
if (hasAppeared) {
window.viewController.viewWillAppear()
}
override fun <T : Window> addWindow(window: T, index: Int): T {
if (window is ScreenHandlerWindow && window.screenHandler != handler) {
throw RuntimeException("Adding ScreenHandlerWindow to CacaoHandledScreen with different screen handler is not supported")
}
if (hasAppeared) {
window.viewController.viewWillAppear()
}
_windows.add(index, window)
_windows.add(index, window)
window.screen = this
window.wasAdded()
window.resize(width, height)
window.screen = this
window.wasAdded()
window.resize(width, height)
return window
}
return window
}
override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size)
}
override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size)
}
override fun removeWindow(window: Window) {
_windows.remove(window)
if (windows.isEmpty()) {
close()
}
}
override fun removeWindow(window: Window) {
_windows.remove(window)
if (windows.isEmpty()) {
close()
}
}
override fun screenWillAppear() {
windows.forEach {
it.viewController.viewWillAppear()
}
}
override fun screenWillAppear() {
windows.forEach {
it.viewController.viewWillAppear()
}
}
override fun init() {
super.init()
override fun init() {
super.init()
windows.forEach {
it.resize(width, height)
}
}
windows.forEach {
it.resize(width, height)
}
}
override fun close() {
super.close()
override fun close() {
super.close()
windows.forEach {
it.viewController.viewWillDisappear()
it.viewController.viewDidDisappear()
windows.forEach {
it.viewController.viewWillDisappear()
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) {
// no-op
}
override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
// no-op
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val mouse = Point(mouseX, mouseY)
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val mouse = Point(mouseX, mouseY)
matrixStack.push()
matrixStack.translate(0.0, 0.0, -350.0)
matrixStack.push()
matrixStack.translate(0.0, 0.0, -350.0)
for (i in windows.indices) {
val it = windows[i]
for (i in windows.indices) {
val it = windows[i]
if (i == windows.size - 1) {
renderBackground(matrixStack)
}
if (i == windows.size - 1) {
renderBackground(matrixStack)
}
if (it is ScreenHandlerWindow) {
if (i == windows.size - 1) {
super.render(matrixStack, mouseX, mouseY, delta)
} else {
// 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)
}
if (it is ScreenHandlerWindow) {
if (i == windows.size - 1) {
super.render(matrixStack, mouseX, mouseY, delta)
} else {
// 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)
}
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 {
val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
true
} else if (window is ScreenHandlerWindow) {
super.mouseClicked(mouseX, mouseY, button)
} else {
false
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
true
} else if (window is ScreenHandlerWindow) {
super.mouseClicked(mouseX, mouseY, button)
} else {
false
}
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY)
val delta = Point(deltaX, deltaY)
val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button))
return if (result == true) {
true
} else if (window is ScreenHandlerWindow) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
} else {
false
}
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY)
val delta = Point(deltaX, deltaY)
val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button))
return if (result == true) {
true
} else if (window is ScreenHandlerWindow) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
} else {
false
}
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
true
} else if (window is ScreenHandlerWindow) {
super.mouseReleased(mouseX, mouseY, button)
} else {
false
}
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
true
} else if (window is ScreenHandlerWindow) {
super.mouseReleased(mouseX, mouseY, button)
} else {
false
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseScrolled(Point(mouseX, mouseY), amount)
return result == true
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseScrolled(Point(mouseX, mouseY), amount)
return result == true
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
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 keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
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 {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.charTyped(char, modifiersSet) }) {
return true
}
return super.charTyped(char, modifiers)
}
override fun charTyped(char: Char, modifiers: Int): Boolean {
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
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
}

View File

@ -20,166 +20,167 @@ import java.util.*
*
* @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.
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
// _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 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
/**
* 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()
}
private var hasAppeared = false
_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
window.wasAdded()
window.resize(width, height)
_windows.add(index, window)
return window
}
window.screen = this
window.wasAdded()
window.resize(width, height)
/**
* 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)
}
return window
}
/**
* Removes the given window from this screen's window list.
*/
override fun removeWindow(window: Window) {
_windows.remove(window)
if (windows.isEmpty()) {
close()
}
}
/**
* 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)
}
override fun screenWillAppear() {
windows.forEach {
it.viewController.viewWillAppear()
}
}
/**
* Removes the given window from this screen's window list.
*/
override fun removeWindow(window: Window) {
_windows.remove(window)
if (windows.isEmpty()) {
close()
}
}
override fun init() {
super.init()
override fun screenWillAppear() {
windows.forEach {
it.viewController.viewWillAppear()
}
}
windows.forEach {
it.resize(width, height)
}
}
override fun init() {
super.init()
override fun close() {
super.close()
windows.forEach {
it.resize(width, height)
}
}
windows.forEach {
it.viewController.viewWillDisappear()
it.viewController.viewDidDisappear()
override fun close() {
super.close()
// resign the current first responder (if any)
it.firstResponder = null
}
}
windows.forEach {
it.viewController.viewWillDisappear()
it.viewController.viewDidDisappear()
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
if (client != null) {
// workaround this.minecraft sometimes being null causing a crash
renderBackground(matrixStack)
}
// resign the current first responder (if any)
it.firstResponder = null
}
}
val mouse = Point(mouseX, mouseY)
windows.forEach {
it.draw(matrixStack, mouse, delta)
}
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
if (client != null) {
// workaround this.minecraft sometimes being null causing a crash
renderBackground(matrixStack)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
true
} else {
false
}
}
val mouse = Point(mouseX, mouseY)
windows.forEach {
it.draw(matrixStack, mouse, delta)
}
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY)
val delta = Point(deltaX, deltaY)
val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button))
return result == true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
true
} else {
false
}
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return result == true
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY)
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 {
val window = windows.lastOrNull()
val result = window?.mouseScrolled(Point(mouseX, mouseY), amount)
return result == true
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return result == true
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
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 mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseScrolled(Point(mouseX, mouseY), amount)
return result == true
}
override fun keyReleased(i: Int, j: Int, k: Int): Boolean {
return super.keyReleased(i, j, k)
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
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 {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.charTyped(char, modifiersSet) }) {
return true
}
return super.charTyped(char, modifiers)
}
override fun keyReleased(i: Int, j: Int, k: Int): Boolean {
return super.keyReleased(i, j, k)
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
override fun charTyped(char: Char, modifiers: Int): Boolean {
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 {
var responder = windows.lastOrNull()?.firstResponder
while (responder != null) {
if (fn(responder)) {
return true
}
responder = responder.nextResponder
}
return false
var responder = windows.lastOrNull()?.firstResponder
while (responder != null) {
if (fn(responder)) {
return true
}
responder = responder.nextResponder
}
return false
}

View File

@ -11,22 +11,22 @@ import no.birkett.kiwi.Variable
* @author shadowfacts
*/
class LayoutVariable(
val view: View?,
val layoutGuide: LayoutGuide?,
val property: String,
): Variable("LayoutVariable") {
val view: View?,
val layoutGuide: LayoutGuide?,
val property: String,
) : Variable("LayoutVariable") {
constructor(view: View, property: String): this(view, null, property)
constructor(layoutGuide: LayoutGuide, property: String): this(null, layoutGuide, property)
constructor(view: View, property: String) : this(view, null, property)
constructor(layoutGuide: LayoutGuide, property: String) : this(null, layoutGuide, property)
init {
if ((view == null) == (layoutGuide == null)) {
throw RuntimeException("LayoutVariable must be constructed with either a view or layout guide")
}
}
init {
if ((view == null) == (layoutGuide == null)) {
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 is a UI framework for Fabric/Minecraft mods based on Apple's [Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API)
UI toolkit.
## Architecture
### Screen
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
@ -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
### Window
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.
@ -21,6 +25,7 @@ view hierarchy.
[Window]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/Window.kt
### View Controller
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.
@ -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
### 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 File

@ -12,89 +12,89 @@ import net.shadowfacts.cacao.window.Window
*/
interface Responder {
/**
* The window that this responder is part of.
*/
val window: Window?
/**
* The window that this responder is part of.
*/
val window: Window?
/**
* Whether this responder is the first responder of its window.
*
* `false` if [window] is null.
*
* @see Window.firstResponder
*/
val isFirstResponder: Boolean
get() = window?.firstResponder === this
/**
* Whether this responder is the first responder of its window.
*
* `false` if [window] is null.
*
* @see Window.firstResponder
*/
val isFirstResponder: Boolean
get() = window?.firstResponder === this
/**
* The next responder in the chain after this. The next responder will receive an event if this responder did not
* accept it.
*
* The next responder may be `null` if this responder is at the end of the chain.
*/
val nextResponder: Responder?
/**
* The next responder in the chain after this. The next responder will receive an event if this responder did not
* accept it.
*
* The next responder may be `null` if this responder is at the end of the chain.
*/
val nextResponder: Responder?
/**
* Makes this responder become the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun becomeFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot become first responder while not in Window")
}
window!!.firstResponder = this
}
/**
* Makes this responder become the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun becomeFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot become first responder while not in Window")
}
window!!.firstResponder = this
}
/**
* Called immediately after this responder has become the window's first responder.
* @see Window.firstResponder
*/
fun didBecomeFirstResponder() {}
/**
* Called immediately after this responder has become the window's first responder.
* @see Window.firstResponder
*/
fun didBecomeFirstResponder() {}
/**
* Removes this object as the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun resignFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot resign first responder while not in Window")
}
window!!.firstResponder = null
}
/**
* Removes this object as the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun resignFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot resign first responder while not in Window")
}
window!!.firstResponder = null
}
/**
* Called immediately before this object is removed as the window's first responder.
* @see Window.firstResponder
*/
fun didResignFirstResponder() {}
/**
* Called immediately before this object is removed as the window's first responder.
* @see Window.firstResponder
*/
fun didResignFirstResponder() {}
/**
* Called when a character has been typed.
*
* @param char The character that 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.
*/
fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
return false
}
/**
* Called when a character has been typed.
*
* @param char The character that 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.
*/
fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
return false
}
/**
* 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
* invoked is undefined and should not be relied upon.
*
* @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.
* @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
*/
fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return false
}
/**
* 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
* invoked is undefined and should not be relied upon.
*
* @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.
* @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
*/
fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return false
}
}

View File

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

View File

@ -6,16 +6,18 @@ package net.shadowfacts.cacao.geometry
* @author shadowfacts
*/
enum class AxisPosition {
/**
* Top for vertical, left for horizontal.
*/
LEADING,
/**
* Center X/Y.
*/
CENTER,
/**
* Bottom for vertical, right for horizontal.
*/
TRAILING;
}
/**
* Top for vertical, left for horizontal.
*/
LEADING,
/**
* Center X/Y.
*/
CENTER,
/**
* 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>) {
init {
if (points.size != 4) {
throw RuntimeException("Cubic bezier curve must have exactly four points")
}
}
init {
if (points.size != 4) {
throw RuntimeException("Cubic bezier curve must have exactly four points")
}
}
fun point(time: Double): Point {
val x = coordinate(time, Axis.HORIZONTAL)
val y = coordinate(time, Axis.VERTICAL)
return Point(x, y)
}
fun point(time: Double): Point {
val x = coordinate(time, Axis.HORIZONTAL)
val y = coordinate(time, Axis.VERTICAL)
return Point(x, y)
}
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
val p0 = points[0][axis]
val p1 = points[1][axis]
val p2 = points[2][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)
}
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
val p0 = points[0][axis]
val p1 = points[1][axis]
val p2 = points[2][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)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
override fun equals(other: Any?): Boolean {
if (this === other) return true
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 {
return points.contentHashCode()
}
override fun hashCode(): Int {
return points.contentHashCode()
}
}
}

View File

@ -7,25 +7,25 @@ package net.shadowfacts.cacao.geometry
*/
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 {
val ORIGIN = Point(0.0, 0.0)
}
companion object {
val ORIGIN = Point(0.0, 0.0)
}
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
operator fun get(axis: Axis): Double {
return when (axis) {
Axis.HORIZONTAL -> x
Axis.VERTICAL -> y
}
}
operator fun get(axis: Axis): Double {
return when (axis) {
Axis.HORIZONTAL -> x
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) {
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 {
left + width
}
val bottom: Double by lazy {
top + height
}
val right: Double by lazy {
left + width
}
val bottom: Double by lazy {
top + height
}
val midX: Double by lazy {
left + width / 2
}
val midY: Double by lazy {
top + height / 2
}
val midX: Double by lazy {
left + width / 2
}
val midY: Double by lazy {
top + height / 2
}
val origin: Point by lazy {
Point(left, top)
}
val center: Point by lazy {
Point(midX, midY)
}
val origin: Point by lazy {
Point(left, top)
}
val center: Point by lazy {
Point(midX, midY)
}
val size: Size by lazy {
Size(width, height)
}
val size: Size by lazy {
Size(width, height)
}
operator fun contains(point: Point): Boolean {
return point.x >= left && point.x < right && point.y >= top && point.y < bottom
}
operator fun contains(point: Point): Boolean {
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) {
/**
* 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)
/**
* 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)
/**
* The ARGB packed representation of this color.
*/
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)
/**
* The ARGB packed representation of this color.
*/
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)
companion object {
val CLEAR = Color(0, alpha = 0)
val WHITE = Color(0xffffff)
val BLACK = Color(0)
val RED = Color(0xff0000)
val GREEN = Color(0x00ff00)
val BLUE = Color(0x0000ff)
val MAGENTA = Color(0xfc46e4)
companion object {
val CLEAR = Color(0, alpha = 0)
val WHITE = Color(0xffffff)
val BLACK = Color(0)
val RED = Color(0xff0000)
val GREEN = Color(0x00ff00)
val BLUE = Color(0x0000ff)
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 {
fun <E: Enum<E>> next(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) + 1
return if (index < constants.size) constants[index] else constants.first()
}
fun <E : Enum<E>> next(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) + 1
return if (index < constants.size) constants[index] else constants.first()
}
fun <E: Enum<E>> previous(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) - 1
return if (index >= 0) constants[index] else constants.last()
}
fun <E : Enum<E>> previous(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) - 1
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) {
val shift: Boolean
get() = this[GLFW.GLFW_MOD_SHIFT]
val shift: Boolean
get() = this[GLFW.GLFW_MOD_SHIFT]
val control: Boolean
get() = this[GLFW.GLFW_MOD_CONTROL]
val control: Boolean
get() = this[GLFW.GLFW_MOD_CONTROL]
val alt: Boolean
get() = this[GLFW.GLFW_MOD_ALT]
val alt: Boolean
get() = this[GLFW.GLFW_MOD_ALT]
val command: Boolean
get() = this[GLFW.GLFW_MOD_SUPER]
val command: Boolean
get() = this[GLFW.GLFW_MOD_SUPER]
val capsLock: Boolean
get() = this[GLFW.GLFW_MOD_CAPS_LOCK]
val capsLock: Boolean
get() = this[GLFW.GLFW_MOD_CAPS_LOCK]
val numLock: Boolean
get() = this[GLFW.GLFW_MOD_NUM_LOCK]
val numLock: Boolean
get() = this[GLFW.GLFW_MOD_NUM_LOCK]
private operator fun get(mod: Int): Boolean {
return (value and mod) == mod
}
private operator fun get(mod: Int): Boolean {
return (value and mod) == mod
}
}

View File

@ -15,19 +15,24 @@ import net.shadowfacts.cacao.view.View
* @author shadowfacts
*/
class LayoutGuide(
val owningView: View,
val owningView: View,
) {
val leftAnchor: LayoutVariable = LayoutVariable(this, "left")
val rightAnchor: LayoutVariable = LayoutVariable(this, "right")
val topAnchor: LayoutVariable = LayoutVariable(this, "top")
val bottomAnchor: LayoutVariable = LayoutVariable(this, "bottom")
val widthAnchor: LayoutVariable = LayoutVariable(this, "width")
val heightAnchor: LayoutVariable = LayoutVariable(this, "height")
val centerXAnchor: LayoutVariable = LayoutVariable(this, "centerX")
val centerYAnchor: LayoutVariable = LayoutVariable(this, "centerY")
val leftAnchor: LayoutVariable = LayoutVariable(this, "left")
val rightAnchor: LayoutVariable = LayoutVariable(this, "right")
val topAnchor: LayoutVariable = LayoutVariable(this, "top")
val bottomAnchor: LayoutVariable = LayoutVariable(this, "bottom")
val widthAnchor: LayoutVariable = LayoutVariable(this, "width")
val heightAnchor: LayoutVariable = LayoutVariable(this, "height")
val centerXAnchor: LayoutVariable = LayoutVariable(this, "centerX")
val centerYAnchor: LayoutVariable = LayoutVariable(this, "centerY")
val frame: Rect
get() = Rect(leftAnchor.value - owningView.leftAnchor.value, topAnchor.value - owningView.topAnchor.value, widthAnchor.value, heightAnchor.value)
val frame: Rect
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 {
fun <Node> find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? {
@Suppress("NAME_SHADOWING") var node1: Node? = node1
@Suppress("NAME_SHADOWING") var node2: Node? = node2
fun <Node> find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? {
@Suppress("NAME_SHADOWING") var node1: Node? = node1
@Suppress("NAME_SHADOWING") var node2: Node? = node2
val parent1 = LinkedList<Node>()
while (node1 != null) {
parent1.push(node1)
node1 = node1.parent()
}
val parent1 = LinkedList<Node>()
while (node1 != null) {
parent1.push(node1)
node1 = node1.parent()
}
val parent2 = LinkedList<Node>()
while (node2 != null) {
parent2.push(node2)
node2 = node2.parent()
}
val parent2 = LinkedList<Node>()
while (node2 != null) {
parent2.push(node2)
node2 = node2.parent()
}
// paths don't converge on the same root element
if (parent1.first != parent2.first) {
return null
}
// paths don't converge on the same root element
if (parent1.first != parent2.first) {
return null
}
var oldNode: Node? = null
while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) {
oldNode = node1
node1 = parent1.popOrNull()
node2 = parent2.popOrNull()
}
return if (node1 == node2) node1!!
else oldNode!!
}
var oldNode: Node? = null
while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) {
oldNode = node1
node1 = parent1.popOrNull()
node2 = parent2.popOrNull()
}
return if (node1 == node2) node1!!
else oldNode!!
}
}
private fun <T> LinkedList<T>.popOrNull(): T? {
return try {
pop()
} catch (e: NoSuchElementException) {
null
}
}
return try {
pop()
} catch (e: NoSuchElementException) {
null
}
}

View File

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

View File

@ -23,107 +23,153 @@ import kotlin.math.roundToInt
*
* @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
fun playSound(event: SoundEvent) {
if (disabled) return
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(event, 1f))
}
// TODO: find a better place for this
fun playSound(event: SoundEvent) {
if (disabled) return
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(event, 1f))
}
/**
* Draws a solid [rect] filled with the given [color].
*/
fun fill(matrixStack: MatrixStack, rect: Rect, color: Color) {
if (disabled) return
fill(matrixStack, rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb)
}
/**
* Draws a solid [rect] filled with the given [color].
*/
fun fill(matrixStack: MatrixStack, rect: Rect, color: Color) {
if (disabled) return
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].
*/
fun draw(matrixStack: MatrixStack, rect: Rect, texture: Texture) {
if (disabled) return
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, texture.location)
draw(matrixStack, rect.left, rect.top, texture.u, texture.v, rect.width, rect.height, texture.width, texture.height)
}
/**
* Binds and draws the given [texture] filling the [rect].
*/
fun draw(matrixStack: MatrixStack, rect: Rect, texture: Texture) {
if (disabled) return
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, texture.location)
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) {
if (disabled) return
fun drawLine(start: Point, end: Point, z: Double, width: Float, color: Color) {
if (disabled) return
RenderSystem.lineWidth(width)
val tessellator = Tessellator.getInstance()
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()
}
RenderSystem.lineWidth(width)
RenderSystem.enableBlend()
RenderSystem.disableTexture()
RenderSystem.defaultBlendFunc()
RenderSystem.setShader(GameRenderer::getPositionColorShader)
val tessellator = Tessellator.getInstance()
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.
*/
fun draw(matrixStack: MatrixStack, x: Double, y: Double, u: Int, v: Int, 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)
}
/**
* 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
) {
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
// 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) {
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)
}
// Copied from net.minecraft.client.gui.DrawableHelper
// 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
) {
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) {
drawTooltip(matrixStack, listOf(text.asOrderedText()), mouse)
}
fun drawTooltip(matrixStack: MatrixStack, text: Text, mouse: Point) {
drawTooltip(matrixStack, listOf(text.asOrderedText()), mouse)
}
fun drawTooltip(matrixStack: MatrixStack, texts: List<Text>, mouse: Point) {
drawTooltip(matrixStack, texts.map(Text::asOrderedText), mouse)
}
fun drawTooltip(matrixStack: MatrixStack, texts: List<Text>, mouse: Point) {
drawTooltip(matrixStack, texts.map(Text::asOrderedText), mouse)
}
private val dummyScreen = object: Screen(LiteralText("")) {
init {
textRenderer = MinecraftClient.getInstance().textRenderer
itemRenderer = MinecraftClient.getInstance().itemRenderer
}
}
private val dummyScreen = object : Screen(LiteralText("")) {
init {
textRenderer = MinecraftClient.getInstance().textRenderer
itemRenderer = MinecraftClient.getInstance().itemRenderer
}
}
@JvmName("drawOrderedTooltip")
fun drawTooltip(matrixStack: MatrixStack, texts: List<OrderedText>, mouse: Point) {
if (disabled) return
if (texts.isEmpty()) return
@JvmName("drawOrderedTooltip")
fun drawTooltip(matrixStack: MatrixStack, texts: List<OrderedText>, mouse: Point) {
if (disabled) return
if (texts.isEmpty()) return
val client = MinecraftClient.getInstance()
dummyScreen.width = client.window.scaledWidth
dummyScreen.height = client.window.scaledHeight
dummyScreen.renderOrderedTooltip(matrixStack, texts, mouse.x.roundToInt(), mouse.y.roundToInt())
}
val client = MinecraftClient.getInstance()
dummyScreen.width = client.window.scaledWidth
dummyScreen.height = client.window.scaledHeight
dummyScreen.renderOrderedTooltip(matrixStack, texts, mouse.x.roundToInt(), mouse.y.roundToInt())
}
/**
* @see org.lwjgl.opengl.GL11.glColor4f
*/
fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return
RenderSystem.setShaderColor(r, g, b, alpha)
}
/**
* @see org.lwjgl.opengl.GL11.glColor4f
*/
fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return
RenderSystem.setShaderColor(r, g, b, alpha)
}
private fun VertexConsumer.color(color: Color): VertexConsumer {
return color(color.red, color.green, color.blue, color.alpha)
}
private fun VertexConsumer.color(color: Color): VertexConsumer {
return color(color.red, color.green, color.blue, color.alpha)
}
}

View File

@ -10,5 +10,5 @@ import no.birkett.kiwi.Variable
* @author shadowfacts
*/
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
*/
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
get() = this::storage.isInitialized
val isInitialized: Boolean
get() = this::storage.isInitialized
operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return storage
}
operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return storage
}
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
storage = value
observer(value)
}
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
storage = value
observer(value)
}
}
}

View File

@ -7,14 +7,14 @@ import kotlin.reflect.KProperty
*/
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 {
if (storage == null) {
storage = create()
onCreate()
}
return storage!!
}
operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
if (storage == null) {
storage = create()
onCreate()
}
return storage!!
}
}
}

View File

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

View File

@ -15,47 +15,53 @@ import net.minecraft.util.Identifier
* @param centerWidth The width 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 {
val PANEL_BG = NinePatchTexture(Texture(Identifier("textures/gui/demo_background.png"), 0, 0), 5, 5, 238, 156)
companion object {
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_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_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_DISABLED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 46), 3, 3, 194, 14)
}
// Corners
val topLeft by lazy {
texture
}
val topRight by lazy {
Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height)
}
val bottomLeft by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height)
}
val bottomRight by lazy {
Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height)
}
// Corners
val topLeft by lazy {
texture
}
val topRight by lazy {
Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height)
}
val bottomLeft by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height)
}
val bottomRight by lazy {
Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height)
}
// Edges
val topMiddle by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height)
}
val bottomMiddle by lazy {
Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height)
}
val leftMiddle by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height)
}
val rightMiddle by lazy {
Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height)
}
// Edges
val topMiddle by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height)
}
val bottomMiddle by lazy {
Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height)
}
val leftMiddle by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height)
}
val rightMiddle by lazy {
Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height)
}
// Center
val center by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v + cornerHeight, texture.width, texture.height)
}
// Center
val center by lazy {
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
*/
class BezierCurveView(val curve: BezierCurve): View() {
class BezierCurveView(val curve: BezierCurve) : View() {
private val points by lazy {
val step = 0.05
var t = 0.0
val points = mutableListOf<Point>()
while (t <= 1) {
points.add(curve.point(t))
t += step
}
points
}
private val points by lazy {
val step = 0.05
var t = 0.0
val points = mutableListOf<Point>()
while (t <= 1) {
points.add(curve.point(t))
t += step
}
points
}
var lineWidth = 3f
var lineColor = Color.BLACK
var lineWidth = 3f
var lineColor = Color.BLACK
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.scale(bounds.width.toFloat(), bounds.height.toFloat(), 1f)
for ((index, point) in points.withIndex()) {
val next = points.getOrNull(index + 1) ?: break
RenderHelper.drawLine(point, next, zIndex, lineWidth, lineColor)
}
matrixStack.pop()
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.scale(bounds.width.toFloat(), bounds.height.toFloat(), 1f)
for ((index, point) in points.withIndex()) {
val next = points.getOrNull(index + 1) ?: break
RenderHelper.drawLine(point, next, zIndex, lineWidth, lineColor)
}
matrixStack.pop()
}
}

View File

@ -14,103 +14,103 @@ import net.shadowfacts.kiwidsl.dsl
* @author shadowfacts
*/
class DialogView(
val title: Text,
val message: Text,
val buttonTypes: Array<ButtonType>,
val iconTexture: Texture?,
val buttonCallback: (ButtonType, Window) -> Unit
): View() {
val title: Text,
val message: Text,
val buttonTypes: Array<ButtonType>,
val iconTexture: Texture?,
val buttonCallback: (ButtonType, Window) -> Unit
) : View() {
interface ButtonType {
val localizedName: Text
}
interface ButtonType {
val localizedName: Text
}
enum class DefaultButtonType: ButtonType {
CANCEL, CONFIRM, OK, CLOSE;
enum class DefaultButtonType : ButtonType {
CANCEL, CONFIRM, OK, CLOSE;
override val localizedName: Text
get() = LiteralText(name.lowercase().replaceFirstChar(Char::titlecase)) // todo: actually localize me
}
override val localizedName: Text
get() = LiteralText(name.lowercase().replaceFirstChar(Char::titlecase)) // todo: actually localize me
}
private lateinit var background: NinePatchView
private lateinit var hStack: StackView
private var iconView: TextureView? = null
private lateinit var vStack: StackView
private lateinit var messageLabel: Label
private var buttonContainer: View? = null
private var buttonStack: StackView? = null
private lateinit var background: NinePatchView
private lateinit var hStack: StackView
private var iconView: TextureView? = null
private lateinit var vStack: StackView
private lateinit var messageLabel: Label
private var buttonContainer: View? = null
private var buttonStack: StackView? = null
override fun wasAdded() {
background = addSubview(NinePatchView(NinePatchTexture.PANEL_BG).apply { zIndex = -1.0 })
override fun wasAdded() {
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) {
iconView = hStack.addArrangedSubview(TextureView(iconTexture))
}
if (iconTexture != null) {
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 {
textColor = Color(0x404040)
})
messageLabel = vStack.addArrangedSubview(Label(message, shadow = false).apply {
textColor = Color(0x404040)
})
vStack.addArrangedSubview(Label(title, shadow = false).apply {
textColor = Color(0x404040)
})
messageLabel = vStack.addArrangedSubview(Label(message, shadow = false).apply {
textColor = Color(0x404040)
})
if (buttonTypes.isNotEmpty()) {
buttonContainer = vStack.addArrangedSubview(View())
buttonStack = buttonContainer!!.addSubview(StackView(Axis.HORIZONTAL))
for (type in buttonTypes) {
buttonStack!!.addArrangedSubview(Button(Label(type.localizedName)).apply {
handler = {
this@DialogView.buttonCallback(type, this@DialogView.window!!)
}
})
}
}
if (buttonTypes.isNotEmpty()) {
buttonContainer = vStack.addArrangedSubview(View())
buttonStack = buttonContainer!!.addSubview(StackView(Axis.HORIZONTAL))
for (type in buttonTypes) {
buttonStack!!.addArrangedSubview(Button(Label(type.localizedName)).apply {
handler = {
this@DialogView.buttonCallback(type, this@DialogView.window!!)
}
})
}
}
super.wasAdded()
}
super.wasAdded()
}
override fun createInternalConstraints() {
super.createInternalConstraints()
override fun createInternalConstraints() {
super.createInternalConstraints()
solver.dsl {
centerXAnchor equalTo window!!.centerXAnchor
centerYAnchor equalTo window!!.centerYAnchor
solver.dsl {
centerXAnchor equalTo window!!.centerXAnchor
centerYAnchor equalTo window!!.centerYAnchor
widthAnchor greaterThanOrEqualTo 175
widthAnchor greaterThanOrEqualTo 175
background.leftAnchor equalTo leftAnchor - 8
background.rightAnchor equalTo rightAnchor + 8
background.topAnchor equalTo topAnchor - 8
background.bottomAnchor equalTo bottomAnchor + 8
background.leftAnchor equalTo leftAnchor - 8
background.rightAnchor equalTo rightAnchor + 8
background.topAnchor equalTo topAnchor - 8
background.bottomAnchor equalTo bottomAnchor + 8
hStack.leftAnchor equalTo leftAnchor
hStack.rightAnchor equalTo rightAnchor
hStack.topAnchor equalTo topAnchor
hStack.bottomAnchor equalTo bottomAnchor
hStack.leftAnchor equalTo leftAnchor
hStack.rightAnchor equalTo rightAnchor
hStack.topAnchor equalTo topAnchor
hStack.bottomAnchor equalTo bottomAnchor
if (iconView != null) {
hStack.bottomAnchor greaterThanOrEqualTo iconView!!.bottomAnchor
}
hStack.bottomAnchor greaterThanOrEqualTo vStack.bottomAnchor
if (iconView != null) {
hStack.bottomAnchor greaterThanOrEqualTo iconView!!.bottomAnchor
}
hStack.bottomAnchor greaterThanOrEqualTo vStack.bottomAnchor
if (iconView != null) {
iconView!!.widthAnchor equalTo 30
iconView!!.heightAnchor equalTo 30
}
if (iconView != null) {
iconView!!.widthAnchor equalTo 30
iconView!!.heightAnchor equalTo 30
}
messageLabel.heightAnchor greaterThanOrEqualTo 50
messageLabel.heightAnchor greaterThanOrEqualTo 50
if (buttonContainer != null) {
buttonStack!!.heightAnchor equalTo buttonContainer!!.heightAnchor
buttonStack!!.centerYAnchor equalTo buttonContainer!!.centerYAnchor
if (buttonContainer != null) {
buttonStack!!.heightAnchor equalTo buttonContainer!!.heightAnchor
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.
*/
class Label(
text: Text,
var shadow: Boolean = false,
val maxLines: Int = 0,
val wrappingMode: WrappingMode = WrappingMode.WRAP,
var textAlignment: TextAlignment = TextAlignment.LEFT
): View() {
text: Text,
var shadow: Boolean = false,
val maxLines: Int = 0,
val wrappingMode: WrappingMode = WrappingMode.WRAP,
var textAlignment: TextAlignment = TextAlignment.LEFT
) : View() {
companion object {
private val textRenderer: TextRenderer
get() = MinecraftClient.getInstance().textRenderer
}
companion object {
private val textRenderer: TextRenderer
get() = MinecraftClient.getInstance().textRenderer
}
enum class WrappingMode {
WRAP, NO_WRAP
}
enum class WrappingMode {
WRAP, NO_WRAP
}
enum class TextAlignment {
LEFT, CENTER, RIGHT
}
enum class TextAlignment {
LEFT, CENTER, RIGHT
}
constructor(
text: String,
shadow: Boolean = false,
maxLines: Int = 0,
wrappingMode: WrappingMode = WrappingMode.WRAP,
textAlignment: TextAlignment = TextAlignment.LEFT,
): this(LiteralText(text), shadow, maxLines, wrappingMode, textAlignment)
constructor(
text: String,
shadow: Boolean = false,
maxLines: Int = 0,
wrappingMode: WrappingMode = WrappingMode.WRAP,
textAlignment: TextAlignment = TextAlignment.LEFT,
) : 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.
*/
var text: Text = text
set(value) {
field = value
// todo: uhhhh
updateIntrinsicContentSize(true)
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
private lateinit var lines: List<OrderedText>
/**
* The text of this label. Mutating this field will update the intrinsic content size and trigger a layout.
*/
var text: Text = text
set(value) {
field = value
// todo: uhhhh
updateIntrinsicContentSize(true)
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
private lateinit var lines: List<OrderedText>
var textColor = Color.WHITE
set(value) {
field = value
textColorARGB = value.argb
}
private var textColorARGB: Int = textColor.argb
var textColor = Color.WHITE
set(value) {
field = value
textColorARGB = value.argb
}
private var textColorARGB: Int = textColor.argb
override fun wasAdded() {
super.wasAdded()
override fun wasAdded() {
super.wasAdded()
updateIntrinsicContentSize(false)
}
updateIntrinsicContentSize(false)
}
private fun updateIntrinsicContentSize(canWrap: Boolean, isFromDidLayout: Boolean = false): Boolean {
if (RenderHelper.disabled) return false
private fun updateIntrinsicContentSize(canWrap: Boolean, isFromDidLayout: Boolean = false): Boolean {
if (RenderHelper.disabled) return false
val oldSize = intrinsicContentSize
// 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) {
val lines = textRenderer.wrapLines(text, bounds.width.toInt())
val height = (if (maxLines == 0) lines.size else min(lines.size, maxLines)) * textRenderer.fontHeight
intrinsicContentSize = Size(bounds.width, height.toDouble())
} else {
val width = textRenderer.getWidth(text)
val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble())
}
return 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
if (wrappingMode == WrappingMode.WRAP && canWrap && hasSolver && isFromDidLayout) {
val lines = textRenderer.wrapLines(text, bounds.width.toInt())
val height = (if (maxLines == 0) lines.size else min(lines.size, maxLines)) * textRenderer.fontHeight
intrinsicContentSize = Size(bounds.width, height.toDouble())
} else {
val width = textRenderer.getWidth(text)
val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble())
}
return oldSize != intrinsicContentSize
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
if (!this::lines.isInitialized) {
computeLines()
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
if (!this::lines.isInitialized) {
computeLines()
}
for (i in 0 until lines.size) {
val x = when (textAlignment) {
TextAlignment.LEFT -> 0.0
TextAlignment.CENTER -> (bounds.width - textRenderer.getWidth(lines[i])) / 2
TextAlignment.RIGHT -> bounds.width - textRenderer.getWidth(lines[i])
}
val y = i * textRenderer.fontHeight
if (shadow) {
textRenderer.drawWithShadow(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
} else {
textRenderer.draw(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
}
}
}
for (i in 0 until lines.size) {
val x = when (textAlignment) {
TextAlignment.LEFT -> 0.0
TextAlignment.CENTER -> (bounds.width - textRenderer.getWidth(lines[i])) / 2
TextAlignment.RIGHT -> bounds.width - textRenderer.getWidth(lines[i])
}
val y = i * textRenderer.fontHeight
if (shadow) {
textRenderer.drawWithShadow(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
} else {
textRenderer.draw(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
}
}
}
override fun didLayout() {
super.didLayout()
override fun didLayout() {
super.didLayout()
computeLines()
if (updateIntrinsicContentSize(true, true)) {
// if the intrinsic content size changes, relayout
window!!.layout()
}
}
computeLines()
if (updateIntrinsicContentSize(true, true)) {
// if the intrinsic content size changes, relayout
window!!.layout()
}
}
private fun computeLines() {
if (wrappingMode == WrappingMode.WRAP) {
var lines = textRenderer.wrapLines(text, bounds.width.toInt())
if (maxLines > 0 && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
}
this.lines = lines
} else {
this.lines = listOf(text.asOrderedText())
}
}
private fun computeLines() {
if (wrappingMode == WrappingMode.WRAP) {
var lines = textRenderer.wrapLines(text, bounds.width.toInt())
if (maxLines > 0 && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
}
this.lines = lines
} else {
this.lines = listOf(text.asOrderedText())
}
}
}

View File

@ -17,121 +17,260 @@ import kotlin.math.roundToInt
* @author shadowfacts
* @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
private val topLeftDelegate = ResettableLazyProperty {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val topLeft by topLeftDelegate
// Corners
private val topLeftDelegate = ResettableLazyProperty {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val topRight by topRightDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(
bounds.width - ninePatch.cornerWidth,
0.0,
ninePatch.cornerWidth.toDouble(),
ninePatch.cornerHeight.toDouble()
)
}
protected open val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val bottomLeft by bottomLeftDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(
0.0,
bounds.height - ninePatch.cornerHeight,
ninePatch.cornerWidth.toDouble(),
ninePatch.cornerHeight.toDouble()
)
}
protected open val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val bottomRight by bottomRightDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(
bounds.width - ninePatch.cornerWidth,
bounds.height - ninePatch.cornerHeight,
ninePatch.cornerWidth.toDouble(),
ninePatch.cornerHeight.toDouble()
)
}
protected open val bottomRight by bottomRightDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble())
}
protected open val topMiddle by topMiddleDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(
ninePatch.cornerWidth.toDouble(),
topLeft.top,
bounds.width - 2 * ninePatch.cornerWidth,
ninePatch.cornerHeight.toDouble()
)
}
protected open val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
}
protected open val bottomMiddle by bottomMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
}
protected open val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight)
}
protected open val leftMiddle by leftMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(
topLeft.left,
ninePatch.cornerHeight.toDouble(),
ninePatch.cornerWidth.toDouble(),
bounds.height - 2 * ninePatch.cornerHeight
)
}
protected open val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
}
protected open val rightMiddle by rightMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
}
protected open val rightMiddle by rightMiddleDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
}
protected open val center by centerDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
}
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() {
super.didLayout()
override fun didLayout() {
super.didLayout()
delegates.forEach(ResettableLazyProperty<Rect>::reset)
}
delegates.forEach(ResettableLazyProperty<Rect>::reset)
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
drawCorners(matrixStack)
drawEdges(matrixStack)
drawCenter(matrixStack)
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
drawCorners(matrixStack)
drawEdges(matrixStack)
drawCenter(matrixStack)
}
private fun drawCorners(matrixStack: MatrixStack) {
RenderHelper.draw(matrixStack, topLeft, ninePatch.topLeft)
RenderHelper.draw(matrixStack, topRight, ninePatch.topRight)
RenderHelper.draw(matrixStack, bottomLeft, ninePatch.bottomLeft)
RenderHelper.draw(matrixStack, bottomRight, ninePatch.bottomRight)
}
private fun drawCorners(matrixStack: MatrixStack) {
RenderHelper.draw(matrixStack, topLeft, ninePatch.topLeft)
RenderHelper.draw(matrixStack, topRight, ninePatch.topRight)
RenderHelper.draw(matrixStack, bottomLeft, ninePatch.bottomLeft)
RenderHelper.draw(matrixStack, bottomRight, ninePatch.bottomRight)
}
private fun drawEdges(matrixStack: MatrixStack) {
// Horizontal
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(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)
}
private fun drawEdges(matrixStack: MatrixStack) {
// Horizontal
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(
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
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(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)
}
}
// Vertical
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(
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) {
for (i in 0 until (center.height.roundToInt() / ninePatch.centerHeight)) {
drawCenterRow(matrixStack, center.top + i * ninePatch.centerHeight.toDouble(), ninePatch.centerHeight.toDouble())
}
val remHeight = center.height.roundToInt() % ninePatch.centerHeight
if (remHeight > 0) {
drawCenterRow(matrixStack, center.bottom - remHeight, remHeight.toDouble())
}
}
private fun drawCenter(matrixStack: MatrixStack) {
for (i in 0 until (center.height.roundToInt() / ninePatch.centerHeight)) {
drawCenterRow(
matrixStack,
center.top + i * ninePatch.centerHeight.toDouble(),
ninePatch.centerHeight.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) {
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)
}
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)
}
}
private fun drawCenterRow(matrixStack: MatrixStack, y: Double, height: Double) {
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
)
}
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.
*/
open class StackView(
val axis: Axis,
val distribution: Distribution = Distribution.FILL,
val spacing: Double = 0.0
): View() {
val axis: Axis,
val distribution: Distribution = Distribution.FILL,
val spacing: Double = 0.0
) : View() {
// the internal, mutable list of arranged subviews
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
// the internal, mutable list of arranged subviews
private val _arrangedSubviews = LinkedList<View>()
private var leadingConnection: Constraint? = null
private var trailingConnection: Constraint? = null
private var arrangedSubviewConnections = mutableListOf<Constraint>()
/**
* 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
/**
* 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)
private var leadingConnection: Constraint? = null
private var trailingConnection: Constraint? = null
private var arrangedSubviewConnections = mutableListOf<Constraint>()
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)
/**
* 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")
}
return view
}
if (index == 0) {
solver.removeConstraint(leadingConnection)
val next = arrangedSubviews.getOrNull(1)
if (next != null) {
solver.dsl {
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next)
}
} 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
}
}
/**
* 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 the removed view is in the middle
if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) {
val prev = arrangedSubviews[index - 1]
val next = arrangedSubviews[index + 1]
solver.dsl {
solver.removeConstraint(arrangedSubviewConnections[index - 1])
solver.removeConstraint(arrangedSubviewConnections[index])
if (index == 0) {
solver.removeConstraint(leadingConnection)
val next = arrangedSubviews.getOrNull(1)
if (next != null) {
solver.dsl {
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next)
}
} 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
arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next)
arrangedSubviewConnections.removeAt(index)
}
}
// if the removed view is in the middle
if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) {
val prev = arrangedSubviews[index - 1]
val next = arrangedSubviews[index + 1]
solver.dsl {
solver.removeConstraint(arrangedSubviewConnections[index - 1])
solver.removeConstraint(arrangedSubviewConnections[index])
_arrangedSubviews.remove(view)
removeSubview(view)
}
// todo: double check me
arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next)
arrangedSubviewConnections.removeAt(index)
}
}
override fun removeSubview(view: View) {
if (arrangedSubviews.contains(view)) {
removeArrangedSubview(view)
} else {
super.removeSubview(view)
}
}
_arrangedSubviews.remove(view)
removeSubview(view)
}
private fun addConstraintsForArrangedView(view: View, index: Int) {
if (index == 0) {
if (leadingConnection != null) {
solver.removeConstraint(leadingConnection)
}
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)
}
}
}
override fun removeSubview(view: View) {
if (arrangedSubviews.contains(view)) {
removeArrangedSubview(view)
} else {
super.removeSubview(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)
}
private fun addConstraintsForArrangedView(view: View, index: Int) {
if (index == 0) {
if (leadingConnection != null) {
solver.removeConstraint(leadingConnection)
}
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)
/**
* 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 {
/**
* The leading edges of arranged subviews are pinned to the leading edge of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
LEADING,
/**
* The centers of the arranged subviews are pinned to the center of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
CENTER,
/**
* The trailing edges of arranged subviews are pinned to the leading edge of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
TRAILING,
/**
* The arranged subviews fill the perpendicular axis of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
FILL
}
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 {
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 {
/**
* The leading edges of arranged subviews are pinned to the leading edge of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
LEADING,
/**
* The centers of the arranged subviews are pinned to the center of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
CENTER,
/**
* The trailing edges of arranged subviews are pinned to the leading edge of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
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
*/
class TextureView(var texture: Texture?): View() {
class TextureView(var texture: Texture?) : View() {
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
texture?.also {
RenderHelper.draw(matrixStack, bounds, it)
}
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
texture?.also {
RenderHelper.draw(matrixStack, bounds, it)
}
}
}

View File

@ -20,502 +20,521 @@ import kotlin.math.floor
*
* @author shadowfacts
*/
open class View(): Responder {
open class View() : Responder {
/**
* The window whose view hierarchy this view belongs to.
* Not initialized until the root view in this hierarchy has been added to a hierarchy,
* using it before that will throw a runtime exception.
*/
override var window: Window? = null
/**
* The window whose view hierarchy this view belongs to.
* Not initialized until the root view in this hierarchy has been added to a hierarchy,
* using it before that will throw a runtime exception.
*/
override var window: Window? = null
/**
* The next responder after this one.
* For views, the next responder is the view's superview.
*/
override val nextResponder: Responder?
// todo: should the view controller be a Responder?
get() = superview
/**
* The next responder after this one.
* For views, the next responder is the view's superview.
*/
override val nextResponder: Responder?
// todo: should the view controller be a Responder?
get() = superview
private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) {
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
private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) {
v.solver = it
}
}
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
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
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")
val hasSolver: Boolean
get() = solverDelegate.isInitialized
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.
*
* To add a layout guide, call [addLayoutGuide].
*
* @see LayoutGuide
*/
val layoutGuides: List<LayoutGuide> = _layoutGuides
/**
* Layout anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = LayoutVariable(this, "right")
/**
* Whether this view uses constraint-based layout.
* 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.
*
* Default is `true`.
*/
var usesConstraintBasedLayout = true
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
val topAnchor = LayoutVariable(this, "top")
/**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
* 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)
}
/**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = LayoutVariable(this, "bottom")
/**
* The rectangle for this view in its own coordinate system.
* If using constraint based layout, this property has zero dimensions until [didLayout] called.
* Otherwise, this will be initialized when [frame] is set.
*/
var bounds = Rect(0.0, 0.0, 0.0, 0.0)
/**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = LayoutVariable(this, "width")
/**
* 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.
*/
var zIndex: Double = 0.0
/**
* Layout anchor for the height of this view in the window's coordinate system.
*/
val heightAnchor = LayoutVariable(this, "height")
/**
* The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no
* intrinsic size.
*
* 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
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = LayoutVariable(this, "centerX")
/**
* The background color of this view.
*/
var backgroundColor = Color.CLEAR
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
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].
*/
var superview: View? = null
// _subviews is the internal, mutable object since we only want it to be mutated by the add/removeSubview methods
private val _subviews = LinkedList<View>()
private var subviewsSortedByZIndex: List<View> = listOf()
/**
* All the layout guides attached to this view.
*
* To add a layout guide, call [addLayoutGuide].
*
* @see LayoutGuide
*/
val layoutGuides: List<LayoutGuide> = _layoutGuides
/**
* 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
/**
* Whether this view uses constraint-based layout.
* 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.
*
* Default is `true`.
*/
var usesConstraintBasedLayout = true
constructor(frame: Rect): this() {
this.usesConstraintBasedLayout = false
this.frame = frame
}
/**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
* 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.
*/
fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable {
return when (axis) {
Axis.HORIZONTAL ->
when (position) {
AxisPosition.LEADING -> leftAnchor
AxisPosition.CENTER -> centerXAnchor
AxisPosition.TRAILING -> rightAnchor
}
Axis.VERTICAL ->
when (position) {
AxisPosition.LEADING -> topAnchor
AxisPosition.CENTER -> centerYAnchor
AxisPosition.TRAILING -> bottomAnchor
}
}
}
/**
* The rectangle for this view in its own coordinate system.
* If using constraint based layout, this property has zero dimensions until [didLayout] called.
* Otherwise, this will be initialized when [frame] is set.
*/
var bounds = Rect(0.0, 0.0, 0.0, 0.0)
/**
* 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)
/**
* 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.
*/
var zIndex: Double = 0.0
view.superview = this
if (hasSolver) {
view.solver = solver
}
view.window = window
/**
* The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no
* intrinsic size.
*
* 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
* 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")
}
/**
* This view's parent view. If `null`, this view is a top-level view in the [Window].
*/
var superview: View? = null
_subviews.remove(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
// _subviews is the internal, mutable object since we only want it to be mutated by the add/removeSubview methods
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
val constraintsToRemove = solver.constraints.filter { constraint ->
val variables = constraint.getVariables().mapNotNull { it as? LayoutVariable }
constructor(frame: Rect) : this() {
this.usesConstraintBasedLayout = false
this.frame = frame
}
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)
/**
* Helper method for retrieve the anchor for a specific position on the given axis.
*/
fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable {
return when (axis) {
Axis.HORIZONTAL ->
when (position) {
AxisPosition.LEADING -> leftAnchor
AxisPosition.CENTER -> centerXAnchor
AxisPosition.TRAILING -> rightAnchor
}
// 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.window = null
view.window = null
// todo: is this necessary?
// todo: is this necessary?
// view.wasRemoved()
}
}
/**
* Creates and returns a new layout guide with this view as its owner.
*/
fun addLayoutGuide(): LayoutGuide {
val guide = LayoutGuide(this)
_layoutGuides.add(guide)
if (hasSolver) {
guide.attachTo(solver)
}
return guide
}
/**
* Creates and returns a new layout guide with this view as its owner.
*/
fun addLayoutGuide(): LayoutGuide {
val guide = LayoutGuide(this)
_layoutGuides.add(guide)
if (hasSolver) {
guide.attachTo(solver)
}
return guide
}
/**
* Removes this view from its superview, if it has one.
*/
fun removeFromSuperview() {
superview?.removeSubview(this)
}
/**
* Removes this view from its superview, if it has one.
*/
fun removeFromSuperview() {
superview?.removeSubview(this)
}
/**
* Finds all subviews that contain the given point.
*
* @param point The point to find subviews for, in the coordinate system of this view.
* @return All views that contain the given point.
*/
fun subviewsAtPoint(point: Point): List<View> {
return subviews.filter { point in it.frame }
}
/**
* Finds all subviews that contain the given point.
*
* @param point The point to find subviews for, in the coordinate system of this view.
* @return All views that contain the given point.
*/
fun subviewsAtPoint(point: Point): List<View> {
return subviews.filter { point in it.frame }
}
/**
* Attempts to find a subview which contains the given point.
* 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].
*
* @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.
*/
fun subviewAtPoint(point: Point): View? {
return subviews.firstOrNull { point in it.frame }
}
/**
* Attempts to find a subview which contains the given point.
* 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].
*
* @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.
*/
fun subviewAtPoint(point: Point): View? {
return subviews.firstOrNull { point in it.frame }
}
/**
* Called when this view was added to a view hierarchy.
* If overridden, the super-class method must be called.
*/
open fun wasAdded() {
createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
/**
* Called when this view was added to a view hierarchy.
* If overridden, the super-class method must be called.
*/
open fun wasAdded() {
createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
layoutGuides.forEach {
it.attachTo(solver)
}
}
layoutGuides.forEach {
it.attachTo(solver)
}
}
/**
* 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.
*/
protected open fun createInternalConstraints() {
if (!usesConstraintBasedLayout) return
/**
* 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.
*/
protected open fun createInternalConstraints() {
if (!usesConstraintBasedLayout) return
solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!usesConstraintBasedLayout || !hasSolver) return
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!usesConstraintBasedLayout || !hasSolver) return
if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
solver.removeConstraint(intrinsicContentSizeHeightConstraint!!)
}
if (new != null) {
solver.dsl {
this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = MEDIUM))
this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = MEDIUM))
}
}
}
if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
solver.removeConstraint(intrinsicContentSizeHeightConstraint!!)
}
if (new != null) {
solver.dsl {
this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = MEDIUM))
this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = MEDIUM))
}
}
}
/**
* Called after this view has been laid-out.
* If overridden, the super-class method must be called.
*/
open fun didLayout() {
if (usesConstraintBasedLayout) {
val superviewLeft = superview?.leftAnchor?.value ?: 0.0
val superviewTop = superview?.topAnchor?.value ?: 0.0
frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value)
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value)
}
/**
* Called after this view has been laid-out.
* If overridden, the super-class method must be called.
*/
open fun didLayout() {
if (usesConstraintBasedLayout) {
val superviewLeft = superview?.leftAnchor?.value ?: 0.0
val superviewTop = superview?.topAnchor?.value ?: 0.0
frame = Rect(
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.
* 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.
* Use [drawContent] to draw any custom content.
*
* @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.translate(frame.left, frame.top, 0.0)
/**
* Called to draw this view.
* 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.
* Use [drawContent] to draw any custom content.
*
* @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
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 {
val mouseInView = convert(mouse, to = it)
it.draw(matrixStack, mouseInView, delta)
}
subviewsSortedByZIndex.forEach {
val mouseInView = convert(mouse, to = it)
it.draw(matrixStack, mouseInView, delta)
}
matrixStack.pop()
}
matrixStack.pop()
}
/**
* 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
* 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 delta The time since the last frame.
*/
open fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {}
/**
* 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
* 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 delta The time since the last frame.
*/
open fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {}
/**
* Called when this view is clicked.
*
* 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
* 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.
*
* 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.
*
* @param point The point in the coordinate system of this view that the mouse was clicked.
* @param mouseButton The mouse button used to click.
* @return Whether the mouse click was handled by this view or any subviews.
*/
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val (inside, outside) = subviews.partition { point in it.frame }
val view = inside.maxByOrNull(View::zIndex)
var result = false
if (view != null) {
val pointInView = convert(point, to = view)
result = view.mouseClicked(pointInView, mouseButton)
}
for (v in outside) {
val pointInV = convert(point, to = v)
v.mouseClickedOutside(pointInV, mouseButton)
}
return result
}
/**
* Called when this view is clicked.
*
* 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
* 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.
*
* 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.
*
* @param point The point in the coordinate system of this view that the mouse was clicked.
* @param mouseButton The mouse button used to click.
* @return Whether the mouse click was handled by this view or any subviews.
*/
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val (inside, outside) = subviews.partition { point in it.frame }
val view = inside.maxByOrNull(View::zIndex)
var result = false
if (view != null) {
val pointInView = convert(point, to = view)
result = view.mouseClicked(pointInView, mouseButton)
}
for (v in outside) {
val pointInV = convert(point, to = v)
v.mouseClickedOutside(pointInV, mouseButton)
}
return result
}
/**
* 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.
*
* @param point The clicked point _in the coordinate space of this view_.
* @param mouseButton The mouse button used to click.
*/
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
for (view in subviews) {
val pointInView = convert(point, to = view)
view.mouseClickedOutside(pointInView, mouseButton)
}
}
/**
* 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.
*
* @param point The clicked point _in the coordinate space of this view_.
* @param mouseButton The mouse button used to click.
*/
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
for (view in subviews) {
val pointInView = convert(point, to = view)
view.mouseClickedOutside(pointInView, mouseButton)
}
}
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(startPoint).maxByOrNull(View::zIndex)
if (view != null) {
val startInView = convert(startPoint, to = view)
return view.mouseDragged(startInView, delta, mouseButton)
}
return false
}
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(startPoint).maxByOrNull(View::zIndex)
if (view != null) {
val startInView = convert(startPoint, to = view)
return view.mouseDragged(startInView, delta, mouseButton)
}
return false
}
open fun mouseDragEnded(point: Point, mouseButton: MouseButton) {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
if (view != null) {
val pointInView = convert(point, to = view)
return view.mouseDragEnded(pointInView, mouseButton)
}
}
open fun mouseDragEnded(point: Point, mouseButton: MouseButton) {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
if (view != null) {
val pointInView = convert(point, to = view)
return view.mouseDragEnded(pointInView, mouseButton)
}
}
open fun mouseScrolled(point: Point, amount: Double): Boolean {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
if (view != null) {
val pointInView = convert(point, to = view)
return view.mouseScrolled(pointInView, amount)
}
return false
}
open fun mouseScrolled(point: Point, amount: Double): Boolean {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
if (view != null) {
val pointInView = convert(point, to = view)
return view.mouseScrolled(pointInView, amount)
}
return false
}
/**
* 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 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.
*/
fun convert(point: Point, to: View?): Point {
if (to != null) {
val ancestor = LowestCommonAncestor.find(this, to, View::superview)
@Suppress("NAME_SHADOWING") var point = point
/**
* 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 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.
*/
fun convert(point: Point, to: View?): Point {
if (to != null) {
val ancestor = LowestCommonAncestor.find(this, to, View::superview)
@Suppress("NAME_SHADOWING") var point = point
// Convert up to the LCA
var view: View? = this
while (view != null && view != ancestor) {
point = Point(point.x + view.frame.left, point.y + view.frame.top)
view = view.superview
}
// Convert up to the LCA
var view: View? = this
while (view != null && view != ancestor) {
point = Point(point.x + view.frame.left, point.y + view.frame.top)
view = view.superview
}
// Convert back down to the other view
view = to
while (view != null && view != ancestor) {
point = Point(point.x - view.frame.left, point.y - view.frame.top)
view = view.superview
}
// Convert back down to the other view
view = to
while (view != null && view != ancestor) {
point = Point(point.x - view.frame.left, point.y - view.frame.top)
view = view.superview
}
return point
} else {
return Point(leftAnchor.value + point.x, topAnchor.value + point.y)
}
}
return point
} else {
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.
*
* @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.
* @return The rectangle in the coordinate system of the [to] view.
*/
fun convert(rect: Rect, to: View?): Rect {
return Rect(convert(rect.origin, to), rect.size)
}
/**
* 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 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.
*/
fun convert(rect: Rect, to: View?): Rect {
return Rect(convert(rect.origin, to), rect.size)
}
}
private fun LayoutGuide.attachTo(solver: Solver) {
solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
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.
* @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 parameter is the type of the concrete button implementation that was used.
*/
var handler: ((Impl) -> Unit)? = null
/**
* The function that handles when this button is clicked.
* The parameter is the type of the concrete button implementation that was used.
*/
var handler: ((Impl) -> Unit)? = null
/**
* Whether the button is disabled.
* Disabled buttons have a different background ([disabledBackground]) and do not receive click events.
*/
var disabled = false
/**
* Whether the button is disabled.
* Disabled buttons have a different background ([disabledBackground]) and do not receive click events.
*/
var disabled = false
/**
* 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.
*
* The background will fill the entire button (going beneath the content [padding]).
* 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
* unless the background view is not fully opaque.
*/
var background: View? = NinePatchView(NinePatchTexture.BUTTON_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The background to draw when the button is hovered over by the mouse.
* If `null`, the normal [background] will be used.
* @see background
*/
var hoveredBackground: View? = NinePatchView(NinePatchTexture.BUTTON_HOVERED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used.
* @see background
*/
var disabledBackground: View? = NinePatchView(NinePatchTexture.BUTTON_DISABLED_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The 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.
*
* The background will fill the entire button (going beneath the content [padding]).
* 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
* unless the background view is not fully opaque.
*/
var background: View? = NinePatchView(NinePatchTexture.BUTTON_BG)
set(value) {
field?.removeFromSuperview()
field = value
value?.also(::addBackground)
}
/**
* The tooltip text shown when this button is hovered.
*/
var tooltip: Text? = null
/**
* 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)
}
override fun wasAdded() {
solver.dsl {
addSubview(content)
content.centerXAnchor equalTo centerXAnchor
content.centerYAnchor equalTo centerYAnchor
/**
* 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)
}
content.leftAnchor.lessThanOrEqualTo((leftAnchor + padding), WEAK)
content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK)
content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK)
content.bottomAnchor.greaterThanOrEqualTo(bottomAnchor - padding, WEAK)
}
/**
* The tooltip text shown when this button is hovered.
*/
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) {
if (superview != null && hasSolver) {
addSubview(view)
solver.dsl {
view.leftAnchor equalTo leftAnchor
view.rightAnchor equalTo rightAnchor
view.topAnchor equalTo topAnchor
view.bottomAnchor equalTo bottomAnchor
}
}
}
listOfNotNull(background, hoveredBackground, disabledBackground).forEach(::addBackground)
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.translate(frame.left, frame.top, 0.0)
super.wasAdded()
}
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
// the edges are all pinned, so the coordinate space is the same
getCurrentBackground(mouse)?.draw(matrixStack, mouse, delta)
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.translate(frame.left, frame.top, 0.0)
val mouseInContent = convert(mouse, to = content)
content.draw(matrixStack, mouseInContent, delta)
RenderHelper.fill(matrixStack, bounds, backgroundColor)
// 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) {
window!!.drawTooltip(listOf(tooltip!!))
}
}
// don't draw subviews, otherwise all background views + content will get drawn
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (disabled) return false
matrixStack.pop()
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
// of AbstractButton.
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
if (tooltip != null && mouse in bounds) {
window!!.drawTooltip(listOf(tooltip!!))
}
}
return true
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (disabled) return false
protected open fun getCurrentBackground(mouse: Point): View? {
return if (disabled) {
disabledBackground ?: background
} else if (mouse in bounds) {
hoveredBackground ?: background
} else {
background
}
}
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
// of AbstractButton.
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
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.
*/
class Button(
content: View,
padding: Double = 4.0,
handler: ((Button) -> Unit)? = null
): AbstractButton<Button>(content, padding) {
init {
this.handler = handler
}
content: View,
padding: Double = 4.0,
handler: ((Button) -> Unit)? = null
) : AbstractButton<Button>(content, padding) {
init {
this.handler = handler
}
}

View File

@ -33,62 +33,62 @@ import net.shadowfacts.kiwidsl.dsl
* 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.
*/
class DropdownButton<Value, ContentView: View>(
val initialValue: Value,
val allValues: Iterable<Value>,
val createView: (Value) -> ContentView,
val updateView: (newValue: Value, view: ContentView) -> Unit,
padding: Double = 4.0
): AbstractButton<DropdownButton<Value, ContentView>>(
StackView(Axis.HORIZONTAL),
padding
class DropdownButton<Value, ContentView : View>(
val initialValue: Value,
val allValues: Iterable<Value>,
val createView: (Value) -> ContentView,
val updateView: (newValue: Value, view: ContentView) -> Unit,
padding: Double = 4.0
) : AbstractButton<DropdownButton<Value, ContentView>>(
StackView(Axis.HORIZONTAL),
padding
) {
companion object {
val DROPDOWN_INDICATOR = Texture(Identifier("asmr", "textures/gui/dropdown.png"), 0, 0)
}
companion object {
val DROPDOWN_INDICATOR = Texture(Identifier("asmr", "textures/gui/dropdown.png"), 0, 0)
}
private val stackView: StackView
get() = content as StackView
private val stackView: StackView
get() = content as StackView
private val contentView: ContentView
get() = stackView.arrangedSubviews.first() as ContentView
private val contentView: ContentView
get() = stackView.arrangedSubviews.first() as ContentView
private lateinit var dropdownIndicator: TextureView
private lateinit var dropdownIndicator: TextureView
/**
* The currently selected [Value] of the dropdown.
*/
var value: Value = initialValue
set(value) {
field = value
updateView(value, contentView)
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
/**
* The currently selected [Value] of the dropdown.
*/
var value: Value = initialValue
set(value) {
field = value
updateView(value, contentView)
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
override fun wasAdded() {
super.wasAdded()
override fun wasAdded() {
super.wasAdded()
stackView.addArrangedSubview(createView(initialValue))
dropdownIndicator = stackView.addArrangedSubview(TextureView(DROPDOWN_INDICATOR))
stackView.addArrangedSubview(createView(initialValue))
dropdownIndicator = stackView.addArrangedSubview(TextureView(DROPDOWN_INDICATOR))
solver.dsl {
dropdownIndicator.widthAnchor equalTo 9
dropdownIndicator.heightAnchor equalTo 9
}
}
solver.dsl {
dropdownIndicator.widthAnchor equalTo 9
dropdownIndicator.heightAnchor equalTo 9
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
return if (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT) {
showDropdown()
true
} else {
super.mouseClicked(point, mouseButton)
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
return if (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT) {
showDropdown()
true
} else {
super.mouseClicked(point, mouseButton)
}
}
private fun showDropdown() {
private fun showDropdown() {
// val dropdownWindow = window.screen.addWindow(Window())
// val dropdownBackground = dropdownWindow.addView(NinePatchView(NinePatchTexture.BUTTON_BG).apply {
// zIndex = -1.0
@ -131,76 +131,96 @@ class DropdownButton<Value, ContentView: View>(
// dropdownBackground.bottomAnchor equalTo stack.bottomAnchor
// }
// dropdownWindow.layout()
}
}
private fun valueSelected(value: Value) {
this.value = value
handler?.invoke(this)
}
private fun valueSelected(value: Value) {
this.value = value
handler?.invoke(this)
}
}
private class DropdownItemBackgroundView(
private val first: Boolean,
private val last: Boolean,
ninePatch: NinePatchTexture
): NinePatchView(ninePatch) {
private val first: Boolean,
private val last: Boolean,
ninePatch: NinePatchTexture
) : NinePatchView(ninePatch) {
// Corners
private val topLeftDelegate = ResettableLazyProperty {
super.topLeft
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), if (first) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val topLeft by topLeftDelegate
// Corners
private val topLeftDelegate = ResettableLazyProperty {
super.topLeft
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), if (first) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, topLeft.width, topLeft.height)
}
override val topRight by topRightDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, topLeft.width, topLeft.height)
}
override val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(topLeft.left, bounds.height - ninePatch.cornerHeight, topLeft.width, if (last) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val bottomLeft by bottomLeftDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(
topLeft.left,
bounds.height - ninePatch.cornerHeight,
topLeft.width,
if (last) ninePatch.cornerHeight.toDouble() else 0.0
)
}
override val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(topRight.left, bottomLeft.top, topLeft.width, bottomLeft.height)
}
override val bottomRight by bottomRightDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(topRight.left, bottomLeft.top, topLeft.width, bottomLeft.height)
}
override val bottomRight by bottomRightDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, topLeft.height)
}
override val topMiddle by topMiddleDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, topLeft.height)
}
override val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, bottomLeft.height)
}
override val bottomMiddle by bottomMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, bottomLeft.height)
}
override val bottomMiddle by bottomMiddleDelegate
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)
}
override val leftMiddle by leftMiddleDelegate
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
)
}
override val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, topRight.bottom, topRight.width, leftMiddle.height)
}
override val rightMiddle by rightMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, topRight.bottom, topRight.width, leftMiddle.height)
}
override val rightMiddle by rightMiddleDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topMiddle.bottom, topMiddle.width, leftMiddle.height)
}
override val center by centerDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topMiddle.bottom, topMiddle.width, leftMiddle.height)
}
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() {
super.didLayout()
override fun 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 localizer A function that takes an enum value and converts into a [Text] for the button's label.
*/
class EnumButton<E: Enum<E>>(
initialValue: E,
val localizer: (E) -> Text
): AbstractButton<EnumButton<E>>(
Label(localizer(initialValue), shadow = true)
class EnumButton<E : Enum<E>>(
initialValue: E,
val localizer: (E) -> Text
) : AbstractButton<EnumButton<E>>(
Label(localizer(initialValue), shadow = true)
) {
private val label: Label
get() = content as Label
private val label: Label
get() = content as Label
/**
* The current value of the enum button.
* Updating this property will use the [localizer] to update the label.
*/
var value: E = initialValue
set(value) {
field = value
label.text = localizer(value)
}
/**
* The current value of the enum button.
* Updating this property will use the [localizer] to update the label.
*/
var value: E = initialValue
set(value) {
field = value
label.text = localizer(value)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value)
else -> {
return false
}
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value)
else -> {
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.
*/
class ToggleButton(
initialState: Boolean,
handler: ((ToggleButton) -> Unit)? = null,
): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF), padding = 0.0) {
initialState: Boolean,
handler: ((ToggleButton) -> Unit)? = null,
) : AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF), padding = 0.0) {
companion object {
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 ON = Texture(Identifier("textures/gui/checkbox.png"), 0, 20, 64, 64)
val ON_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 20, 64, 64)
}
companion object {
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 ON = Texture(Identifier("textures/gui/checkbox.png"), 0, 20, 64, 64)
val ON_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 20, 64, 64)
}
private val textureView: TextureView
get() = content as TextureView
private val textureView: TextureView
get() = content as TextureView
/**
* The button's current on/off state.
* Updating this property updates the button's texture.
*/
var state: Boolean = initialState
/**
* The button's current on/off state.
* Updating this property updates the button's texture.
*/
var state: Boolean = initialState
init {
this.handler = handler
intrinsicContentSize = Size(20.0, 20.0)
init {
this.handler = handler
intrinsicContentSize = Size(20.0, 20.0)
background = null
disabledBackground = null
hoveredBackground = null
}
background = null
disabledBackground = null
hoveredBackground = null
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
state = !state
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
state = !state
}
return super.mouseClicked(point, mouseButton)
}
return super.mouseClicked(point, mouseButton)
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
val hovered = mouse in bounds
textureView.texture = if (state) {
if (hovered) ON_HOVERED else ON
} else {
if (hovered) OFF_HOVERED else OFF
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
val hovered = mouse in bounds
textureView.texture = if (state) {
if (hovered) ON_HOVERED else ON
} else {
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.
* @param initialText The initial value of the text field.
*/
abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
initialText: String
): View() {
abstract class AbstractTextField<Impl : AbstractTextField<Impl>>(
initialText: String
) : View() {
/**
* A function that is invoked when the text in this text field changes.
*/
var handler: ((Impl) -> Unit)? = null
/**
* A function that is invoked when the text in this text field changes.
*/
var handler: ((Impl) -> Unit)? = null
/**
* Whether the text field is disabled.
* Disabled text fields cannot be interacted with.
*/
var disabled = false
/**
* Whether the text field is disabled.
* Disabled text fields cannot be interacted with.
*/
var disabled = false
/**
* Whether this text field is focused (i.e. [isFirstResponder]) and receives key events.
*/
val focused: Boolean
get() = isFirstResponder
/**
* Whether this text field is focused (i.e. [isFirstResponder]) and receives key events.
*/
val focused: Boolean
get() = isFirstResponder
/**
* The current text of this text field.
*/
var text: String
get() = minecraftWidget.text
set(value) {
minecraftWidget.text = value
}
/**
* The current text of this text field.
*/
var text: String
get() = minecraftWidget.text
set(value) {
minecraftWidget.text = value
}
/**
* 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).
*/
var maxLength: Int
get() = (minecraftWidget as TextFieldWidgetAccessor).cacao_getMaxLength()
set(value) {
minecraftWidget.setMaxLength(value)
}
/**
* 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).
*/
var maxLength: Int
get() = (minecraftWidget as TextFieldWidgetAccessor).cacao_getMaxLength()
set(value) {
minecraftWidget.setMaxLength(value)
}
/**
* Whether the Minecraft builtin black background and border are drawn. Defaults to true.
*/
var drawBackground = true
set(value) {
field = value
minecraftWidget.setDrawsBackground(value)
}
/**
* Whether the Minecraft builtin black background and border are drawn. Defaults to true.
*/
var drawBackground = true
set(value) {
field = value
minecraftWidget.setDrawsBackground(value)
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setDrawsBackground(drawBackground)
}
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setDrawsBackground(drawBackground)
}
/**
* A function used by subclasses to determine whether a proposed value is acceptable for this field.
*/
abstract fun validate(proposedText: String): Boolean
/**
* A function used by subclasses to determine whether a proposed value is acceptable for this field.
*/
abstract fun validate(proposedText: String): Boolean
override fun didLayout() {
super.didLayout()
override fun 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
minecraftWidget.x = originInWindow.x.toInt() + 1
minecraftWidget.y = originInWindow.y.toInt() + 1
minecraftWidget.width = bounds.width.toInt() - 2
minecraftWidget.height = bounds.height.toInt() - 2
// offset View dimensions by 1 on each side because TextFieldWidget draws the border outside its dimensions
minecraftWidget.x = originInWindow.x.toInt() + 1
minecraftWidget.y = originInWindow.y.toInt() + 1
minecraftWidget.width = bounds.width.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
minecraftWidget.text = text
}
// after dimensions change call setText on the widget to make sure its internal scroll position is up-to-date
minecraftWidget.text = text
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.translate(-originInWindow.x, -originInWindow.y, 0.0)
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
matrixStack.push()
matrixStack.translate(-originInWindow.x, -originInWindow.y, 0.0)
val mouseXInWindow = (mouse.x + originInWindow.x).toInt()
val mouseYInWindow = (mouse.y + originInWindow.y).toInt()
minecraftWidget.render(matrixStack, mouseXInWindow, mouseYInWindow, delta)
val mouseXInWindow = (mouse.x + originInWindow.x).toInt()
val mouseYInWindow = (mouse.y + originInWindow.y).toInt()
minecraftWidget.render(matrixStack, mouseXInWindow, mouseYInWindow, delta)
matrixStack.pop()
}
matrixStack.pop()
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
if (focused) {
val mouseXInWindow = (point.x + originInWindow.x)
val mouseYInWindow = (point.y + originInWindow.y)
minecraftWidget.mouseClicked(mouseXInWindow, mouseYInWindow, mouseButton.ordinal)
} else {
becomeFirstResponder()
}
}
// don't play sound when interacting with text field
return false
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
if (focused) {
val mouseXInWindow = (point.x + originInWindow.x)
val mouseYInWindow = (point.y + originInWindow.y)
minecraftWidget.mouseClicked(mouseXInWindow, mouseYInWindow, mouseButton.ordinal)
} else {
becomeFirstResponder()
}
}
// don't play sound when interacting with text field
return false
}
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
if (focused) {
resignFirstResponder()
}
}
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
if (focused) {
resignFirstResponder()
}
}
override fun didBecomeFirstResponder() {
super.didBecomeFirstResponder()
minecraftWidget.setTextFieldFocused(true)
}
override fun didBecomeFirstResponder() {
super.didBecomeFirstResponder()
minecraftWidget.setTextFieldFocused(true)
}
override fun didResignFirstResponder() {
super.didResignFirstResponder()
minecraftWidget.setTextFieldFocused(false)
}
override fun didResignFirstResponder() {
super.didResignFirstResponder()
minecraftWidget.setTextFieldFocused(false)
}
override fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
val oldText = text
val result = minecraftWidget.charTyped(char, modifiers.value)
if (text != oldText) {
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result
}
override fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
val oldText = text
val result = minecraftWidget.charTyped(char, modifiers.value)
if (text != oldText) {
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result
}
override fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
val oldText = text
// scanCode isn't used by TextFieldWidget, hopefully this doesn't break :/
val result = minecraftWidget.keyPressed(keyCode, -1, modifiers.value)
if (text != oldText) {
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE)
}
override fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
val oldText = text
// scanCode isn't used by TextFieldWidget, hopefully this doesn't break :/
val result = minecraftWidget.keyPressed(keyCode, -1, modifiers.value)
if (text != oldText) {
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE)
}
fun tick() {
minecraftWidget.tick()
}
fun tick() {
minecraftWidget.tick()
}
// todo: label for the TextFieldWidget?
private class ProxyWidget: TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
// AbstractButtonWidget.height is protected
fun setHeight(height: Int) {
this.height = height
}
}
// todo: label for the TextFieldWidget?
private class ProxyWidget :
TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
// AbstractButtonWidget.height is protected
fun setHeight(height: Int) {
this.height = height
}
}
}

View File

@ -4,43 +4,43 @@ package net.shadowfacts.cacao.view.textfield
* @author shadowfacts
*/
open class NumberField(
initialValue: Int,
handler: ((NumberField) -> Unit)? = null,
): AbstractTextField<NumberField>(initialValue.toString()) {
initialValue: Int,
handler: ((NumberField) -> Unit)? = null,
) : AbstractTextField<NumberField>(initialValue.toString()) {
var number: Int?
get() {
return if (isTextTemporarilyAllowed(text)) {
null
} else {
try {
Integer.parseInt(text)
} catch (e: NumberFormatException) {
null
}
}
}
set(value) {
text = value?.toString() ?: ""
}
var number: Int?
get() {
return if (isTextTemporarilyAllowed(text)) {
null
} else {
try {
Integer.parseInt(text)
} catch (e: NumberFormatException) {
null
}
}
}
set(value) {
text = value?.toString() ?: ""
}
var validator: ((Int) -> Boolean)? = null
var validator: ((Int) -> Boolean)? = null
init {
this.handler = handler
}
init {
this.handler = handler
}
override fun validate(proposedText: String): Boolean {
return isTextTemporarilyAllowed(proposedText) || try {
val value = Integer.parseInt(proposedText)
validator?.invoke(value) ?: true
} catch (e: NumberFormatException) {
false
}
}
override fun validate(proposedText: String): Boolean {
return isTextTemporarilyAllowed(proposedText) || try {
val value = Integer.parseInt(proposedText)
validator?.invoke(value) ?: true
} catch (e: NumberFormatException) {
false
}
}
private fun isTextTemporarilyAllowed(s: String): Boolean {
return s.isEmpty() || s == "-"
}
private fun isTextTemporarilyAllowed(s: String): Boolean {
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.
*/
open class TextField(
initialText: String,
handler: ((TextField) -> Unit)? = null
): AbstractTextField<TextField>(initialText) {
init {
this.handler = handler
}
initialText: String,
handler: ((TextField) -> Unit)? = null
) : AbstractTextField<TextField>(initialText) {
init {
this.handler = handler
}
override fun validate(proposedText: String): Boolean {
return true
}
override fun validate(proposedText: String): Boolean {
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
* updated).
*/
class TabViewController<T: TabViewController.Tab>(
val tabs: List<T>,
initialTab: T = tabs.first(),
val onTabChange: ((T) -> Unit)? = null
): ViewController() {
class TabViewController<T : TabViewController.Tab>(
val tabs: List<T>,
initialTab: T = tabs.first(),
val onTabChange: ((T) -> Unit)? = null
) : ViewController() {
/**
* 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.
* @see SimpleTab
*/
interface Tab {
/**
* The view displayed on the button for this tab.
*/
val tabView: View
/**
* 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.
* @see SimpleTab
*/
interface Tab {
/**
* The view displayed on the button for this tab.
*/
val tabView: View
/**
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
*/
val tooltip: Text?
/**
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
*/
val tooltip: Text?
/**
* 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.
*/
val controller: ViewController
/**
* 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.
*/
val controller: ViewController
/**
* 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].
*/
val isVisible: Boolean
get() = true
}
/**
* 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].
*/
val isVisible: Boolean
get() = true
}
/**
* A simple [Tab] implementation that provides the minimum necessary information.
* @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 controller The content view controller for this tab.
* @param visible A function that determines if the tab should currently be visible.
*/
class SimpleTab(
override val tabView: View,
override val tooltip: Text? = null,
override val controller: ViewController,
private val visible: (() -> Boolean)? = null
): Tab {
override val isVisible: Boolean
get() = visible?.invoke() ?: true
}
/**
* A simple [Tab] implementation that provides the minimum necessary information.
* @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 controller The content view controller for this tab.
* @param visible A function that determines if the tab should currently be visible.
*/
class SimpleTab(
override val tabView: View,
override val tooltip: Text? = null,
override val controller: ViewController,
private val visible: (() -> Boolean)? = null
) : Tab {
override val isVisible: Boolean
get() = visible?.invoke() ?: true
}
/**
* The currently selected tab.
*/
var currentTab: T = initialTab
private set
/**
* The currently selected tab.
*/
var currentTab: T = initialTab
private set
private lateinit var tabButtons: List<TabButton<T>>
private lateinit var tabButtons: List<TabButton<T>>
private lateinit var outerStack: StackView
private lateinit var tabStack: StackView
private lateinit var currentTabController: ViewController
// todo: this shouldn't be public, use layout guides
lateinit var tabVCContainer: View
private set
private lateinit var outerStack: StackView
private lateinit var tabStack: StackView
private lateinit var currentTabController: ViewController
override fun viewDidLoad() {
super.viewDidLoad()
// todo: this shouldn't be public, use layout guides
lateinit var tabVCContainer: View
private set
// todo: might be simpler to just not use a stack view
// padding is -4 so tab button texture overlaps with panel BG as expected
outerStack = StackView(Axis.VERTICAL, StackView.Distribution.FILL, -4.0)
view.addSubview(outerStack)
override fun viewDidLoad() {
super.viewDidLoad()
tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL)
tabStack.zIndex = 1.0
outerStack.addArrangedSubview(tabStack)
updateTabButtons()
// todo: might be simpler to just not use a stack view
// padding is -4 so tab button texture overlaps with panel BG as expected
outerStack = StackView(Axis.VERTICAL, StackView.Distribution.FILL, -4.0)
view.addSubview(outerStack)
val background = NinePatchView(NinePatchTexture.PANEL_BG)
outerStack.addArrangedSubview(background)
tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL)
tabStack.zIndex = 1.0
outerStack.addArrangedSubview(tabStack)
updateTabButtons()
tabVCContainer = View()
tabVCContainer.zIndex = 1.0
view.addSubview(tabVCContainer)
val background = NinePatchView(NinePatchTexture.PANEL_BG)
outerStack.addArrangedSubview(background)
currentTabController = currentTab.controller
currentTabController.willMoveTo(this)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
// will/did appear events for the initial VC are provided by this class' implementations of those
tabVCContainer = View()
tabVCContainer.zIndex = 1.0
view.addSubview(tabVCContainer)
view.solver.dsl {
outerStack.leftAnchor equalTo view.leftAnchor
outerStack.rightAnchor equalTo view.rightAnchor
outerStack.topAnchor equalTo view.topAnchor
outerStack.bottomAnchor equalTo view.bottomAnchor
currentTabController = currentTab.controller
currentTabController.willMoveTo(this)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
// will/did appear events for the initial VC are provided by this class' implementations of those
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
}
view.solver.dsl {
outerStack.leftAnchor equalTo view.leftAnchor
outerStack.rightAnchor equalTo view.rightAnchor
outerStack.topAnchor equalTo view.topAnchor
outerStack.bottomAnchor equalTo view.bottomAnchor
override fun viewWillAppear() {
super.viewWillAppear()
currentTabController.viewWillAppear()
}
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
}
override fun viewWillDisappear() {
super.viewWillDisappear()
currentTabController.viewWillDisappear()
}
override fun viewWillAppear() {
super.viewWillAppear()
currentTabController.viewWillAppear()
}
override fun viewDidDisappear() {
super.viewDidDisappear()
currentTabController.viewDidDisappear()
}
override fun viewWillDisappear() {
super.viewWillDisappear()
currentTabController.viewWillDisappear()
}
private fun updateTabButtons() {
while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first())
override fun viewDidDisappear() {
super.viewDidDisappear()
currentTabController.viewDidDisappear()
}
tabButtons = tabs.mapNotNull { tab ->
if (!tab.isVisible) {
return@mapNotNull null
}
private fun updateTabButtons() {
while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first())
val btn = TabButton(tab)
btn.handler = { selectTab(it.tab) }
if (tab == currentTab) {
btn.setSelected(true)
}
btn
}
// todo: batch calls to addArrangedSubview
tabButtons.forEach(tabStack::addArrangedSubview)
tabButtons = tabs.mapNotNull { tab ->
if (!tab.isVisible) {
return@mapNotNull null
}
// spacer
tabStack.addArrangedSubview(View())
val btn = TabButton(tab)
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())
/**
* Call this method when the conditions that make the configured tabs visible change.
*/
fun visibleTabsChanged() {
updateTabButtons()
}
window!!.layout()
}
/**
* 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")
}
/**
* Call this method when the conditions that make the configured tabs visible change.
*/
fun visibleTabsChanged() {
updateTabButtons()
}
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 {
it.setSelected(it.tab === tab)
}
currentTabController.viewWillDisappear()
currentTabController.view.removeFromSuperview()
currentTabController.viewDidDisappear()
currentTabController.willMoveTo(null)
currentTabController.removeFromParent()
currentTabController.didMoveTo(null)
val oldTab = currentTab
currentTab = tab
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)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
currentTabController.viewWillAppear()
currentTabController = currentTab.controller
onTabChange?.invoke(currentTab)
currentTabController.willMoveTo(this)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
currentTabController.viewWillAppear()
// todo: setNeedsLayout
window!!.layout()
}
onTabChange?.invoke(currentTab)
private class TabButton<T: Tab>(
val tab: T,
): AbstractButton<TabButton<T>>(
tab.tabView,
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("phycon:textures/gui/tabs.png")
}
// todo: setNeedsLayout
window!!.layout()
}
private var selected = false
private var backgroundView = TextureView(Texture(BACKGROUND, 0, 0))
private class TabButton<T : Tab>(
val tab: T,
) : AbstractButton<TabButton<T>>(
tab.tabView,
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("phycon:textures/gui/tabs.png")
}
init {
intrinsicContentSize = Size(28.0, 32.0)
background = null
hoveredBackground = null
disabledBackground = null
}
private var selected = false
private var backgroundView = TextureView(Texture(BACKGROUND, 0, 0))
override fun wasAdded() {
super.wasAdded()
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)
}
}
init {
intrinsicContentSize = Size(28.0, 32.0)
background = null
hoveredBackground = null
disabledBackground = null
}
override fun didLayout() {
super.didLayout()
updateBackgroundTexture()
}
override fun wasAdded() {
super.wasAdded()
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) {
this.selected = selected
updateBackgroundTexture()
}
override fun didLayout() {
super.didLayout()
updateBackgroundTexture()
}
override fun getCurrentBackground(mouse: Point) = backgroundView
fun setSelected(selected: Boolean) {
this.selected = selected
updateBackgroundTexture()
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
super.draw(matrixStack, mouse, delta)
override fun getCurrentBackground(mouse: Point) = backgroundView
if (mouse in bounds && tab.tooltip != null) {
window!!.drawTooltip(listOf(tab.tooltip!!))
}
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
super.draw(matrixStack, mouse, delta)
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (selected) return false
else return super.mouseClicked(point, mouseButton)
}
if (mouse in bounds && tab.tooltip != null) {
window!!.drawTooltip(listOf(tab.tooltip!!))
}
}
private fun updateBackgroundTexture() {
val v = if (selected) 32 else 0
val u = when {
superview == null -> 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)
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (selected) return false
else return super.mouseClicked(point, mouseButton)
}
private fun updateBackgroundTexture() {
val v = if (selected) 32 else 0
val u = when {
superview == null -> 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 {
/**
* The window that contains this view controller.
* This property is not set until either:
* 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.
*/
var window: Window? = null
set(value) {
field = value
for (vc in children) {
vc.window = value
}
}
/**
* The window that contains this view controller.
* This property is not set until either:
* 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.
*/
var window: Window? = null
set(value) {
field = value
for (vc in children) {
vc.window = value
}
}
/**
* Helper function for creating layout constraints in the domain of this VC's window.
* This function is not usable until [window] is initialized.
*/
val createConstraints
get() = window!!.solver::dsl
/**
* Helper function for creating layout constraints in the domain of this VC's window.
* This function is not usable until [window] is initialized.
*/
val createConstraints
get() = window!!.solver::dsl
/**
* The view that this View Controller has.
* This property is created by [loadView] and is not initialized before that method has been called.
*
* @see loadView
*/
lateinit var view: View
protected set
/**
* The view that this View Controller has.
* This property is created by [loadView] and is not initialized before that method has been called.
*
* @see loadView
*/
lateinit var view: View
protected set
/**
* This VC's parent view controller. If `null`, this VC is the root view controller of its [window].
*/
var parent: ViewController? = null
set(value) {
willMoveTo(value)
field = value
didMoveTo(value)
}
/**
* This VC's parent view controller. If `null`, this VC is the root view controller of its [window].
*/
var parent: ViewController? = null
set(value) {
willMoveTo(value)
field = value
didMoveTo(value)
}
// _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods
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
// _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods
private var _children = LinkedList<ViewController>()
/**
* This method somehow loads a [View] and sets this VC's [view] property to it.
*
* 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].
*
* The default implementation simply creates a [View] and does nothing else with it.
*/
open fun loadView() {
view = View()
}
/**
* 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
/**
* If the view for this controller has already been loaded.
*/
val isViewLoaded: Boolean
get() = ::view.isInitialized
/**
* This method somehow loads a [View] and sets this VC's [view] property to it.
*
* 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].
*
* 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.
*/
fun loadViewIfNeeded() {
if (!isViewLoaded) {
loadView()
}
}
/**
* If the view for this controller has already been loaded.
*/
val isViewLoaded: Boolean
get() = ::view.isInitialized
/**
* This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been
* called.
*/
open fun viewDidLoad() {}
/**
* Calls [loadView] to load this controller's view only if it has not already been loaded.
*/
fun loadViewIfNeeded() {
if (!isViewLoaded) {
loadView()
}
}
/**
* This method is called immediately before the [Window.solver] is going to solve constraints and update variables.
* If overridden, the superclass method must be called.
*/
open fun viewWillLayoutSubviews() {
children.forEach(ViewController::viewWillLayoutSubviews)
}
/**
* This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been
* called.
*/
open fun viewDidLoad() {}
/**
* 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.
* If overridden, the superclass method must be called.
*/
open fun viewDidLayoutSubviews() {
view.didLayout()
children.forEach(ViewController::viewDidLayoutSubviews)
}
/**
* This method is called immediately before the [Window.solver] is going to solve constraints and update variables.
* If overridden, the superclass method must be called.
*/
open fun viewWillLayoutSubviews() {
children.forEach(ViewController::viewWillLayoutSubviews)
}
/**
* 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)
}
/**
* 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.
* If overridden, the superclass method must be called.
*/
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.
@ -129,87 +130,87 @@ abstract class ViewController {
// 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
* or because the [net.shadowfacts.cacao.CacaoScreen] has been closed.
*/
open fun viewWillDisappear() {
children.forEach(ViewController::viewWillDisappear)
}
/**
* 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.
*/
open fun viewWillDisappear() {
children.forEach(ViewController::viewWillDisappear)
}
/**
* Called after the view has disappeared from the screen.
*/
open fun viewDidDisappear() {
children.forEach(ViewController::viewDidDisappear)
}
/**
* Called after the view has disappeared from the screen.
*/
open fun viewDidDisappear() {
children.forEach(ViewController::viewDidDisappear)
}
/**
* Called before the view controller's parent changes to the given new value.
*
* @param parent The new parent view controller.
*/
open fun willMoveTo(parent: ViewController?) {}
/**
* Called before the view controller's parent changes to the given new value.
*
* @param parent The new parent view controller.
*/
open fun willMoveTo(parent: ViewController?) {}
/**
* Called after the view controller's parent has changed to the given new value.
*
* @param parent The new parent view controller.
*/
open fun didMoveTo(parent: ViewController?) {}
/**
* Called after the view controller's parent has changed to the given new value.
*
* @param parent The new parent view controller.
*/
open fun didMoveTo(parent: ViewController?) {}
/**
* Embeds a child view controller in this 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 pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges.
* Defaults to `true`.
*/
fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) {
viewController.parent = this
viewController.window = window
_children.add(viewController)
val wasViewLoaded = viewController.isViewLoaded
viewController.loadViewIfNeeded()
/**
* Embeds a child view controller in this 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 pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges.
* Defaults to `true`.
*/
fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) {
viewController.parent = this
viewController.window = window
_children.add(viewController)
val wasViewLoaded = viewController.isViewLoaded
viewController.loadViewIfNeeded()
container.addSubview(viewController.view)
container.addSubview(viewController.view)
if (pinEdges) {
createConstraints {
viewController.view.leftAnchor equalTo container.leftAnchor
viewController.view.rightAnchor equalTo container.rightAnchor
viewController.view.topAnchor equalTo container.topAnchor
viewController.view.bottomAnchor equalTo container.bottomAnchor
}
}
if (pinEdges) {
createConstraints {
viewController.view.leftAnchor equalTo container.leftAnchor
viewController.view.rightAnchor equalTo container.rightAnchor
viewController.view.topAnchor equalTo container.topAnchor
viewController.view.bottomAnchor equalTo container.bottomAnchor
}
}
if (!wasViewLoaded) {
viewController.viewDidLoad()
}
}
if (!wasViewLoaded) {
viewController.viewDidLoad()
}
}
/**
* Removes the given 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.
*/
fun removeChild(viewController: ViewController) {
if (viewController.parent != this) {
throw RuntimeException("Cannot remove child view controller whose parent is not this view controller")
}
/**
* Removes the given 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.
*/
fun removeChild(viewController: ViewController) {
if (viewController.parent != this) {
throw RuntimeException("Cannot remove child view controller whose parent is not this view controller")
}
viewController.parent = null
_children.remove(viewController)
}
viewController.parent = null
_children.remove(viewController)
}
/**
* Removes this view controller from its parent, if it has one.
*/
fun removeFromParent() {
parent?.removeChild(this)
view.removeFromSuperview()
}
/**
* Removes this view controller from its parent, if it has one.
*/
fun removeFromParent() {
parent?.removeChild(this)
view.removeFromSuperview()
}
}

View File

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

View File

@ -27,249 +27,256 @@ import java.lang.RuntimeException
* @param viewController The root view controller for this window.
*/
open class Window(
/**
* The root view controller for this window.
*/
val viewController: ViewController
/**
* The root view controller for this window.
*/
val viewController: ViewController
) {
/**
* 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.
*/
lateinit var screen: AbstractCacaoScreen
/**
* 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.
*/
lateinit var screen: AbstractCacaoScreen
/**
* The constraint solver used by this window and all its views and subviews.
*/
var solver = Solver()
/**
* The constraint solver used by this window and all its views and subviews.
*/
var solver = Solver()
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
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")
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
val leftAnchor = Variable("left")
/**
* 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 anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = Variable("right")
// 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
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
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
viewController.loadViewIfNeeded()
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = Variable("centerX")
viewController.view.window = this
viewController.view.solver = solver
viewController.view.wasAdded()
viewController.createConstraints {
viewController.view.leftAnchor equalTo leftAnchor
viewController.view.rightAnchor equalTo rightAnchor
viewController.view.topAnchor equalTo topAnchor
viewController.view.bottomAnchor equalTo bottomAnchor
}
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
val centerYAnchor = Variable("centerY")
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
/**
* 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
private var currentDragReceiver: View? = null
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
private var currentDeferredTooltip: List<Text>? = null
/**
* Called by the window's [screen] when the Minecraft screen is resized.
* 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()
}
init {
createInternalConstraints()
}
/**
* Convenience method that removes this window from its [screen].
*/
fun removeFromScreen() {
viewController.viewWillDisappear()
screen.removeWindow(this)
viewController.viewDidDisappear()
}
fun wasAdded() {
viewController.window = this
viewController.loadViewIfNeeded()
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
*/
fun layout() {
viewController.viewWillLayoutSubviews()
solver.updateVariables()
viewController.viewDidLayoutSubviews()
}
viewController.view.window = this
viewController.view.solver = solver
viewController.view.wasAdded()
viewController.createConstraints {
viewController.view.leftAnchor equalTo leftAnchor
viewController.view.rightAnchor equalTo rightAnchor
viewController.view.topAnchor equalTo topAnchor
viewController.view.bottomAnchor equalTo bottomAnchor
}
/**
* 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
viewController.viewDidLoad()
val mouseInView = Point(mouse.x - viewController.view.frame.left, mouse.y - viewController.view.frame.top)
viewController.view.draw(matrixStack, mouseInView, delta)
layout()
}
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
/**
* 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
}
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
/**
* 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
}
/**
* Called by the window's [screen] when the Minecraft screen is resized.
* 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 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
}
/**
* Convenience method that removes this window from its [screen].
*/
fun removeFromScreen() {
viewController.viewWillDisappear()
screen.removeWindow(this)
viewController.viewDidDisappear()
}
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
}
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
*/
fun layout() {
viewController.viewWillLayoutSubviews()
solver.updateVariables()
viewController.viewDidLayoutSubviews()
}
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
*/
class KiwiContext(val solver: Solver) {
val REQUIRED = Strength.REQUIRED
val STRONG = Strength.STRONG
val MEDIUM = Strength.MEDIUM
val WEAK = Strength.WEAK
val REQUIRED = Strength.REQUIRED
val STRONG = Strength.STRONG
val MEDIUM = Strength.MEDIUM
val WEAK = Strength.WEAK
// Constraints
infix fun ExpressionConvertible.equalTo(other: ExpressionConvertible): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
// Constraints
infix fun ExpressionConvertible.equalTo(other: ExpressionConvertible): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
fun ExpressionConvertible.equalTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint)
}
fun ExpressionConvertible.equalTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).setStrength(strength)
.apply(solver::addConstraint)
}
infix fun ExpressionConvertible.equalTo(constant: Number): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.equalTo(constant: Number): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
fun ExpressionConvertible.equalTo(constant: Number, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint)
}
fun ExpressionConvertible.equalTo(constant: Number, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).setStrength(strength)
.apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint)
}
fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength)
.apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(constant: Number): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(constant: Number): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
fun ExpressionConvertible.lessThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint)
}
fun ExpressionConvertible.lessThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength)
.apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint)
}
fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength)
.apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint)
}
fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength)
.apply(solver::addConstraint)
}
// Addition
operator fun ExpressionConvertible.plus(other: ExpressionConvertible): Expression {
return Symbolics.add(this.toExpression(), other.toExpression())
}
// Addition
operator fun ExpressionConvertible.plus(other: ExpressionConvertible): Expression {
return Symbolics.add(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.plus(constant: Number): Expression {
return Symbolics.add(this.toExpression(), constant.toDouble())
}
operator fun ExpressionConvertible.plus(constant: Number): Expression {
return Symbolics.add(this.toExpression(), constant.toDouble())
}
// Subtraction
operator fun ExpressionConvertible.minus(other: ExpressionConvertible): Expression {
return Symbolics.subtract(this.toExpression(), other.toExpression())
}
// Subtraction
operator fun ExpressionConvertible.minus(other: ExpressionConvertible): Expression {
return Symbolics.subtract(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.minus(constant: Number): Expression {
return Symbolics.subtract(this.toExpression(), constant.toDouble())
}
operator fun ExpressionConvertible.minus(constant: Number): Expression {
return Symbolics.subtract(this.toExpression(), constant.toDouble())
}
// Multiplication
operator fun ExpressionConvertible.times(other: ExpressionConvertible): Expression {
return Symbolics.multiply(this.toExpression(), other.toExpression())
}
// Multiplication
operator fun ExpressionConvertible.times(other: ExpressionConvertible): Expression {
return Symbolics.multiply(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.times(constant: Number): Expression {
return Symbolics.multiply(this.toExpression(), constant.toDouble())
}
operator fun ExpressionConvertible.times(constant: Number): Expression {
return Symbolics.multiply(this.toExpression(), constant.toDouble())
}
// Division
operator fun ExpressionConvertible.div(other: ExpressionConvertible): Expression {
return Symbolics.divide(this.toExpression(), other.toExpression())
}
// Division
operator fun ExpressionConvertible.div(other: ExpressionConvertible): Expression {
return Symbolics.divide(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.div(constant: Number): Expression {
return Symbolics.divide(this.toExpression(), constant.toDouble())
}
operator fun ExpressionConvertible.div(constant: Number): Expression {
return Symbolics.divide(this.toExpression(), constant.toDouble())
}
}
fun Solver.dsl(init: KiwiContext.() -> Unit): Solver {
KiwiContext(this).init()
return this
KiwiContext(this).init()
return this
}

View File

@ -9,14 +9,15 @@ import net.shadowfacts.phycon.util.SortMode
/**
* @author shadowfacts
*/
object DefaultPlugin: PhyConPlugin {
object DefaultPlugin : PhyConPlugin {
lateinit var SORT_MODE: TerminalSettingKey<SortMode>
private set
lateinit var SORT_MODE: TerminalSettingKey<SortMode>
private set
override fun initializePhyCon(api: PhyConAPI) {
SORT_MODE = api.registerTerminalSetting(Identifier(PhysicalConnectivity.MODID, "sort"), SortMode.COUNT_HIGH_FIRST)
SORT_MODE.setPriority(Int.MAX_VALUE)
}
override fun initializePhyCon(api: PhyConAPI) {
SORT_MODE =
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
*/
object PhyConAPIImpl: PhyConAPI {
object PhyConAPIImpl : PhyConAPI {
override fun <E> registerTerminalSetting(id: Identifier, defaultValue: E): TerminalSettingKey<E> where E: Enum<E>, E: TerminalSetting? {
return TerminalSettings.register(id, defaultValue)
}
override fun <E> registerTerminalSetting(
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
*/
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() {
PhyBlocks.init()
PhyBlockEntities.init()
PhyItems.init()
PhyScreens.init()
override fun onInitialize() {
PhyBlocks.init()
PhyBlockEntities.init()
PhyItems.init()
PhyScreens.init()
registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem)
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)) {
it.initializePhyCon(PhyConAPIImpl)
}
}
for (it in FabricLoader.getInstance().getEntrypoints("phycon", PhyConPlugin::class.java)) {
it.initializePhyCon(PhyConAPIImpl)
}
}
private fun registerGlobalReceiver(receiver: ServerReceiver) {
ServerPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
}
private fun registerGlobalReceiver(receiver: ServerReceiver) {
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.material.RenderMaterial
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.terminal.CraftingTerminalScreen
import net.shadowfacts.phycon.init.PhyScreens
@ -20,36 +21,37 @@ import net.shadowfacts.phycon.util.TerminalSettings
/**
* @author shadowfacts
*/
object PhysicalConnectivityClient: ClientModInitializer {
object PhysicalConnectivityClient : ClientModInitializer {
val terminalSettings = TerminalSettings()
val terminalSettings = TerminalSettings()
var screenMaterial: RenderMaterial? = null
private set
var screenMaterial: RenderMaterial? = null
private set
override fun onInitializeClient() {
ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyModelProvider)
override fun onInitializeClient() {
ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyModelProvider)
RendererAccess.INSTANCE.renderer?.also { renderer ->
screenMaterial = renderer.materialFinder()
.emissive(0, true)
.disableAo(0, true)
.disableDiffuse(0, true)
.find()
RendererAccess.INSTANCE.renderer?.also { renderer ->
screenMaterial = renderer.materialFinder()
.emissive(0, true)
.disableAo(0, true)
.disableDiffuse(0, true)
.find()
ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyExtendedModelProvider)
}
ModelLoadingRegistry.INSTANCE.registerResourceProvider(::PhyExtendedModelProvider)
}
ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen)
ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen)
ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen)
ScreenRegistry.register(PhyScreens.REDSTONE_EMITTER, ::RedstoneEmitterScreen)
ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen)
ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen)
ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen)
ScreenRegistry.register(PhyScreens.REDSTONE_EMITTER, ::RedstoneEmitterScreen)
ScreenRegistry.register(PhyScreens.SWITCH_CONSOLE, ::SwitchConsoleScreen)
registerGlobalReceiver(S2CTerminalUpdateDisplayedItems)
}
registerGlobalReceiver(S2CTerminalUpdateDisplayedItems)
}
private fun registerGlobalReceiver(receiver: ClientReceiver) {
ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
}
private fun registerGlobalReceiver(receiver: ClientReceiver) {
ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
}
}

View File

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

View File

@ -15,27 +15,41 @@ import net.shadowfacts.phycon.api.NetworkComponentBlock
/**
* @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? {
return getBlockEntity(world, pos)!!
}
override fun getNetworkInterfaceForSide(
side: Direction,
state: BlockState,
world: WorldAccess,
pos: BlockPos
): Interface? {
return getBlockEntity(world, pos)!!
}
override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) {
super.onBreak(world, pos, state, player)
getBlockEntity(world, pos)!!.onBreak()
}
override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) {
super.onBreak(world, pos, state, player)
getBlockEntity(world, pos)!!.onBreak()
}
override fun <T: BlockEntity> getTicker(world: World, state: BlockState, type: BlockEntityType<T>): BlockEntityTicker<T>? {
return if (world.isClient) {
null
} else {
BlockEntityTicker { world, blockPos, blockState, blockEntity ->
(blockEntity as DeviceBlockEntity).tick()
}
}
}
override fun <T : BlockEntity> getTicker(
world: World,
state: BlockState,
type: BlockEntityType<T>
): BlockEntityTicker<T>? {
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
*/
abstract class DeviceBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): BlockEntity(type, pos, state),
PacketSink,
PacketSource,
Interface {
abstract class DeviceBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState) :
BlockEntity(type, pos, state),
PacketSink,
PacketSource,
Interface {
companion object {
private const val ARP_RETRY_TIMEOUT = 200
}
companion object {
private const val ARP_RETRY_TIMEOUT = 200
}
var macAddress: MACAddress = MACAddress.random()
protected set
var macAddress: MACAddress = MACAddress.random()
protected set
var ipAddress: IPAddress = IPAddress.random()
protected set
var ipAddress: IPAddress = IPAddress.random()
protected set
private val arpTable = mutableMapOf<IPAddress, MACAddress>()
private val packetQueue = LinkedList<PendingPacket>()
private var cachedDestination: WeakReference<Interface>? = null
private val arpTable = mutableMapOf<IPAddress, MACAddress>()
private val packetQueue = LinkedList<PendingPacket>()
private var cachedDestination: WeakReference<Interface>? = null
var counter: Long = 0
var counter: Long = 0
override fun getIPAddress() = ipAddress
override fun getMACAddress() = macAddress
override fun getIPAddress() = ipAddress
override fun getMACAddress() = macAddress
abstract override fun handle(packet: Packet)
abstract override fun handle(packet: Packet)
private fun doHandlePacket(packet: Packet) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) received packet: {}", this, ipAddress, packet)
when (packet) {
is DeviceRemovedPacket -> {
arpTable.remove(packet.source)
}
is PingPacket -> {
sendPacket(PongPacket(ipAddress, packet.source))
}
}
handle(packet)
}
private fun doHandlePacket(packet: Packet) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) received packet: {}", this, ipAddress, packet)
when (packet) {
is DeviceRemovedPacket -> {
arpTable.remove(packet.source)
}
override fun send(frame: EthernetFrame) {
findDestination()?.receive(frame)
}
is PingPacket -> {
sendPacket(PongPacket(ipAddress, packet.source))
}
}
handle(packet)
}
override fun receive(frame: EthernetFrame) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) received frame from {}: {}", this, ipAddress, 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)
}
}
}
}
override fun send(frame: EthernetFrame) {
findDestination()?.receive(frame)
}
private fun handleARPQuery(frame: ARPQueryFrame) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}), received ARP query for {}", this, ipAddress, frame.queryIP)
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))
}
}
override fun receive(frame: EthernetFrame) {
PhysicalConnectivity.NETWORK_LOGGER.debug(
"{} ({}, {}) received frame from {}: {}",
this,
ipAddress,
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) {
arpTable[frame.query] = frame.source
PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}) received ARP response for {} with {}", this, ipAddress, frame.query, frame.source)
private fun handleARPQuery(frame: ARPQueryFrame) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}), received ARP query for {}", this, ipAddress, frame.queryIP)
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, _) ->
if (packet.destination == frame.query) {
send(BasePacketFrame(packet, macAddress, frame.source))
true
} else {
false
}
}
packetQueue.removeAll(toRemove)
}
private fun handleARPResponse(frame: ARPResponseFrame) {
arpTable[frame.query] = frame.source
PhysicalConnectivity.NETWORK_LOGGER.debug(
"{}, ({}) received ARP response for {} with {}",
this,
ipAddress,
frame.query,
frame.source
)
protected open fun handleNetworkSplit() {
arpTable.clear()
cachedDestination = null
}
val toRemove = packetQueue.filter { (packet, _) ->
if (packet.destination == frame.query) {
send(BasePacketFrame(packet, macAddress, frame.source))
true
} else {
false
}
}
packetQueue.removeAll(toRemove)
}
override fun sendPacket(packet: Packet) {
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))
protected open fun handleNetworkSplit() {
arpTable.clear()
cachedDestination = null
}
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) sending ARP query for {}", this, ipAddress, packet.destination)
send(ARPQueryFrame(packet.destination, ipAddress, macAddress))
}
}
override fun sendPacket(packet: Packet) {
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? {
val cachedDestination = this.cachedDestination?.get()
if (cachedDestination != null) {
return cachedDestination
}
PhysicalConnectivity.NETWORK_LOGGER.debug(
"{} ({}) sending ARP query for {}",
this,
ipAddress,
packet.destination
)
send(ARPQueryFrame(packet.destination, ipAddress, macAddress))
}
}
val sides = (cachedState.block as NetworkComponentBlock).getNetworkConnectedSides(cachedState, world!!, pos)
return when (sides.size) {
0 -> null
1 -> {
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")
}
}
open fun findDestination(): Interface? {
val cachedDestination = this.cachedDestination?.get()
if (cachedDestination != null) {
return cachedDestination
}
override fun cableDisconnected() {
cachedDestination = null
handleNetworkSplit()
}
val sides = (cachedState.block as NetworkComponentBlock).getNetworkConnectedSides(cachedState, world!!, pos)
return when (sides.size) {
0 -> null
1 -> {
NetworkUtil.findConnectedInterface(world!!, pos, sides.first())?.also {
this.cachedDestination = WeakReference(it)
}
}
open fun tick() {
counter++
else -> throw RuntimeException("DeviceBlockEntity.findDestination must be overridden by devices which have more than 1 network connected side")
}
}
if (!world!!.isClient) {
val toRemove = packetQueue.filter { entry ->
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 cableDisconnected() {
cachedDestination = null
handleNetworkSplit()
}
protected open fun toCommonTag(tag: NbtCompound) {
tag.putInt("IPAddress", ipAddress.address)
tag.putLong("MACAddress", macAddress.address)
}
open fun tick() {
counter++
protected open fun fromCommonTag(tag: NbtCompound) {
ipAddress = IPAddress(tag.getInt("IPAddress"))
macAddress = MACAddress(tag.getLong("MACAddress"))
}
if (!world!!.isClient) {
val toRemove = packetQueue.filter { entry ->
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) {
super.writeNbt(tag)
toCommonTag(tag)
}
protected open fun toCommonTag(tag: NbtCompound) {
tag.putInt("IPAddress", ipAddress.address)
tag.putLong("MACAddress", macAddress.address)
}
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
fromCommonTag(tag)
if (tag.getBoolean("_SyncPacket")) {
fromClientTag(tag)
}
}
protected open fun fromCommonTag(tag: NbtCompound) {
ipAddress = IPAddress(tag.getInt("IPAddress"))
macAddress = MACAddress(tag.getLong("MACAddress"))
}
override fun toUpdatePacket(): BlockEntityUpdateS2CPacket {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
toCommonTag(tag)
}
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putBoolean("_SyncPacket", true)
return toClientTag(tag)
}
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
fromCommonTag(tag)
if (tag.getBoolean("_SyncPacket")) {
fromClientTag(tag)
}
}
open fun toClientTag(tag: NbtCompound): NbtCompound {
toCommonTag(tag)
return tag
}
override fun toUpdatePacket(): BlockEntityUpdateS2CPacket {
return BlockEntityUpdateS2CPacket.create(this)
}
open fun fromClientTag(tag: NbtCompound) {
}
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putBoolean("_SyncPacket", true)
return toClientTag(tag)
}
fun markUpdate() {
markDirty()
world!!.updateListeners(pos, cachedState, cachedState, 3)
}
open fun toClientTag(tag: NbtCompound): NbtCompound {
toCommonTag(tag)
return tag
}
fun onBreak() {
if (!world!!.isClient) {
sendPacket(DeviceRemovedPacket(this))
}
}
open fun fromClientTag(tag: NbtCompound) {
}
data class PendingPacket(
val packet: Packet,
var timestamp: Long,
)
fun markUpdate() {
markDirty()
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
*/
abstract class FaceDeviceBlock<T: DeviceBlockEntity>(settings: Settings): DeviceBlock<T>(settings) {
companion object {
val FACING = Properties.FACING
val CABLE_CONNECTION = EnumProperty.of("cable_connection", FaceCableConnection::class.java)
val COLOR = EnumProperty.of("color", DyeColor::class.java)
}
abstract class FaceDeviceBlock<T : DeviceBlockEntity>(settings: Settings) : DeviceBlock<T>(settings) {
companion object {
val FACING = Properties.FACING
val CABLE_CONNECTION = EnumProperty.of("cable_connection", FaceCableConnection::class.java)
val COLOR = EnumProperty.of("color", DyeColor::class.java)
}
enum class FaceCableConnection : StringIdentifiable {
NONE, DOWN, UP, NORTH, SOUTH, WEST, EAST;
enum class FaceCableConnection : StringIdentifiable {
NONE, DOWN, UP, NORTH, SOUTH, WEST, EAST;
companion object {
fun from(dir: Direction?) = when (dir) {
null -> NONE
Direction.DOWN -> DOWN
Direction.UP -> UP
Direction.NORTH -> NORTH
Direction.SOUTH -> SOUTH
Direction.WEST -> WEST
Direction.EAST -> EAST
}
}
companion object {
fun from(dir: Direction?) = when (dir) {
null -> NONE
Direction.DOWN -> DOWN
Direction.UP -> UP
Direction.NORTH -> NORTH
Direction.SOUTH -> SOUTH
Direction.WEST -> WEST
Direction.EAST -> EAST
}
}
val direction: Direction?
get() = when (this) {
NONE -> null
DOWN -> Direction.DOWN
UP -> Direction.UP
NORTH -> Direction.NORTH
SOUTH -> Direction.SOUTH
WEST -> Direction.WEST
EAST -> Direction.EAST
}
val direction: Direction?
get() = when (this) {
NONE -> null
DOWN -> Direction.DOWN
UP -> Direction.UP
NORTH -> Direction.NORTH
SOUTH -> Direction.SOUTH
WEST -> Direction.WEST
EAST -> Direction.EAST
}
override fun asString() = name.toLowerCase()
}
override fun asString() = name.toLowerCase()
}
protected abstract val faceThickness: Double
abstract val faceShapes: Map<Direction, VoxelShape>
private val centerShapes: Map<Direction, VoxelShape> by lazy {
mapOf(
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.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.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)
)
}
private val shapeCache = mutableMapOf<Pair<Direction, FaceCableConnection>, VoxelShape>()
protected abstract val faceThickness: Double
abstract val faceShapes: Map<Direction, VoxelShape>
private val centerShapes: Map<Direction, VoxelShape> by lazy {
mapOf(
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.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.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)
)
}
private val shapeCache = mutableMapOf<Pair<Direction, FaceCableConnection>, VoxelShape>()
private fun getShape(facing: Direction, cableConnection: FaceCableConnection): VoxelShape {
return shapeCache.getOrPut(facing to cableConnection) {
if (cableConnection == FaceCableConnection.NONE) {
VoxelShapes.union(faceShapes[facing], centerShapes[facing])
} else {
VoxelShapes.union(faceShapes[facing], centerShapes[facing], CableBlock.SIDE_SHAPES[cableConnection.direction])
}
}
}
private fun getShape(facing: Direction, cableConnection: FaceCableConnection): VoxelShape {
return shapeCache.getOrPut(facing to cableConnection) {
if (cableConnection == FaceCableConnection.NONE) {
VoxelShapes.union(faceShapes[facing], centerShapes[facing])
} else {
VoxelShapes.union(
faceShapes[facing],
centerShapes[facing],
CableBlock.SIDE_SHAPES[cableConnection.direction]
)
}
}
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val direction = state[CABLE_CONNECTION].direction
return if (direction != null) EnumSet.of(direction) else setOf()
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val direction = state[CABLE_CONNECTION].direction
return if (direction != null) EnumSet.of(direction) else setOf()
}
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? {
return if (side == state[FACING]) {
null
} else {
getBlockEntity(world, pos)
}
}
override fun getNetworkInterfaceForSide(
side: Direction,
state: BlockState,
world: WorldAccess,
pos: BlockPos
): Interface? {
return if (side == state[FACING]) {
null
} else {
getBlockEntity(world, pos)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
builder.add(CABLE_CONNECTION)
builder.add(COLOR)
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
builder.add(CABLE_CONNECTION)
builder.add(COLOR)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite
// todo: this should never be called
val cableConnection = FaceCableConnection.from(getCableConnectedSide(context.world, context.blockPos, facing, DyeColor.BLUE))
return defaultState
.with(FACING, facing)
.with(CABLE_CONNECTION, cableConnection)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
val facing =
if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite
// todo: this should never be called
val cableConnection =
FaceCableConnection.from(getCableConnectedSide(context.world, context.blockPos, facing, DyeColor.BLUE))
return defaultState
.with(FACING, facing)
.with(CABLE_CONNECTION, cableConnection)
}
private fun getCableConnectedSide(world: WorldAccess, pos: BlockPos, facing: Direction, color: DyeColor): Direction? {
for (side in Direction.values()) {
if (side == facing) {
continue
}
val offsetPos = pos.offset(side)
val state = world.getBlockState(offsetPos)
if (canConnectTo(world, side, state, offsetPos, color)) {
return side
}
}
return null
}
private fun getCableConnectedSide(
world: WorldAccess,
pos: BlockPos,
facing: Direction,
color: DyeColor
): Direction? {
for (side in Direction.values()) {
if (side == facing) {
continue
}
val offsetPos = pos.offset(side)
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 {
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)
}
}
private fun canConnectTo(
world: WorldAccess,
side: Direction,
candidateState: BlockState,
candidatePos: BlockPos,
myColor: DyeColor
): Boolean {
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 {
val current = state[CABLE_CONNECTION]
var newConnection = current
override fun getStateForNeighborUpdate(
state: BlockState,
side: Direction,
neighborState: BlockState,
world: WorldAccess,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val current = state[CABLE_CONNECTION]
var newConnection = current
if (current == FaceCableConnection.NONE) {
if (canConnectTo(world, side, neighborState, neighborPos, state[COLOR])) {
newConnection = FaceCableConnection.from(side)
}
} else {
val currentConnectedPos = pos.offset(current.direction)
if (neighborPos == currentConnectedPos && neighborState.block !is NetworkComponentBlock) {
// the old cable connection is no longer correct, try to find another
newConnection = FaceCableConnection.from(getCableConnectedSide(world, pos, state[FACING], state[COLOR]))
}
}
return state.with(CABLE_CONNECTION, newConnection)
}
if (current == FaceCableConnection.NONE) {
if (canConnectTo(world, side, neighborState, neighborPos, state[COLOR])) {
newConnection = FaceCableConnection.from(side)
}
} else {
val currentConnectedPos = pos.offset(current.direction)
if (neighborPos == currentConnectedPos && neighborState.block !is NetworkComponentBlock) {
// the old cable connection is no longer correct, try to find another
newConnection = FaceCableConnection.from(getCableConnectedSide(world, pos, state[FACING], state[COLOR]))
}
}
return state.with(CABLE_CONNECTION, newConnection)
}
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape {
return getShape(state[FACING], state[CABLE_CONNECTION])
}
override fun getOutlineShape(
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) {
super.onStacksDropped(state, world, pos, stack)
override fun onStacksDropped(state: BlockState, world: ServerWorld, pos: BlockPos, stack: ItemStack) {
super.onStacksDropped(state, world, pos, stack)
val cableStack = ItemStack(PhyItems.CABLES[state[COLOR]])
dropStack(world, pos, cableStack)
}
val cableStack = ItemStack(PhyItems.CABLES[state[COLOR]])
dropStack(world, pos, cableStack)
}
}

View File

@ -37,180 +37,209 @@ import java.util.*
* @author shadowfacts
*/
class CableBlock(
val color: DyeColor,
): Block(
FabricBlockSettings.of(CABLE_MATERIAL)
.strength(0.3f)
.nonOpaque()
val color: DyeColor,
) : Block(
FabricBlockSettings.of(CABLE_MATERIAL)
.strength(0.3f)
.nonOpaque()
), NetworkCableBlock {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "cable")
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 SIDE_SHAPES = mapOf<Direction, VoxelShape>(
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.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.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)
)
private val SHAPE_CACHE = Array<VoxelShape>(64) { key ->
val connectedSides = Direction.values().filterIndexed { index, _ ->
((key shr index) and 1) == 1
}
connectedSides.fold(CENTER_SHAPE) { acc, 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) }
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "cable")
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 SIDE_SHAPES = mapOf<Direction, VoxelShape>(
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.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.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)
)
private val SHAPE_CACHE = Array<VoxelShape>(64) { key ->
val connectedSides = Direction.values().filterIndexed { index, _ ->
((key shr index) and 1) == 1
}
connectedSides.fold(CENTER_SHAPE) { acc, 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) }
fun getShape(state: BlockState): VoxelShape {
val key = Direction.values().foldIndexed(0) { i, acc, dir ->
if (state[CONNECTIONS[dir]] == CableConnection.ON) {
acc or (1 shl i)
} else {
acc
}
}
return SHAPE_CACHE[key]
}
}
fun getShape(state: BlockState): VoxelShape {
val key = Direction.values().foldIndexed(0) { i, acc, dir ->
if (state[CONNECTIONS[dir]] == CableConnection.ON) {
acc or (1 shl i)
} else {
acc
}
}
return SHAPE_CACHE[key]
}
}
init {
defaultState = CONNECTIONS.values.fold(stateManager.defaultState) { acc, prop ->
acc.with(prop, CableConnection.OFF)
}
}
init {
defaultState = CONNECTIONS.values.fold(stateManager.defaultState) { acc, prop ->
acc.with(prop, CableConnection.OFF)
}
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.noneOf(Direction::class.java)
for ((side, prop) in CONNECTIONS) {
if (state[prop] == CableConnection.ON) {
set.add(side)
}
}
return set
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
val set = EnumSet.noneOf(Direction::class.java)
for ((side, prop) in CONNECTIONS) {
if (state[prop] == CableConnection.ON) {
set.add(side)
}
}
return set
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
CONNECTIONS.values.forEach {
builder.add(it)
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
CONNECTIONS.values.forEach {
builder.add(it)
}
}
fun getInitialState(world: World, pos: BlockPos): BlockState {
return CONNECTIONS.entries.fold(defaultState, { acc, (dir, prop) ->
acc.with(prop, getConnectionStateInDirection(world, pos, dir))
})
}
fun getInitialState(world: World, pos: BlockPos): BlockState {
return CONNECTIONS.entries.fold(defaultState, { acc, (dir, prop) ->
acc.with(prop, getConnectionStateInDirection(world, pos, dir))
})
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return getInitialState(context.world, context.blockPos)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return getInitialState(context.world, context.blockPos)
}
override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, 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))
}
}
override fun getStateForNeighborUpdate(
state: BlockState,
side: Direction,
neighborState: BlockState,
world: WorldAccess,
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 {
val offsetPos = pos.offset(direction)
val state = world.getBlockState(offsetPos)
val block = state.block
return if (block == this) {
val prop = CONNECTIONS[direction.opposite]
when (state[prop]) {
CableConnection.DISABLED -> CableConnection.DISABLED
else -> CableConnection.ON
}
} else if (block is NetworkComponentBlock && block !is CableBlock) {
if (block.getNetworkConnectedSides(state, world, offsetPos).contains(direction.opposite)) {
CableConnection.ON
} else {
CableConnection.OFF
}
} else {
CableConnection.OFF
}
}
private fun getConnectionStateInDirection(
world: WorldAccess,
pos: BlockPos,
direction: Direction
): CableConnection {
val offsetPos = pos.offset(direction)
val state = world.getBlockState(offsetPos)
val block = state.block
return if (block == this) {
val prop = CONNECTIONS[direction.opposite]
when (state[prop]) {
CableConnection.DISABLED -> CableConnection.DISABLED
else -> CableConnection.ON
}
} else if (block is NetworkComponentBlock && block !is CableBlock) {
if (block.getNetworkConnectedSides(state, world, offsetPos).contains(direction.opposite)) {
CableConnection.ON
} else {
CableConnection.OFF
}
} else {
CableConnection.OFF
}
}
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? {
// cables don't have network interfaces
return null
}
override fun getNetworkInterfaceForSide(
side: Direction,
state: BlockState,
world: WorldAccess,
pos: BlockPos
): Interface? {
// cables don't have network interfaces
return null
}
override fun onUse(
state: BlockState,
world: World,
pos: BlockPos,
player: PlayerEntity,
hand: Hand,
hitResult: BlockHitResult
): ActionResult {
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 hitConnection = SIDE_SHAPES.entries.firstOrNull { (_, shape) ->
shape.boundingBox.containsInclusive(hitPos)
}
if (hitConnection != null) {
val side = hitConnection.key
val prop = CONNECTIONS[side]
val newState = when (state[prop]) {
CableConnection.DISABLED -> {
// 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
// "halves" of the connection are in the same state
val connectedToPos = pos.offset(side)
val connectedTo = world.getBlockState(connectedToPos)
if (connectedTo.block == this && connectedTo[CONNECTIONS[side.opposite]] == CableConnection.DISABLED) {
world.setBlockState(connectedToPos, connectedTo.with(CONNECTIONS[side.opposite], CableConnection.ON))
}
override fun onUse(
state: BlockState,
world: World,
pos: BlockPos,
player: PlayerEntity,
hand: Hand,
hitResult: BlockHitResult
): ActionResult {
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 hitConnection = SIDE_SHAPES.entries.firstOrNull { (_, shape) ->
shape.boundingBox.containsInclusive(hitPos)
}
if (hitConnection != null) {
val side = hitConnection.key
val prop = CONNECTIONS[side]
val newState = when (state[prop]) {
CableConnection.DISABLED -> {
// 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
// "halves" of the connection are in the same state
val connectedToPos = pos.offset(side)
val connectedTo = world.getBlockState(connectedToPos)
if (connectedTo.block == this && connectedTo[CONNECTIONS[side.opposite]] == CableConnection.DISABLED) {
world.setBlockState(
connectedToPos,
connectedTo.with(CONNECTIONS[side.opposite], CableConnection.ON)
)
}
state.with(prop, if (connectedTo.block is NetworkComponentBlock) CableConnection.ON else CableConnection.OFF)
}
else -> state.with(prop, CableConnection.DISABLED)
}
world.setBlockState(pos, newState)
}
return ActionResult.SUCCESS
}
return ActionResult.PASS
}
state.with(
prop,
if (connectedTo.block is NetworkComponentBlock) CableConnection.ON else CableConnection.OFF
)
}
override fun canReplace(state: BlockState, context: ItemPlacementContext): Boolean {
return context.stack.item is FaceDeviceBlockItem
}
else -> state.with(prop, CableConnection.DISABLED)
}
world.setBlockState(pos, newState)
}
return ActionResult.SUCCESS
}
return ActionResult.PASS
}
override fun isTranslucent(blockState_1: BlockState?, blockView_1: BlockView?, blockPos_1: BlockPos?): Boolean {
return true
}
override fun canReplace(state: BlockState, context: ItemPlacementContext): Boolean {
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 {
// return false
// }
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape {
return getShape(state)
}
override fun getOutlineShape(
state: BlockState,
world: BlockView,
pos: BlockPos,
context: ShapeContext
): VoxelShape {
return getShape(state)
}
override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) {
super.onBreak(world, pos, state, player)
if (!world.isClient) {
world.server?.execute {
// notify devices on either end that the connection was broken (i.e., unset the cached receivers)
val connectedSides = getNetworkConnectedSides(state, world, pos)
for (side in connectedSides) {
val dest = NetworkUtil.findConnectedInterface(world, pos, side)
dest?.cableDisconnected()
}
}
}
}
override fun onBreak(world: World, pos: BlockPos, state: BlockState, player: PlayerEntity) {
super.onBreak(world, pos, state, player)
if (!world.isClient) {
world.server?.execute {
// notify devices on either end that the connection was broken (i.e., unset the cached receivers)
val connectedSides = getNetworkConnectedSides(state, world, pos)
for (side in connectedSides) {
val dest = NetworkUtil.findConnectedInterface(world, pos, side)
dest?.cableDisconnected()
}
}
}
}
}

View File

@ -21,66 +21,61 @@ import kotlin.math.min
/**
* @author shadowfacts
*/
class ExtractorBlock: FaceDeviceBlock<ExtractorBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class ExtractorBlock : FaceDeviceBlock<ExtractorBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "extractor")
private val EXTRACTOR_SHAPES = mutableMapOf<Direction, VoxelShape>()
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "extractor")
private val EXTRACTOR_SHAPES = mutableMapOf<Direction, VoxelShape>()
init {
val components = arrayOf(
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(4.0, 4.0, 4.0, 12.0, 6.0, 12.0),
)
val directions = arrayOf(
Triple(Direction.DOWN, null, false),
Triple(Direction.UP, null, true),
Triple(Direction.NORTH, 2, false),
Triple(Direction.SOUTH, 2, true),
Triple(Direction.WEST, 1, false),
Triple(Direction.EAST, 1, true),
)
for ((dir, rotate, flip) in directions) {
val shapes = components.map { it ->
val arr = it.copyOf()
if (rotate != null) {
for (i in 0 until 3) {
arr[i] = it[(i + rotate) % 3]
arr[3 + i] = it[3 + ((i + rotate) % 3)]
}
}
if (flip) {
for (i in arr.indices) {
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]))
}
EXTRACTOR_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) }
}
}
}
init {
val components = arrayOf(
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(4.0, 4.0, 4.0, 12.0, 6.0, 12.0),
)
val directions = arrayOf(
Triple(Direction.DOWN, null, false),
Triple(Direction.UP, null, true),
Triple(Direction.NORTH, 2, false),
Triple(Direction.SOUTH, 2, true),
Triple(Direction.WEST, 1, false),
Triple(Direction.EAST, 1, true),
)
for ((dir, rotate, flip) in directions) {
val shapes = components.map { it ->
val arr = it.copyOf()
if (rotate != null) {
for (i in 0 until 3) {
arr[i] = it[(i + rotate) % 3]
arr[3 + i] = it[3 + ((i + rotate) % 3)]
}
}
if (flip) {
for (i in arr.indices) {
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])
)
}
EXTRACTOR_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) }
}
}
}
override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = EXTRACTOR_SHAPES
override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = EXTRACTOR_SHAPES
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()
}
}
override fun createBlockEntity(pos: BlockPos, state: BlockState) = ExtractorBlockEntity(pos, state)
}

View File

@ -1,13 +1,14 @@
package net.shadowfacts.phycon.block.extractor
import alexiil.mc.lib.attributes.SearchOptions
import alexiil.mc.lib.attributes.Simulation
import alexiil.mc.lib.attributes.item.FixedItemInv
import alexiil.mc.lib.attributes.item.ItemAttributes
import alexiil.mc.lib.attributes.item.filter.ExactItemStackFilter
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
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.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
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.util.ActivationMode
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.copyWithCount
import kotlin.math.min
import kotlin.properties.Delegates
/**
* @author shadowfacts
*/
class ExtractorBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.EXTRACTOR, pos, state),
NetworkStackDispatcher<ExtractorBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
class ExtractorBlockEntity(pos: BlockPos, state: BlockState) :
DeviceBlockEntity(PhyBlockEntities.EXTRACTOR, pos, state),
NetworkStackDispatcher<ExtractorBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
companion object {
val SLEEP_TIME = 40L
}
companion object {
val SLEEP_TIME = 40L
}
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
private var inventory: FixedItemInv? = null
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = 1L
override val controller = ActivationController(SLEEP_TIME, this)
private var inventory: Pair<BlockState, BlockApiCache<Storage<ItemVariant>, Direction>>? = null
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = 1L
override val controller = ActivationController(SLEEP_TIME, this)
fun updateInventory() {
val offsetPos = pos.offset(facing)
val option = SearchOptions.inDirection(facing)
inventory = ItemAttributes.FIXED_INV.getFirstOrNull(world, offsetPos, option)
}
private fun getInventory(): Storage<ItemVariant>? {
if (inventory == null) {
val offsetPos = pos.offset(facing)
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? {
if (inventory == null) updateInventory()
return inventory
}
override fun handle(packet: Packet) {
when (packet) {
is CapacityPacket -> handleCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
override fun handle(packet: Packet) {
when (packet) {
is CapacityPacket -> handleCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
// we can't insert things back into the inventory, so just let them spawn
return packet.stack
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
// 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 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 {
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 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() {
super.tick()
override fun tick() {
super.tick()
if (!world!!.isClient) {
controller.tick()
if (!world!!.isClient) {
controller.tick()
finishTimedOutPendingInsertions()
}
}
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 {
val inventory = getInventory() ?: return false
for (slot in 0 until inventory.slotCount) {
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) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
}
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
lateinit var inventory: FixedItemInv
var inventorySlot by Delegates.notNull<Int>()
}
class PendingInsertion(stack: ItemStack, timestamp: Long) :
NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
lateinit var inventory: Storage<ItemVariant>
var inventorySlot by Delegates.notNull<Int>()
}
}

View File

@ -31,87 +31,89 @@ import kotlin.math.min
/**
* @author shadowfacts
*/
class InserterBlock: FaceDeviceBlock<InserterBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class InserterBlock : FaceDeviceBlock<InserterBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "inserter")
private val INSERTER_SHAPES = mutableMapOf<Direction, VoxelShape>()
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "inserter")
private val INSERTER_SHAPES = mutableMapOf<Direction, VoxelShape>()
init {
val components = arrayOf(
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(0.0, 4.0, 0.0, 16.0, 6.0, 16.0),
)
val directions = arrayOf(
Triple(Direction.DOWN, null, false),
Triple(Direction.UP, null, true),
Triple(Direction.NORTH, 2, false),
Triple(Direction.SOUTH, 2, true),
Triple(Direction.WEST, 1, false),
Triple(Direction.EAST, 1, true),
)
for ((dir, rotate, flip) in directions) {
val shapes = components.map { it ->
val arr = it.copyOf()
if (rotate != null) {
for (i in 0 until 3) {
arr[i] = it[(i + rotate) % 3]
arr[3 + i] = it[3 + ((i + rotate) % 3)]
}
}
if (flip) {
for (i in arr.indices) {
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]))
}
INSERTER_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) }
}
}
}
init {
val components = arrayOf(
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(0.0, 4.0, 0.0, 16.0, 6.0, 16.0),
)
val directions = arrayOf(
Triple(Direction.DOWN, null, false),
Triple(Direction.UP, null, true),
Triple(Direction.NORTH, 2, false),
Triple(Direction.SOUTH, 2, true),
Triple(Direction.WEST, 1, false),
Triple(Direction.EAST, 1, true),
)
for ((dir, rotate, flip) in directions) {
val shapes = components.map { it ->
val arr = it.copyOf()
if (rotate != null) {
for (i in 0 until 3) {
arr[i] = it[(i + rotate) % 3]
arr[3 + i] = it[3 + ((i + rotate) % 3)]
}
}
if (flip) {
for (i in arr.indices) {
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])
)
}
INSERTER_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) }
}
}
}
override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = INSERTER_SHAPES
override val faceThickness = 6.0
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) {
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
override fun onUse(
state: BlockState,
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) {
if (!world.isClient) {
getBlockEntity(world, pos)!!.updateInventory()
}
}
be.markUpdate()
override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult {
if (!world.isClient) {
val be = getBlockEntity(world, pos)!!
val factory = object : ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler {
return InserterScreenHandler(syncId, playerInv, be)
}
be.markUpdate()
override fun getDisplayName() = this@InserterBlock.name
val factory = object: ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler {
return InserterScreenHandler(syncId, playerInv, be)
}
override fun getDisplayName() = this@InserterBlock.name
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(be.pos)
}
}
player.openHandledScreen(factory)
}
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
import alexiil.mc.lib.attributes.SearchOptions
import alexiil.mc.lib.attributes.Simulation
import alexiil.mc.lib.attributes.item.ItemAttributes
import alexiil.mc.lib.attributes.item.ItemInsertable
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
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.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
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.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.GhostInv
import net.shadowfacts.phycon.util.*
import kotlin.math.min
/**
* @author shadowfacts
*/
class InserterBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.INSERTER, pos, state),
ItemStackPacketHandler,
ActivationController.ActivatableDevice,
ClientConfigurableDevice,
GhostInv {
class InserterBlockEntity(pos: BlockPos, state: BlockState) : DeviceBlockEntity(PhyBlockEntities.INSERTER, pos, state),
ItemStackPacketHandler,
ActivationController.ActivatableDevice,
ClientConfigurableDevice,
GhostInv {
companion object {
val SLEEP_TIME = 40L
val REQUEST_TIMEOUT = 40
}
companion object {
val SLEEP_TIME = 40L
val REQUEST_TIMEOUT = 40
}
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
private var inventory: ItemInsertable? = null
private var currentRequest: PendingExtractRequest? = null
var stackToExtract: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack
get() = stackToExtract
set(value) { stackToExtract = value }
var amountToExtract = 1
override val controller = ActivationController(SLEEP_TIME, this)
private var inventory: Pair<BlockState, BlockApiCache<Storage<ItemVariant>, Direction>>? = null
private var currentRequest: PendingExtractRequest? = null
var stackToExtract: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack
get() = stackToExtract
set(value) {
stackToExtract = value
}
var amountToExtract = 1
override val controller = ActivationController(SLEEP_TIME, this)
fun updateInventory() {
val offsetPos = pos.offset(facing)
val option = SearchOptions.inDirection(facing)
inventory = ItemAttributes.INSERTABLE.getFirstOrNull(world, offsetPos, option)
}
private fun getInventory(): Storage<ItemVariant>? {
if (inventory == null) {
val offsetPos = pos.offset(facing)
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? {
if (inventory == null) updateInventory()
return inventory
}
override fun handle(packet: Packet) {
when (packet) {
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
override fun handle(packet: Packet) {
when (packet) {
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val inventory = getInventory()
if (inventory != null) {
val transaction = Transaction.openOuter()
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 {
val inventory = getInventory()
return if (inventory != null) {
inventory.attemptInsertion(packet.stack, Simulation.ACTION)
} else {
// no inventory, entire stack remains
packet.stack
}
}
private fun handleStackLocation(packet: StackLocationPacket) {
val request = currentRequest
if (request != null && request.stack.equalsIgnoringAmount(packet.stack)) {
request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) {
finishRequest()
}
}
}
private fun handleStackLocation(packet: StackLocationPacket) {
val request = currentRequest
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() {
super.tick()
override fun tick() {
super.tick()
if (!world!!.isClient) {
controller.tick()
if (!world!!.isClient) {
controller.tick()
val request = currentRequest
if (request != null) {
if (request.isFinishable(counter)) {
finishRequest()
} else if (counter - request.timestamp >= REQUEST_TIMEOUT && request.totalAmount == 0) {
currentRequest = null
}
}
}
}
val request = currentRequest
if (request != null) {
if (request.isFinishable(counter)) {
finishRequest()
} else if (counter - request.timestamp >= REQUEST_TIMEOUT && request.totalAmount == 0) {
currentRequest = null
}
}
}
}
override fun activate(): Boolean {
if (currentRequest != null || stackToExtract.isEmpty) {
return false
}
override fun activate(): Boolean {
if (currentRequest != null || stackToExtract.isEmpty) {
return false
}
// todo: configure me
currentRequest = PendingExtractRequest(stackToExtract, counter)
sendPacket(LocateStackPacket(stackToExtract, ipAddress))
return true
}
// todo: configure me
currentRequest = PendingExtractRequest(stackToExtract, counter)
sendPacket(LocateStackPacket(stackToExtract, ipAddress))
return true
}
private fun finishRequest() {
val request = currentRequest ?: return
private fun finishRequest() {
val request = currentRequest ?: return
// todo: dedup with TerminalBlockEntity.stackLocateRequestCompleted
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
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
}
currentRequest = null
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
tag.put("StackToExtract", stackToExtract.writeNbt(NbtCompound()))
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
tag.put("StackToExtract", stackToExtract.writeNbt(NbtCompound()))
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
stackToExtract = ItemStack.fromNbt(tag.getCompound("StackToExtract"))
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
stackToExtract = ItemStack.fromNbt(tag.getCompound("StackToExtract"))
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("AmountToExtract", amountToExtract)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("AmountToExtract", amountToExtract)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
amountToExtract = tag.getInt("AmountToExtract")
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
amountToExtract = tag.getInt("AmountToExtract")
}
class PendingExtractRequest(
val stack: ItemStack,
val timestamp: Long,
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
) {
val totalAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
class PendingExtractRequest(
val stack: ItemStack,
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)
}
}
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
*/
class InserterScreen(
handler: InserterScreenHandler,
playerInv: PlayerInventory,
title: Text,
): HandledScreen<InserterScreenHandler>(
handler,
playerInv,
title
handler: InserterScreenHandler,
playerInv: PlayerInventory,
title: Text,
) : HandledScreen<InserterScreenHandler>(
handler,
playerInv,
title
) {
companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/inserter.png")
}
companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/inserter.png")
}
private lateinit var amountField: TextFieldWidget
private lateinit var amountField: TextFieldWidget
init {
backgroundWidth = 176
backgroundHeight = 133
playerInventoryTitleY = backgroundHeight - 94
}
init {
backgroundWidth = 176
backgroundHeight = 133
playerInventoryTitleY = backgroundHeight - 94
}
override fun init() {
super.init()
override fun init() {
super.init()
amountField = TextFieldWidget(textRenderer, x + 57, y + 24, 80, 9, LiteralText("Amount"))
amountField.text = handler.inserter.amountToExtract.toString()
amountField.setDrawsBackground(false)
amountField.isVisible = true
amountField.setTextFieldFocused(true)
amountField.setEditableColor(0xffffff)
amountField.setTextPredicate {
if (it.isEmpty()) {
true
} else {
try {
val value = Integer.parseInt(it)
value in 1..64
} catch (e: NumberFormatException) {
false
}
}
}
addDrawableChild(amountField)
}
amountField = TextFieldWidget(textRenderer, x + 57, y + 24, 80, 9, LiteralText("Amount"))
amountField.text = handler.inserter.amountToExtract.toString()
amountField.setDrawsBackground(false)
amountField.isVisible = true
amountField.setTextFieldFocused(true)
amountField.setEditableColor(0xffffff)
amountField.setTextPredicate {
if (it.isEmpty()) {
true
} else {
try {
val value = Integer.parseInt(it)
value in 1..64
} catch (e: NumberFormatException) {
false
}
}
}
addDrawableChild(amountField)
}
fun amountUpdated() {
if (amountField.text.isNotEmpty()) {
handler.inserter.amountToExtract = Integer.parseInt(amountField.text)
client!!.player!!.networkHandler.sendPacket(C2SConfigureDevice(handler.inserter))
}
}
fun amountUpdated() {
if (amountField.text.isNotEmpty()) {
handler.inserter.amountToExtract = Integer.parseInt(amountField.text)
client!!.player!!.networkHandler.sendPacket(C2SConfigureDevice(handler.inserter))
}
}
override fun handledScreenTick() {
super.handledScreenTick()
amountField.tick()
}
override fun handledScreenTick() {
super.handledScreenTick()
amountField.tick()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
renderBackground(matrixStack)
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
renderBackground(matrixStack)
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
super.render(matrixStack, mouseX, mouseY, delta)
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
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?) {
super.onMouseClick(slot, invSlot, clickData, slotActionType)
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, slotActionType: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, slotActionType)
amountField.setTextFieldFocused(true)
}
amountField.setTextFieldFocused(true)
}
override fun charTyped(c: Char, i: Int): Boolean {
val oldText = amountField.text
if (amountField.charTyped(c, i)) {
if (oldText != amountField.text) {
amountUpdated()
}
return true
}
return super.charTyped(c, i)
}
override fun charTyped(c: Char, i: Int): Boolean {
val oldText = amountField.text
if (amountField.charTyped(c, i)) {
if (oldText != amountField.text) {
amountUpdated()
}
return true
}
return super.charTyped(c, i)
}
override fun keyPressed(i: Int, j: Int, k: Int): Boolean {
val oldText = amountField.text
if (amountField.keyPressed(i, j, k)) {
if (oldText != amountField.text) {
amountUpdated()
}
return true
}
return super.keyPressed(i, j, k)
}
override fun keyPressed(i: Int, j: Int, k: Int): Boolean {
val oldText = amountField.text
if (amountField.keyPressed(i, j, k)) {
if (oldText != amountField.text) {
amountUpdated()
}
return true
}
return super.keyPressed(i, j, k)
}
}

View File

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

View File

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

View File

@ -1,8 +1,10 @@
package net.shadowfacts.phycon.block.miner
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackUtil
import alexiil.mc.lib.attributes.item.filter.ItemFilter
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView
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.BlockState
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.ClientConfigurableDevice
import net.shadowfacts.phycon.util.copyWithCount
import net.shadowfacts.phycon.util.equalsIgnoringAmount
import kotlin.math.min
/**
* @author shadowfacts
*/
class MinerBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.MINER, pos, state),
NetworkStackProvider,
NetworkStackDispatcher<MinerBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
class MinerBlockEntity(pos: BlockPos, state: BlockState) : DeviceBlockEntity(PhyBlockEntities.MINER, pos, state),
NetworkStackProvider,
NetworkStackDispatcher<MinerBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
private val facing: Direction
get() = cachedState[MinerBlock.FACING]
private val facing: Direction
get() = cachedState[MinerBlock.FACING]
private val invProxy = MinerInvProxy(this)
private val invProxy = MinerStorageProxy(this)
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT
override val controller = ActivationController(40L, this)
override var providerPriority = 0
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT
override val controller = ActivationController(40L, this)
override var providerPriority = 0
var minerMode = MinerMode.ON_DEMAND
var minerMode = MinerMode.ON_DEMAND
override fun handle(packet: Packet) {
when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet)
is LocateStackPacket -> handleLocateStack(packet)
is ExtractStackPacket -> handleExtractStack(packet)
is CapacityPacket -> handleCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
override fun handle(packet: Packet) {
when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet)
is LocateStackPacket -> handleLocateStack(packet)
is ExtractStackPacket -> handleExtractStack(packet)
is CapacityPacket -> handleCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (minerMode != MinerMode.ON_DEMAND || packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
sendPacket(ReadGroupedInventoryPacket(invProxy, ipAddress, packet.source))
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (minerMode != MinerMode.ON_DEMAND || packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
sendPacket(ReadItemStoragePacket(invProxy, ipAddress, packet.source))
}
private fun handleLocateStack(packet: LocateStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
return
}
val amount = invProxy.getAmount(packet.stack)
if (amount > 0) {
sendPacket(StackLocationPacket(packet.stack, amount, this, ipAddress, packet.source))
}
}
private fun handleLocateStack(packet: LocateStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
return
}
val amount = invProxy.simulateExtract(ItemVariant.of(packet.stack), Long.MAX_VALUE, null)
if (amount > 0) {
sendPacket(StackLocationPacket(packet.stack, amount.toInt(), this, ipAddress, packet.source))
}
}
private fun handleExtractStack(packet: ExtractStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
return
}
private fun handleExtractStack(packet: ExtractStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
return
}
// always recalculate immediately before breaking
val drops = invProxy.getDrops(recalculate = true)
if (invProxy.getAmount(packet.stack) > 0) {
world!!.breakBlock(pos.offset(facing), false)
// always recalculate immediately before breaking
val drops = invProxy.getDrops(recalculate = true)
val amount = invProxy.simulateExtract(ItemVariant.of(packet.stack), packet.amount.toLong(), null)
if (amount > 0) {
world!!.breakBlock(pos.offset(facing), false)
// send the requested amount back to the requester
var remaining = packet.amount
for (droppedStack in drops) {
if (remaining <= 0) {
break
}
if (!ItemStackUtil.areEqualIgnoreAmounts(droppedStack, packet.stack)) {
continue
}
// send the requested amount back to the requester
var remaining = packet.amount
for (droppedStack in drops) {
if (remaining <= 0) {
break
}
if (!droppedStack.equalsIgnoringAmount(packet.stack)) {
continue
}
val toDecr = min(droppedStack.count, remaining)
val copy = droppedStack.copyWithCount(toDecr)
droppedStack.decrement(toDecr)
remaining -= toDecr
val toDecr = min(droppedStack.count, remaining)
val copy = droppedStack.copyWithCount(toDecr)
droppedStack.decrement(toDecr)
remaining -= toDecr
// todo: should this try to combine stacks and send as few packets as possible?
sendPacket(ItemStackPacket(copy, ipAddress, packet.source))
}
// todo: should this try to combine stacks and send as few packets as possible?
sendPacket(ItemStackPacket(copy, ipAddress, packet.source))
}
// dump any remaining drops into the network
for (droppedStack in drops) {
if (droppedStack.isEmpty) continue
dispatchItemStack(droppedStack)
}
}
}
// dump any remaining drops into the network
for (droppedStack in drops) {
if (droppedStack.isEmpty) continue
dispatchItemStack(droppedStack)
}
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
// miner can't receive stacks, so remaining is the entire packet stack
return packet.stack
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
// miner can't receive stacks, so remaining is the entire packet stack
return packet.stack
}
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun tick() {
super.tick()
override fun tick() {
super.tick()
if (!world!!.isClient) {
if (minerMode == MinerMode.AUTOMATIC) {
controller.tick()
}
finishTimedOutPendingInsertions()
}
}
if (!world!!.isClient) {
if (minerMode == MinerMode.AUTOMATIC) {
controller.tick()
}
finishTimedOutPendingInsertions()
}
}
override fun activate(): Boolean {
if (minerMode == MinerMode.ON_DEMAND) {
return false
}
override fun activate(): Boolean {
if (minerMode == MinerMode.ON_DEMAND) {
return false
}
val drops = invProxy.getDrops(recalculate = true)
if (!world!!.getBlockState(pos.offset(facing)).isAir) {
world!!.breakBlock(pos.offset(facing), false)
val drops = invProxy.getDrops(recalculate = true)
if (!world!!.getBlockState(pos.offset(facing)).isAir) {
world!!.breakBlock(pos.offset(facing), false)
for (stack in drops) {
if (stack.isEmpty) continue
dispatchItemStack(stack)
}
for (stack in drops) {
if (stack.isEmpty) continue
dispatchItemStack(stack)
}
return true
} else {
return false
}
}
return true
} else {
return false
}
}
override fun canConfigureActivationController(): Boolean {
return minerMode == MinerMode.AUTOMATIC
}
override fun canConfigureActivationController(): Boolean {
return minerMode == MinerMode.AUTOMATIC
}
override fun canConfigureProviderPriority(): Boolean {
return minerMode == MinerMode.ON_DEMAND
}
override fun canConfigureProviderPriority(): Boolean {
return minerMode == MinerMode.ON_DEMAND
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("MinerMode", minerMode.name)
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("ProviderPriority", providerPriority)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("MinerMode", minerMode.name)
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("ProviderPriority", providerPriority)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
minerMode = MinerMode.valueOf(tag.getString("MinerMode"))
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
providerPriority = tag.getInt("ProviderPriority")
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
minerMode = MinerMode.valueOf(tag.getString("MinerMode"))
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
providerPriority = tag.getInt("ProviderPriority")
}
enum class MinerMode {
ON_DEMAND, AUTOMATIC;
enum class MinerMode {
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 {
companion object {
val TOOL = ItemStack(Items.DIAMOND_PICKAXE)
}
class MinerStorageProxy(val miner: MinerBlockEntity) :
SnapshotParticipant<Pair<BlockState, List<ItemStack>>?>(),
ExtractionOnlyStorage<ItemVariant>
{
companion object {
val TOOL = ItemStack(Items.DIAMOND_PICKAXE)
}
private var cachedState: BlockState? = null
private var cachedDrops: List<ItemStack>? = null
private var cache: Pair<BlockState, List<ItemStack>>? = null
private val world: World
get() = miner.world!!
private val pos: BlockPos
get() = miner.pos!!
private val facing: Direction
get() = miner.facing
private val world: World
get() = miner.world!!
private val pos: BlockPos
get() = miner.pos!!
private val facing: Direction
get() = miner.facing
fun getDrops(recalculate: Boolean = false): List<ItemStack> {
val targetPos = pos.offset(facing)
val realState = world.getBlockState(targetPos)
// todo: does BlockState.equals actually work or is reference equality fine for BlockStates?
if (cachedDrops == null || realState != cachedState || recalculate) {
cachedState = realState
fun getDrops(recalculate: Boolean = false): List<ItemStack> {
val targetPos = pos.offset(facing)
val realState = world.getBlockState(targetPos)
val cache = this.cache
// todo: does BlockState.equals actually work or is reference equality fine for BlockStates?
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
cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be, null, TOOL)
}
return cachedDrops!!
}
override fun createSnapshot(): Pair<BlockState, List<ItemStack>>? {
return cache
}
override fun getStoredStacks(): Set<ItemStack> {
if (miner.minerMode != MinerMode.ON_DEMAND) {
return setOf()
}
return getDrops().toSet()
}
override fun readSnapshot(snapshot: Pair<BlockState, List<ItemStack>>?) {
cache = snapshot
}
override fun getTotalCapacity(): Int {
return Int.MAX_VALUE
}
override fun extract(resource: ItemVariant, maxAmount: Long, transaction: TransactionContext): Long {
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 {
var totalCount = 0
for (s in storedStacks) {
if (filter.matches(s)) {
totalCount += s.count
}
}
return GroupedItemInvView.ItemInvStatistic(filter, totalCount, 0, Int.MAX_VALUE)
}
}
override fun iterator(transaction: TransactionContext): Iterator<StorageView<ItemVariant>> {
return getDrops().map { View(it, this) }.iterator()
}
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
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.*
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
@ -18,44 +16,27 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class InterfaceBlock : FaceDeviceBlock<InterfaceBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
NetworkComponentBlock {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "network_interface")
}
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "network_interface")
}
override val faceThickness = 2.0
override val faceShapes = mapOf(
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.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.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)
)
override val faceThickness = 2.0
override val faceShapes = mapOf(
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.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.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)
)
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))
}
override fun createBlockEntity(pos: BlockPos, state: BlockState) = InterfaceBlockEntity(pos, state)
}

View File

@ -1,12 +1,14 @@
package net.shadowfacts.phycon.block.netinterface
import alexiil.mc.lib.attributes.SearchOptions
import alexiil.mc.lib.attributes.Simulation
import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.ItemAttributes
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
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.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
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.packet.*
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import java.lang.ref.WeakReference
import net.shadowfacts.phycon.util.copyWithCount
import kotlin.math.min
/**
* @author shadowfacts
*/
class InterfaceBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.INTERFACE, pos, state),
ItemStackPacketHandler,
NetworkStackProvider,
NetworkStackReceiver,
ClientConfigurableDevice {
class InterfaceBlockEntity(pos: BlockPos, state: BlockState) :
DeviceBlockEntity(PhyBlockEntities.INTERFACE, pos, state),
ItemStackPacketHandler,
NetworkStackProvider,
NetworkStackReceiver,
ClientConfigurableDevice {
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
override var providerPriority = 0
override var receiverPriority = 0
var syncPriorities = true
override var providerPriority = 0
override var receiverPriority = 0
var syncPriorities = true
private var inventory: WeakReference<GroupedItemInv>? = null
private var cachedFacedBlock: BlockState? = null
private var inventoryCache: BlockApiCache<Storage<ItemVariant>, Direction>? = null
fun updateInventory() {
val offsetPos = pos.offset(facing)
val option = SearchOptions.inDirection(facing)
inventory = ItemAttributes.GROUPED_INV.getFirstOrNull(world, offsetPos, option).let {
WeakReference(it)
}
}
private fun getInventory(): Storage<ItemVariant>? {
if (cachedFacedBlock == null) {
cachedFacedBlock = world!!.getBlockState(pos.offset(facing))
}
if (inventoryCache == null) {
inventoryCache = BlockApiCache.create(ItemStorage.SIDED, world!! as ServerWorld, pos.offset(facing))
}
return inventoryCache!!.find(cachedFacedBlock!!, facing.opposite)
}
private fun getInventory(): GroupedItemInv? {
// if we don't have an inventory, try to get one
// this happens when readAll is called before a neighbor state changes, such as immediately after world load
if (inventory?.get() == null) updateInventory()
return inventory?.get()
}
override fun handle(packet: Packet) {
when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet)
is LocateStackPacket -> handleLocateStack(packet)
is ExtractStackPacket -> handleExtractStack(packet)
is CheckCapacityPacket -> handleCheckCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
override fun handle(packet: Packet) {
when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet)
is LocateStackPacket -> handleLocateStack(packet)
is ExtractStackPacket -> handleExtractStack(packet)
is CheckCapacityPacket -> handleCheckCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
}
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
getInventory()?.also { inv ->
sendPacket(ReadItemStoragePacket(inv, ipAddress, packet.source))
}
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
getInventory()?.also { inv ->
sendPacket(ReadGroupedInventoryPacket(inv, ipAddress, packet.source))
}
}
private fun handleLocateStack(packet: LocateStackPacket) {
getInventory()?.also { inv ->
val transaction = Transaction.openOuter()
val amount = inv.simulateExtract(ItemVariant.of(packet.stack), Long.MAX_VALUE, transaction);
transaction.close()
sendPacket(StackLocationPacket(packet.stack, amount.toInt(), this, ipAddress, packet.source))
}
}
private fun handleLocateStack(packet: LocateStackPacket) {
getInventory()?.also { inv ->
val amount = inv.getAmount(packet.stack)
sendPacket(StackLocationPacket(packet.stack, amount, this, ipAddress, packet.source))
}
}
private fun handleExtractStack(packet: ExtractStackPacket) {
getInventory()?.also { inv ->
var remaining = packet.amount
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) {
getInventory()?.also { inv ->
var amount = packet.amount
while (amount > 0) {
val extracted = inv.extract(packet.stack, min(amount, packet.stack.maxCount))
if (extracted.isEmpty) {
break
} else {
amount -= extracted.count
sendPacket(ItemStackPacket(extracted, ipAddress, packet.source))
}
}
}
}
private fun handleCheckCapacity(packet: CheckCapacityPacket) {
getInventory()?.also { inv ->
val transaction = Transaction.openOuter()
val inserted = inv.simulateInsert(ItemVariant.of(packet.stack), packet.stack.count.toLong(), transaction)
transaction.close()
sendPacket(CapacityPacket(packet.stack, inserted.toInt(), this, ipAddress, packet.source))
}
}
private fun handleCheckCapacity(packet: CheckCapacityPacket) {
getInventory()?.also { inv ->
val remaining = inv.attemptInsertion(packet.stack, Simulation.SIMULATE)
val couldAccept = packet.stack.count - remaining.count
sendPacket(CapacityPacket(packet.stack, couldAccept, this, ipAddress, packet.source))
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val inventory = getInventory()
if (inventory != null) {
val transaction = Transaction.openOuter()
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 {
val inventory = getInventory()
if (inventory != null) {
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) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("ProviderPriority", providerPriority)
tag.putInt("ReceiverPriority", receiverPriority)
tag.putBoolean("SyncPriorities", syncPriorities)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("ProviderPriority", providerPriority)
tag.putInt("ReceiverPriority", receiverPriority)
tag.putBoolean("SyncPriorities", syncPriorities)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
providerPriority = tag.getInt("ProviderPriority")
receiverPriority = tag.getInt("ReceiverPriority")
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
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.block.entity.BlockEntity
@ -11,7 +9,6 @@ import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
@ -23,39 +20,43 @@ import java.util.*
/**
* @author shadowfacts
*/
class SwitchBlock: BlockWithEntity<SwitchBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class SwitchBlock : BlockWithEntity<SwitchBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
NetworkComponentBlock {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "switch")
}
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "switch")
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.allOf(Direction::class.java)
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.allOf(Direction::class.java)
}
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? {
return getBlockEntity(world, pos)?.interfaces?.find { it.side == side }
}
override fun getNetworkInterfaceForSide(
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>? {
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))
}
override fun <T : BlockEntity> getTicker(
world: World,
state: BlockState,
type: BlockEntityType<T>
): BlockEntityTicker<T>? {
return if (world.isClient) {
null
} else {
BlockEntityTicker { world, blockPos, blockState, blockEntity ->
(blockEntity as SwitchBlockEntity).tick()
}
}
}
}

View File

@ -20,6 +20,7 @@ import net.shadowfacts.phycon.frame.BasePacketFrame
import net.shadowfacts.phycon.frame.NetworkSplitFrame
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.ItemStackPacket
import net.shadowfacts.phycon.util.IntRingBuffer
import net.shadowfacts.phycon.util.NetworkUtil
import java.lang.ref.WeakReference
import java.util.Deque
@ -28,155 +29,188 @@ import java.util.LinkedList
/**
* @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 {
var SWITCHING_CAPACITY = 256 // 256 packets/tick
}
companion object {
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 destinationCache = Array<WeakReference<Interface>?>(6) { null }
private var packetsHandledThisTick = 0
private var delayedPackets: Deque<Pair<PacketFrame, SwitchInterface>> = LinkedList()
private val macTable = mutableMapOf<MACAddress, Direction>()
private val destinationCache = Array<WeakReference<Interface>?>(6) { null }
val packetStatistics = IntRingBuffer(60) // 1 minute's worth
private val currentSecondPacketStatistics = IntRingBuffer(20)
private var packetsHandledThisTick = 0
private var delayedPackets: Deque<Pair<PacketFrame, SwitchInterface>> = LinkedList()
fun interfaceForSide(side: Direction): SwitchInterface {
return interfaces.find { it.side == side }!!
}
var statisticsObserver: (() -> Unit)? = null
private fun handle(frame: EthernetFrame, fromItf: SwitchInterface) {
macTable[frame.source] = fromItf.side
fun interfaceForSide(side: Direction): SwitchInterface {
return interfaces.find { it.side == side }!!
}
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 handle(frame: EthernetFrame, fromItf: SwitchInterface) {
macTable[frame.source] = fromItf.side
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) {
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)
}
}
resend(frame, fromItf)
}
private fun flood(frame: EthernetFrame, source: SwitchInterface) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) flooding {}", this, source.side, source.macAddress, frame)
for (itf in interfaces) {
if (source == itf) continue
itf.send(frame)
}
}
private fun resend(frame: EthernetFrame, fromItf: SwitchInterface) {
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 findDestination(fromItf: Interface): Interface? {
val side = (fromItf as SwitchInterface).side
return destinationCache[side.ordinal]?.get()
?: NetworkUtil.findConnectedInterface(world!!, pos, side)?.also {
destinationCache[side.ordinal] = WeakReference(it)
}
}
private fun flood(frame: EthernetFrame, source: SwitchInterface) {
PhysicalConnectivity.NETWORK_LOGGER.debug(
"{} ({}, {}) flooding {}",
this,
source.side,
source.macAddress,
frame
)
for (itf in interfaces) {
if (source == itf) continue
itf.send(frame)
}
}
private fun cableDisconnected(itf: SwitchInterface) {
macTable.entries.filter {
it.value == itf.side
}.forEach {
macTable.remove(it.key)
}
destinationCache[itf.side.ordinal] = null
flood(NetworkSplitFrame(itf.macAddress), itf)
}
private fun findDestination(fromItf: Interface): Interface? {
val side = (fromItf as SwitchInterface).side
return destinationCache[side.ordinal]?.get()
?: NetworkUtil.findConnectedInterface(world!!, pos, side)?.also {
destinationCache[side.ordinal] = WeakReference(it)
}
}
fun tick() {
packetsHandledThisTick = 0
private fun cableDisconnected(itf: SwitchInterface) {
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) {
val (frame, fromItf) = delayedPackets.pop()
resend(frame, fromItf)
}
}
fun tick() {
if (statisticsObserver != null) {
if (currentSecondPacketStatistics.size == 20) {
packetStatistics.add(currentSecondPacketStatistics.sum())
currentSecondPacketStatistics.clear()
statisticsObserver?.invoke()
} else {
currentSecondPacketStatistics.add(packetsHandledThisTick)
}
}
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
packetsHandledThisTick = 0
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
val list = NbtList()
for ((frame, fromItf) in delayedPackets) {
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)
}
while (delayedPackets.isNotEmpty() && packetsHandledThisTick <= SWITCHING_CAPACITY) {
val (frame, fromItf) = delayedPackets.pop()
resend(frame, fromItf)
}
}
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
tag.getLongArray("InterfaceAddresses")?.forEachIndexed { i, l ->
interfaces[i].macAddress = MACAddress(l)
}
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.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
val list = NbtList()
for ((frame, fromItf) in delayedPackets) {
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 toUpdatePacket(): Packet<ClientPlayPacketListener>? {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
return tag
}
tag.getLongArray("InterfaceAddresses")?.forEachIndexed { i, l ->
interfaces[i].macAddress = MACAddress(l)
}
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(
val side: Direction,
val switch: WeakReference<SwitchBlockEntity>,
@JvmField var macAddress: MACAddress,
): Interface {
override fun getMACAddress() = macAddress
override fun toUpdatePacket(): Packet<ClientPlayPacketListener>? {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun receive(frame: EthernetFrame) {
switch.get()?.handle(frame, this)
}
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
tag.putIntArray("PacketStatistics", packetStatistics.asContiguousArray())
return tag
}
override fun send(frame: EthernetFrame) {
switch.get()?.findDestination(this)?.receive(frame)
}
class SwitchInterface(
val side: Direction,
val switch: WeakReference<SwitchBlockEntity>,
@JvmField var macAddress: MACAddress,
) : Interface {
override fun getMACAddress() = macAddress
override fun cableDisconnected() {
switch.get()?.cableDisconnected(this)
}
}
override fun receive(frame: EthernetFrame) {
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
*/
class P2PInterfaceBlock: FaceDeviceBlock<P2PInterfaceBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class P2PInterfaceBlock : FaceDeviceBlock<P2PInterfaceBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
@ -24,12 +24,12 @@ class P2PInterfaceBlock: FaceDeviceBlock<P2PInterfaceBlockEntity>(
override val faceThickness = 4.0
override val faceShapes = mapOf(
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.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.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.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.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.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)
)
override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PInterfaceBlockEntity(pos, state)

View File

@ -15,7 +15,8 @@ import net.shadowfacts.phycon.packet.RequestInventoryPacket
/**
* @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

View File

@ -16,10 +16,10 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class P2PReceiverBlock: FaceDeviceBlock<P2PReceiverBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class P2PReceiverBlock : FaceDeviceBlock<P2PReceiverBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
@ -28,12 +28,12 @@ class P2PReceiverBlock: FaceDeviceBlock<P2PReceiverBlockEntity>(
override val faceThickness = 4.0
override val faceShapes = mapOf(
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.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.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.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.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.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)
)
override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PReceiverBlockEntity(pos, state)

View File

@ -21,7 +21,8 @@ import java.lang.ref.WeakReference
/**
* @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 {
enum class Status {

View File

@ -18,58 +18,65 @@ import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
class RedstoneControllerBlock : FaceDeviceBlock<RedstoneControllerBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "redstone_controller")
val POWERED = Properties.POWERED
}
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "redstone_controller")
val POWERED = Properties.POWERED
}
override val faceThickness = 3.0
override val faceShapes = mapOf(
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.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.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)
)
override val faceThickness = 3.0
override val faceShapes = mapOf(
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.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.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)
)
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(POWERED)
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
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 {
val state = super.getPlacementState(context)
return state.with(POWERED, isPowered(context.world, context.blockPos, state[FACING]))
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
val state = super.getPlacementState(context)
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) {
// 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)
}
}
}
override fun neighborUpdate(
state: BlockState,
world: World,
pos: BlockPos,
neighborBlock: Block,
neighborPos: BlockPos,
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 {
val offset = pos.offset(facing)
return world.getEmittedRedstonePower(offset, facing) > 0
}
private fun isPowered(world: World, pos: BlockPos, facing: Direction): Boolean {
val offset = pos.offset(facing)
return world.getEmittedRedstonePower(offset, facing) > 0
}
private fun toggleLit(state: BlockState, world: World, pos: BlockPos) {
world.setBlockState(pos, state.cycle(POWERED), 2)
getBlockEntity(world, pos)!!.redstoneStateChanged()
}
private fun toggleLit(state: BlockState, world: World, pos: BlockPos) {
world.setBlockState(pos, state.cycle(POWERED), 2)
getBlockEntity(world, pos)!!.redstoneStateChanged()
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
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.item.ItemStack
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.init.PhyBlockEntities
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.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.GhostInv
import kotlin.math.round
/**
* @author shadowfacts
*/
class RedstoneEmitterBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.REDSTONE_EMITTER, pos, state),
ClientConfigurableDevice,
GhostInv {
class RedstoneEmitterBlockEntity(pos: BlockPos, state: BlockState) :
DeviceBlockEntity(PhyBlockEntities.REDSTONE_EMITTER, pos, state),
ClientConfigurableDevice,
GhostInv {
private val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
var cachedEmittedPower: Int = 0
private set
private val inventoryCache = mutableMapOf<IPAddress, Storage<ItemVariant>>()
var cachedEmittedPower: Int = 0
private set
var stackToMonitor: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack
get() = stackToMonitor
set(value) { stackToMonitor = value }
var maxAmount = 64
var mode = Mode.ANALOG
set(value) {
field = value
recalculateRedstone()
}
var stackToMonitor: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack
get() = stackToMonitor
set(value) {
stackToMonitor = value
}
var maxAmount = 64
var mode = Mode.ANALOG
set(value) {
field = value
recalculateRedstone()
}
override fun handle(packet: Packet) {
when (packet) {
is ReadGroupedInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
}
}
override fun handle(packet: Packet) {
when (packet) {
is ReadItemStoragePacket -> handleReadItemStorage(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
}
}
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
recalculateRedstone()
}
private fun handleReadItemStorage(packet: ReadItemStoragePacket) {
inventoryCache[packet.source] = packet.inventory
recalculateRedstone()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
recalculateRedstone()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
recalculateRedstone()
}
override fun tick() {
super.tick()
override fun tick() {
super.tick()
if (!world!!.isClient && counter % 20 == 0L) {
if (counter % 80 == 0L) {
updateInventories()
} else if (counter % 20 == 0L) {
recalculateRedstone()
}
}
}
if (!world!!.isClient && counter % 20 == 0L) {
if (counter % 80 == 0L) {
updateInventories()
}
recalculateRedstone()
}
}
private fun updateInventories() {
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
}
private fun updateInventories() {
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
}
private fun recalculateRedstone() {
if (world == null || world!!.isClient) return
private fun recalculateRedstone() {
if (world == null || world!!.isClient) return
if (stackToMonitor.isEmpty) {
cachedEmittedPower = 0
updateWorld()
return
}
val networkAmount = inventoryCache.values.fold(0) { acc, inv ->
acc + inv.getAmount(stackToMonitor)
}
cachedEmittedPower =
when (mode) {
Mode.ANALOG -> if (networkAmount == 0) {
0
} else {
1 + round(networkAmount / maxAmount.toDouble() * 14).toInt()
}
Mode.DIGITAL -> if (networkAmount >= maxAmount) 15 else 0
}
if (stackToMonitor.isEmpty) {
cachedEmittedPower = 0
updateWorld()
return
}
val variant = ItemVariant.of(stackToMonitor)
val networkAmount = inventoryCache.values.fold(0) { acc, inv ->
acc + inv.simulateExtract(variant, Long.MAX_VALUE, null).toInt()
}
cachedEmittedPower =
when (mode) {
Mode.ANALOG -> if (networkAmount == 0) {
0
} else {
1 + (networkAmount / maxAmount.toDouble() * 14).toInt()
}
updateWorld()
}
Mode.DIGITAL -> if (networkAmount >= maxAmount) 15 else 0
}
private fun updateWorld() {
world!!.updateNeighborsAlways(pos, cachedState.block)
world!!.updateNeighborsAlways(pos.offset(cachedState[FaceDeviceBlock.FACING]), cachedState.block)
}
updateWorld()
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.putInt("CachedEmittedPower", cachedEmittedPower)
tag.put("StackToMonitor", stackToMonitor.writeNbt(NbtCompound()))
writeDeviceConfiguration(tag)
}
private fun updateWorld() {
world!!.updateNeighborsAlways(pos, cachedState.block)
world!!.updateNeighborsAlways(pos.offset(cachedState[FaceDeviceBlock.FACING]), cachedState.block)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
cachedEmittedPower = tag.getInt("CachedEmittedPower")
stackToMonitor = ItemStack.fromNbt(tag.getCompound("StackToMonitor"))
loadDeviceConfiguration(tag)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.putInt("CachedEmittedPower", cachedEmittedPower)
tag.put("StackToMonitor", stackToMonitor.writeNbt(NbtCompound()))
writeDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("MaxAmount", maxAmount)
tag.putString("Mode", mode.name)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
cachedEmittedPower = tag.getInt("CachedEmittedPower")
stackToMonitor = ItemStack.fromNbt(tag.getCompound("StackToMonitor"))
loadDeviceConfiguration(tag)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
maxAmount = tag.getInt("MaxAmount")
mode = Mode.valueOf(tag.getString("Mode"))
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("MaxAmount", maxAmount)
tag.putString("Mode", mode.name)
}
enum class Mode {
ANALOG, DIGITAL;
override fun loadDeviceConfiguration(tag: NbtCompound) {
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
*/
class RedstoneEmitterScreen(
handler: RedstoneEmitterScreenHandler,
playerInv: PlayerInventory,
title: Text
): CacaoHandledScreen<RedstoneEmitterScreenHandler>(
handler,
playerInv,
title
handler: RedstoneEmitterScreenHandler,
playerInv: PlayerInventory,
title: Text
) : CacaoHandledScreen<RedstoneEmitterScreenHandler>(
handler,
playerInv,
title
) {
companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/redstone_emitter.png")
}
companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/redstone_emitter.png")
}
init {
backgroundWidth = 176
backgroundHeight = 166
init {
backgroundWidth = 176
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) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
}
class ViewController(
private val emitter: RedstoneEmitterBlockEntity,
): net.shadowfacts.cacao.viewcontroller.ViewController() {
class ViewController(
private val emitter: RedstoneEmitterBlockEntity,
) : net.shadowfacts.cacao.viewcontroller.ViewController() {
lateinit var halfLabel: Label
lateinit var fullLabel: Label
lateinit var halfLabel: Label
lateinit var fullLabel: Label
override fun viewDidLoad() {
super.viewDidLoad()
override fun viewDidLoad() {
super.viewDidLoad()
val title = Label(PhyBlocks.REDSTONE_EMITTER.name)
title.textColor = Color.TEXT
view.addSubview(title)
val title = Label(PhyBlocks.REDSTONE_EMITTER.name)
title.textColor = Color.TEXT
view.addSubview(title)
val inv = Label(MinecraftClient.getInstance().player!!.inventory.displayName)
inv.textColor = Color.TEXT
view.addSubview(inv)
val inv = Label(MinecraftClient.getInstance().player!!.inventory.displayName)
inv.textColor = Color.TEXT
view.addSubview(inv)
val field = NumberField(emitter.maxAmount) {
if (it.number != null) {
emitter.maxAmount = it.number!!
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureDevice(emitter))
}
updateLabelTexts()
}
field.validator = { it >= 0 }
field.drawBackground = false
view.addSubview(field)
val field = NumberField(emitter.maxAmount) {
if (it.number != null) {
emitter.maxAmount = it.number!!
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureDevice(emitter))
}
updateLabelTexts()
}
field.validator = { it >= 0 }
field.drawBackground = false
view.addSubview(field)
val hStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL, spacing = 2.0)
view.addSubview(hStack)
val hStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL, spacing = 2.0)
view.addSubview(hStack)
val zeroStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
zeroStack.addArrangedSubview(Label(TranslatableText("gui.phycon.emitter.count", 0), textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT
}
zeroStack.addArrangedSubview(View())
zeroStack.addArrangedSubview(Label("0", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED
}
val zeroStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
zeroStack.addArrangedSubview(
Label(
TranslatableText("gui.phycon.emitter.count", 0),
textAlignment = Label.TextAlignment.CENTER
)
).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))
halfLabel = halfStack.addArrangedSubview(Label("half", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT
}
halfStack.addArrangedSubview(View())
halfStack.addArrangedSubview(Label("8", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED
}
val halfStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
halfLabel = halfStack.addArrangedSubview(Label("half", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT
}
halfStack.addArrangedSubview(View())
halfStack.addArrangedSubview(Label("8", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED
}
val fullStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
fullLabel = fullStack.addArrangedSubview(Label("full", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT
}
fullStack.addArrangedSubview(View())
fullStack.addArrangedSubview(Label("15", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED
}
val fullStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL))
fullLabel = fullStack.addArrangedSubview(Label("full", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.TEXT
}
fullStack.addArrangedSubview(View())
fullStack.addArrangedSubview(Label("15", textAlignment = Label.TextAlignment.CENTER)).apply {
textColor = Color.RED
}
updateLabelTexts()
updateLabelTexts()
view.solver.dsl {
val minX = Variable("minX")
val minY = Variable("minY")
minX equalTo ((view.widthAnchor - 176) / 2)
minY equalTo ((view.heightAnchor - 166) / 2)
view.solver.dsl {
val minX = Variable("minX")
val minY = Variable("minY")
minX equalTo ((view.widthAnchor - 176) / 2)
minY equalTo ((view.heightAnchor - 166) / 2)
title.leftAnchor equalTo (minX + 8)
title.topAnchor equalTo (minY + 6)
title.leftAnchor equalTo (minX + 8)
title.topAnchor equalTo (minY + 6)
inv.leftAnchor equalTo (minX + 8)
inv.topAnchor equalTo (minY + 72)
inv.leftAnchor equalTo (minX + 8)
inv.topAnchor equalTo (minY + 72)
field.widthAnchor equalTo 82
field.heightAnchor equalTo 11
field.leftAnchor equalTo (minX + 57)
field.topAnchor equalTo (minY + 23)
field.widthAnchor equalTo 82
field.heightAnchor equalTo 11
field.leftAnchor equalTo (minX + 57)
field.topAnchor equalTo (minY + 23)
hStack.centerXAnchor equalTo view.centerXAnchor
hStack.widthAnchor equalTo (176 - 4)
hStack.topAnchor equalTo (field.bottomAnchor + 8)
hStack.bottomAnchor equalTo inv.topAnchor
hStack.centerXAnchor equalTo view.centerXAnchor
hStack.widthAnchor equalTo (176 - 4)
hStack.topAnchor equalTo (field.bottomAnchor + 8)
hStack.bottomAnchor equalTo inv.topAnchor
zeroStack.widthAnchor equalTo halfStack.widthAnchor
halfStack.widthAnchor equalTo fullStack.widthAnchor
}
}
zeroStack.widthAnchor equalTo halfStack.widthAnchor
halfStack.widthAnchor equalTo fullStack.widthAnchor
}
}
private fun updateLabelTexts() {
halfLabel.text = TranslatableText("gui.phycon.emitter.count", ceil(emitter.maxAmount / 2.0).toInt())
fullLabel.text = TranslatableText("gui.phycon.emitter.count", emitter.maxAmount)
window!!.layout()
}
}
private fun updateLabelTexts() {
halfLabel.text = TranslatableText("gui.phycon.emitter.count", ceil(emitter.maxAmount / 2.0).toInt())
fullLabel.text = TranslatableText("gui.phycon.emitter.count", emitter.maxAmount)
window!!.layout()
}
}
}

View File

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

View File

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

View File

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

View File

@ -27,178 +27,197 @@ import kotlin.math.min
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreen<BE: AbstractTerminalBlockEntity, T: AbstractTerminalScreenHandler<BE>>(
handler: T,
playerInv: PlayerInventory,
title: Text,
val terminalBackgroundWidth: Int,
val terminalBackgroundHeight: Int,
): CacaoHandledScreen<T>(handler, playerInv, title) {
abstract class AbstractTerminalScreen<BE : AbstractTerminalBlockEntity, T : AbstractTerminalScreenHandler<BE>>(
handler: T,
playerInv: PlayerInventory,
title: Text,
val terminalBackgroundWidth: Int,
val terminalBackgroundHeight: Int,
) : CacaoHandledScreen<T>(handler, playerInv, title) {
interface SearchQueryListener {
fun terminalSearchQueryChanged(newValue: String)
fun requestTerminalSearchFieldUpdate(): String?
}
interface SearchQueryListener {
fun terminalSearchQueryChanged(newValue: String)
fun requestTerminalSearchFieldUpdate(): String?
}
companion object {
var searchQueryListener: SearchQueryListener? = null
}
companion object {
var searchQueryListener: SearchQueryListener? = null
}
abstract val backgroundTexture: Identifier
abstract val backgroundTexture: Identifier
val terminalVC: AbstractTerminalViewController<*, *, *>
var amountVC: TerminalRequestAmountViewController? = null
val terminalVC: AbstractTerminalViewController<*, *, *>
var amountVC: TerminalRequestAmountViewController? = null
private var prevSearchQuery = ""
var searchQuery = ""
set(value) {
field = value
if (prevSearchQuery != value) {
searchQueryListener?.terminalSearchQueryChanged(value)
}
prevSearchQuery = value
}
var scrollPosition = 0.0
private var prevSearchQuery = ""
var searchQuery = ""
set(value) {
field = value
if (prevSearchQuery != value) {
searchQueryListener?.terminalSearchQueryChanged(value)
}
prevSearchQuery = value
}
var scrollPosition = 0.0
init {
backgroundWidth = terminalBackgroundWidth
backgroundHeight = terminalBackgroundHeight
init {
backgroundWidth = terminalBackgroundWidth
backgroundHeight = terminalBackgroundHeight
terminalVC = createViewController()
addWindow(ScreenHandlerWindow(handler, terminalVC))
terminalVC = createViewController()
addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems()
}
requestUpdatedItems()
}
abstract fun createViewController(): AbstractTerminalViewController<*, *, *>
abstract fun createViewController(): AbstractTerminalViewController<*, *, *>
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat()))
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(
C2STerminalUpdateDisplayedItems(
handler.terminal,
searchQuery,
scrollPosition.toFloat()
)
)
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
// draw damage bar
// empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
// 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()
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
// draw damage bar
// 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) {
RenderSystem.setShader(GameRenderer::getPositionTexColorShader)
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 drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
override fun handledScreenTick() {
super.handledScreenTick()
drawBackgroundTexture(matrixStack)
}
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
open fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.setShader(GameRenderer::getPositionTexColorShader)
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)
}
val newSearchQuery = searchQueryListener?.requestTerminalSearchFieldUpdate()
if (newSearchQuery != null && searchQuery != newSearchQuery) {
searchQuery = newSearchQuery
terminalVC.searchField.text = newSearchQuery
requestUpdatedItems()
}
}
override fun handledScreenTick() {
super.handledScreenTick()
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id) && handler.cursorStack.isEmpty) {
val stack = slot.stack
val newSearchQuery = searchQueryListener?.requestTerminalSearchFieldUpdate()
if (newSearchQuery != null && searchQuery != newSearchQuery) {
searchQuery = newSearchQuery
terminalVC.searchField.text = newSearchQuery
requestUpdatedItems()
}
}
if (type == SlotActionType.QUICK_MOVE) {
// shift click, request full stack
requestItem(stack, min(stack.count, stack.maxCount))
} else if (type == SlotActionType.PICKUP) {
if (clickData == 1) {
// right click, request half stack
requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
} else {
showRequestAmountDialog(stack)
}
}
}
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
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()
}
}
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id) && handler.cursorStack.isEmpty) {
val stack = slot.stack
if (type == SlotActionType.QUICK_MOVE) {
// shift click, request full stack
requestItem(stack, min(stack.count, stack.maxCount))
} else if (type == SlotActionType.PICKUP) {
if (clickData == 1) {
// right click, request half stack
requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
} else {
showRequestAmountDialog(stack)
}
}
}
}
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
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.ScreenHandlerType
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.DefaultPlugin
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.copyWithCount
import net.shadowfacts.phycon.util.name
import java.lang.ref.WeakReference
import kotlin.math.ceil
import kotlin.math.max
@ -28,227 +24,252 @@ import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreenHandler<T: AbstractTerminalBlockEntity>(
handlerType: ScreenHandlerType<*>,
syncId: Int,
val playerInv: PlayerInventory,
val terminal: T,
): ScreenHandler(handlerType, syncId),
AbstractTerminalBlockEntity.NetItemObserver {
abstract class AbstractTerminalScreenHandler<T : AbstractTerminalBlockEntity>(
handlerType: ScreenHandlerType<*>,
syncId: Int,
val playerInv: PlayerInventory,
val terminal: T,
) : ScreenHandler(handlerType, syncId),
AbstractTerminalBlockEntity.NetItemObserver {
private val rowsDisplayed = 6
private val rowsDisplayed = 6
private val fakeInv = FakeInventory(this)
private var searchQuery: String = ""
private var settings = TerminalSettings()
var totalEntries = 0
private set
var scrollPosition = 0f
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
it.stack.copyWithCount(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
private val fakeInv = FakeInventory(this)
private var searchQuery: String = ""
private var settings = TerminalSettings()
var totalEntries = 0
private set
var scrollPosition = 0f
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
it.variant.toStack(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
open val xOffset: Int = 0
open val xOffset: Int = 0
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
// intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings
}
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
// intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings
}
val xOffset = xOffset
val xOffset = xOffset
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, xOffset + 66 + x * 18, 18 + y * 18))
}
}
// network
for (y in 0 until 6) {
for (x in 0 until 9) {
addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, xOffset + 66 + x * 18, 18 + y * 18))
}
}
// internal buffer
for (y in 0 until 6) {
for (x in 0 until 3) {
addSlot(Slot(terminal.internalBuffer, y * 3 + x, xOffset + 8 + x * 18, 18 + y * 18))
}
}
// internal buffer
for (y in 0 until 6) {
for (x in 0 until 3) {
addSlot(Slot(terminal.internalBuffer, y * 3 + x, xOffset + 8 + x * 18, 18 + y * 18))
}
}
// player inv
for (y in 0 until 3) {
for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, xOffset + 66 + x * 18, 140 + y * 18))
}
}
// hotbar
for (x in 0 until 9) {
addSlot(Slot(playerInv, x, xOffset + 66 + x * 18, 198))
}
}
// player inv
for (y in 0 until 3) {
for (x in 0 until 9) {
addSlot(Slot(playerInv, x + y * 9 + 9, xOffset + 66 + x * 18, 140 + y * 18))
}
}
// hotbar
for (x in 0 until 9) {
addSlot(Slot(playerInv, x, xOffset + 66 + x * 18, 198))
}
}
override fun netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
override fun netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1)
val key = Registry.ITEM.getKey(it.key.item)
if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) {
return@filter true
}
}
it.key.name.string.contains(searchQuery, true)
}
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) {
val unprefixed = searchQuery.drop(1)
val key = Registry.ITEM.getKey(it.key.item)
if (key.isPresent && key.get().value.namespace.startsWith(unprefixed, true)) {
return@filter true
}
}
// 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 =
when (settings[DefaultPlugin.SORT_MODE]) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
val sorted =
when (settings[DefaultPlugin.SORT_MODE]) {
SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue }
SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue }
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
}
val offsetInItems = currentScrollOffsetInItems()
val end = min(offsetInItems + rowsDisplayed * 9, sorted.size)
itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) }
val offsetInItems = currentScrollOffsetInItems()
val end = min(offsetInItems + rowsDisplayed * 9, sorted.size)
itemEntries = sorted.subList(offsetInItems, end).map { Entry(it.key, it.intValue) }
// itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, settings, scrollPosition, totalEntries))
}
(player as ServerPlayerEntity).networkHandler.sendPacket(
S2CTerminalUpdateDisplayedItems(
terminal,
itemEntries,
searchQuery,
settings,
scrollPosition,
totalEntries
)
)
}
fun totalRows(): Int {
return ceil(totalEntries / 9f).toInt()
}
fun totalRows(): Int {
return ceil(totalEntries / 9f).toInt()
}
fun maxScrollOffsetInRows(): Int {
return totalRows() - rowsDisplayed
}
fun maxScrollOffsetInRows(): Int {
return totalRows() - rowsDisplayed
}
fun currentScrollOffsetInRows(): Int {
return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt())
}
fun currentScrollOffsetInRows(): Int {
return max(0, (scrollPosition * maxScrollOffsetInRows()).roundToInt())
}
fun currentScrollOffsetInItems(): Int {
return currentScrollOffsetInRows() * 9
}
fun currentScrollOffsetInItems(): Int {
return currentScrollOffsetInRows() * 9
}
fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, settings: TerminalSettings, scrollPosition: Float) {
this.searchQuery = query
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun sendUpdatedItemsToClient(
player: ServerPlayerEntity,
query: String,
settings: TerminalSettings,
scrollPosition: Float
) {
this.searchQuery = query
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
this.searchQuery = query
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun canUse(player: PlayerEntity): Boolean {
return true
}
override fun close(player: PlayerEntity) {
super.close(player)
override fun close(player: PlayerEntity) {
super.close(player)
terminal.netItemObserver = null
}
terminal.netItemObserver = null
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity) {
if (isBufferSlot(slotId)) {
// todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !cursorStack.isEmpty) {
// placing cursor stack into buffer
val bufferSlot = slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index
terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK)
}
}
super.onSlotClick(slotId, clickData, actionType, player)
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity) {
if (isBufferSlot(slotId)) {
// todo: why does this think it's quick_craft sometimes?
if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !cursorStack.isEmpty) {
// placing cursor stack into buffer
val bufferSlot =
slotId - bufferSlotsStart // subtract 54 to convert the handler slot ID to a valid buffer index
terminal.internalBuffer.markSlot(bufferSlot, TerminalBufferInventory.Mode.TO_NETWORK)
}
}
super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY;
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
if (isNetworkSlot(slotId)) {
return ItemStack.EMPTY;
}
val slot = slots[slotId]
if (!slot.hasStack()) {
return ItemStack.EMPTY
}
val slot = slots[slotId]
if (!slot.hasStack()) {
return ItemStack.EMPTY
}
val result = slot.stack.copy()
val result = slot.stack.copy()
if (isBufferSlot(slotId)) {
// last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
if (slot.stack.isEmpty) {
terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED)
}
} else if (isPlayerSlot(slotId)) {
val slotsInsertedInto = tryInsertItem(slot.stack, bufferSlotsStart until playerSlotsStart) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK }
slotsInsertedInto.forEach { terminal.internalBuffer.markSlot(it - bufferSlotsStart, TerminalBufferInventory.Mode.TO_NETWORK) }
if (slotsInsertedInto.isEmpty()) {
return ItemStack.EMPTY
}
}
if (isBufferSlot(slotId)) {
// last boolean param is fromLast
if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
if (slot.stack.isEmpty) {
terminal.internalBuffer.markSlot(slotId - bufferSlotsStart, TerminalBufferInventory.Mode.UNASSIGNED)
}
} else if (isPlayerSlot(slotId)) {
val slotsInsertedInto = tryInsertItem(
slot.stack,
bufferSlotsStart until playerSlotsStart
) { terminal.internalBuffer.getMode(it - bufferSlotsStart) != TerminalBufferInventory.Mode.FROM_NETWORK }
slotsInsertedInto.forEach {
terminal.internalBuffer.markSlot(
it - bufferSlotsStart,
TerminalBufferInventory.Mode.TO_NETWORK
)
}
if (slotsInsertedInto.isEmpty()) {
return ItemStack.EMPTY
}
}
return result
}
return result
}
private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection<Int> {
val slotsInsertedInto = mutableListOf<Int>()
for (index in slots) {
if (stack.isEmpty) break
if (!slotPredicate(index)) continue
private fun tryInsertItem(stack: ItemStack, slots: IntRange, slotPredicate: (Int) -> Boolean): Collection<Int> {
val slotsInsertedInto = mutableListOf<Int>()
for (index in slots) {
if (stack.isEmpty) break
if (!slotPredicate(index)) continue
val slot = this.slots[index]
val slotStack = slot.stack
if (slotStack.isEmpty) {
slot.stack = stack.copy()
stack.count = 0
val slot = this.slots[index]
val slotStack = slot.stack
if (slotStack.isEmpty) {
slot.stack = stack.copy()
stack.count = 0
slot.markDirty()
slotsInsertedInto.add(index)
} else if (ItemStack.canCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
val maxToMove = slotStack.maxCount - slotStack.count
val toMove = min(maxToMove, stack.count)
slotStack.increment(toMove)
stack.decrement(toMove)
slot.markDirty()
slotsInsertedInto.add(index)
} else if (ItemStack.canCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
val maxToMove = slotStack.maxCount - slotStack.count
val toMove = min(maxToMove, stack.count)
slotStack.increment(toMove)
stack.decrement(toMove)
slot.markDirty()
slotsInsertedInto.add(index)
}
}
return slotsInsertedInto
}
slot.markDirty()
slotsInsertedInto.add(index)
}
}
return slotsInsertedInto
}
val networkSlotsStart = 0
val networkSlotsEnd = 54
val bufferSlotsStart = 54
val bufferSlotsEnd = 72
val playerSlotsStart = 72
val playerSlotsEnd = 72 + 36
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart
val networkSlotsStart = 0
val networkSlotsEnd = 54
val bufferSlotsStart = 54
val bufferSlotsEnd = 72
val playerSlotsStart = 72
val playerSlotsEnd = 72 + 36
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
data class Entry(val variant: ItemVariant, val amount: Int)
}

View File

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

View File

@ -9,12 +9,12 @@ import net.shadowfacts.phycon.PhysicalConnectivity
/**
* @author shadowfacts
*/
class CraftingTerminalBlock: AbstractTerminalBlock<CraftingTerminalBlockEntity>() {
class CraftingTerminalBlock : AbstractTerminalBlock<CraftingTerminalBlockEntity>() {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "crafting_terminal")
}
companion object {
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
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import it.unimi.dsi.fastutil.ints.IntBinaryOperator
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
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.entity.player.PlayerEntity
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.packet.ItemStackPacket
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.toTag
import java.util.LinkedList
import kotlin.math.min
/**
* @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) {
super.onActivate(player)
override fun onActivate(player: PlayerEntity) {
super.onActivate(player)
if (!world!!.isClient) {
val factory = object: ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? {
return CraftingTerminalScreenHandler(syncId, playerInv, this@CraftingTerminalBlockEntity)
}
if (!world!!.isClient) {
val factory = object : ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? {
return CraftingTerminalScreenHandler(syncId, playerInv, this@CraftingTerminalBlockEntity)
}
override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal")
override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal")
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos)
}
}
player.openHandledScreen(factory)
}
}
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos)
}
}
player.openHandledScreen(factory)
}
}
fun requestItemsForCrafting(maxAmount: Int) {
val amounts = ItemStackCollections.map<IntArray>()
fun requestItemsForCrafting(maxAmount: Int) {
// 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 craftingInvStack = craftingInv.getStack(i)
if (craftingInvStack.isEmpty) continue
if (craftingInvStack.count >= craftingInvStack.maxCount) continue
val or = IntBinaryOperator { a, b -> a or b }
for (i in 0 until craftingInv.size()) {
val craftingInvStack = craftingInv.getStack(i)
if (craftingInvStack.isEmpty) continue
if (craftingInvStack.count >= craftingInvStack.maxCount) continue
if (craftingInvStack !in amounts) amounts[craftingInvStack] = IntArray(9) { 0 }
amounts[craftingInvStack]!![i] = min(maxAmount, craftingInvStack.maxCount - craftingInvStack.count)
}
stackToSlotsMap.mergeInt(ItemVariant.of(craftingInvStack), 1 shl i, or)
}
for ((stack, amountPerSlot) in amounts) {
val total = amountPerSlot.sum()
val request = CraftingStackLocateRequest(stack, total, counter, amountPerSlot)
pendingRequests.add(request)
sendPacket(LocateStackPacket(stack, ipAddress))
}
}
for ((variant, slots) in stackToSlotsMap) {
val total = slots.countOneBits()
val stack = variant.toStack()
val request = CraftingStackLocateRequest(stack, total, counter, slots)
pendingRequests.add(request)
sendPacket(LocateStackPacket(stack, ipAddress))
}
}
override fun stackLocateRequestCompleted(request: StackLocateRequest) {
if (request is CraftingStackLocateRequest) {
completedCraftingStackRequests.add(request)
}
override fun stackLocateRequestCompleted(request: StackLocateRequest) {
if (request is CraftingStackLocateRequest) {
completedCraftingStackRequests.add(request)
}
super.stackLocateRequestCompleted(request)
}
super.stackLocateRequestCompleted(request)
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val craftingReq = completedCraftingStackRequests.find { ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) }
if (craftingReq != null) {
var remaining = packet.stack.copy()
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val craftingReq =
completedCraftingStackRequests.find { it.stack.equalsIgnoringAmount(packet.stack) }
if (craftingReq != null) {
var remaining = packet.stack.copy()
for (i in 0 until craftingInv.size()) {
val currentStack = craftingInv.getStack(i)
if (currentStack.count >= currentStack.maxCount) continue
if (!ItemStackUtil.areEqualIgnoreAmounts(currentStack, remaining)) continue
for (i in 0 until craftingInv.size()) {
val currentStack = craftingInv.getStack(i)
if (currentStack.count >= currentStack.maxCount) continue
if (!currentStack.equalsIgnoringAmount(remaining)) continue
val toInsert = minOf(remaining.count, currentStack.maxCount - currentStack.count, craftingReq.amountPerSlot[i])
currentStack.count += toInsert
remaining.count -= toInsert
craftingReq.amountPerSlot[i] -= toInsert
craftingReq.received += toInsert
currentStack.count += 1
remaining.count -= 1
if (remaining.isEmpty) {
break
}
}
craftingReq.slots = craftingReq.slots and (1 shl i).inv()
craftingReq.received += 1
if (craftingReq.amountPerSlot.sum() == 0 || craftingReq.received >= craftingReq.totalResultAmount) {
completedCraftingStackRequests.remove(craftingReq)
}
if (remaining.isEmpty) {
break
}
}
if (!remaining.isEmpty) {
remaining = internalBuffer.insert(remaining, TerminalBufferInventory.Mode.FROM_NETWORK)
}
// if slots == 0, there are no more slots needing items for this request
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
} else {
return super.doHandleItemStack(packet)
}
}
updateAndSync()
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.put("CraftingInv", craftingInv.toTag())
}
return remaining
} else {
return super.doHandleItemStack(packet)
}
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
craftingInv.fromTag(tag.getList("CraftingInv", 10))
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.put("CraftingInv", craftingInv.toTag())
}
class CraftingStackLocateRequest(
stack: ItemStack,
amount: Int,
timestamp: Long,
val amountPerSlot: IntArray,
): StackLocateRequest(stack, amount, timestamp) {
var received: Int = 0
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
craftingInv.fromTag(tag.getList("CraftingInv", 10))
}
class CraftingStackLocateRequest(
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
*/
class CraftingTerminalScreen(
handler: CraftingTerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
): AbstractTerminalScreen<CraftingTerminalBlockEntity, CraftingTerminalScreenHandler>(
handler,
playerInv,
title,
259,
252,
handler: CraftingTerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
) : AbstractTerminalScreen<CraftingTerminalBlockEntity, CraftingTerminalScreenHandler>(
handler,
playerInv,
title,
259,
252,
) {
companion object {
private val BACKGROUND_1 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_1.png")
private val BACKGROUND_2 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_2.png")
}
companion object {
private val BACKGROUND_1 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_1.png")
private val BACKGROUND_2 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_2.png")
}
override val backgroundTexture = BACKGROUND_1
override val backgroundTexture = BACKGROUND_1
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return CraftingTerminalViewController(this, handler)
}
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return CraftingTerminalViewController(this, handler)
}
override fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND_1)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, 256, 252)
override fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, BACKGROUND_1)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, 256, 252)
RenderSystem.setShaderTexture(0, BACKGROUND_2)
drawTexture(matrixStack, x + 256, y, 0, 0, 3, 252)
}
RenderSystem.setShaderTexture(0, BACKGROUND_2)
drawTexture(matrixStack, x + 256, y, 0, 0, 3, 252)
}
}

View File

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

View File

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

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