Compare commits

...

57 Commits
v0.1.0 ... main

Author SHA1 Message Date
Shadowfacts b64a05e0ad Fix P2P receiver constantly flooding network with request inventory packets 2023-03-13 11:24:55 -04:00
Shadowfacts 73de26387a Fix P2P receiver not invalidating target on netsplit 2023-03-13 11:21:53 -04:00
Shadowfacts 33614e0dc6 Tweak NetworkStackDispatcher behavior on incomplete insertions 2023-03-12 22:53:50 -04:00
Shadowfacts c18af9794b Store inventories as weak references in interface 2023-03-12 22:28:36 -04:00
Shadowfacts 6d97af8bdc Fix terminal not updating when all networked inventories are disconnected 2023-03-12 22:26:35 -04:00
Shadowfacts d527185888 Cache destination interfaces 2023-03-12 22:18:34 -04:00
Shadowfacts 82482ca9c6 Disable plugins 2023-03-12 22:15:32 -04:00
Shadowfacts 9200dea350 1.18.2 2022-06-16 10:45:32 -04:00
Shadowfacts e88ecd3215 Add P2P inventory system 2021-12-24 15:47:33 -05:00
Shadowfacts 9cbad193e2 fancy new java feature 2021-12-24 11:56:49 -05:00
Shadowfacts b416f98ef6 Fix not being able to place face devices by right-clicking non-full blocks 2021-12-23 14:41:55 -05:00
Shadowfacts b2e794e5a4 Add REI sync mode for terminal search field 2021-12-23 14:41:55 -05:00
Shadowfacts b21a45fbbb Fix twisted pair advancement not matching recipe 2021-12-23 12:53:22 -05:00
Shadowfacts ef9aa9e958 Fix terminal search field not unfocusing when REI focused 2021-12-23 11:36:46 -05:00
Shadowfacts c5ede3bd62 Add Cotton Resources tags 2021-12-22 22:35:28 -05:00
Shadowfacts 30300dbc40 Update REI plugin 2021-12-22 22:17:25 -05:00
Shadowfacts 5488cc295a Initial update to 1.18.1 2021-12-22 22:12:11 -05:00
Shadowfacts 172536cdb8 Fix magenta cable recipe 2021-11-08 18:19:44 -05:00
Shadowfacts 46e00cea97
Add Crafting Terminal loot table 2021-04-03 10:42:17 -04:00
Shadowfacts ce511e62e1
Fix dupe when shift-clicking crafting results out of terminal 2021-04-03 10:32:53 -04:00
Shadowfacts 5eb948802c
Fix crash when shift-clicking stack into terminal buffer but no slots available 2021-04-03 10:32:27 -04:00
Shadowfacts 7447c89394
TechReborn: Fix being client-only 2021-03-29 19:21:43 -04:00
Shadowfacts 87f0bdb85a
REI: Add support for filling recipes in the Crafting Terminal 2021-03-29 19:18:15 -04:00
Shadowfacts 236d6707f6
Don't show amount dialog if holding stack in cursor 2021-03-29 18:45:28 -04:00
Shadowfacts 961c74de34
Add Crafting Terminal recipe and advancement 2021-03-28 14:57:34 -04:00
Shadowfacts c278d137ef
Fix terminal model Z fighting 2021-03-28 14:57:34 -04:00
Shadowfacts 12e055d645
Add Crafting Terminal texture/model 2021-03-28 14:57:34 -04:00
Shadowfacts 9d98481ba5
Add Crafting Terminal 2021-03-28 14:57:32 -04:00
Shadowfacts e4662b0f6f
Fix terminal appearing to change buffer mode to from_network when insertion fails 2021-03-28 14:56:38 -04:00
Shadowfacts 4fa5a12746
Cleanup TerminalBufferInventory code 2021-03-28 14:56:38 -04:00
Shadowfacts 7286efcfc2
Cacao: Fix changing button backgrounds after adding button to superview 2021-03-28 14:56:38 -04:00
Shadowfacts 7cb0168c2f
Cacao: Add button tooltip 2021-03-28 14:56:38 -04:00
Shadowfacts b435948ee3
Cacao: Fix button not flooring offsets 2021-03-28 14:56:38 -04:00
Shadowfacts c15700bf5d
Extract terminal stuff to abstract base classes 2021-03-28 14:56:36 -04:00
Shadowfacts 9418ff8917
Move ScrollTrackView package 2021-03-25 22:16:28 -04:00
Shadowfacts 6f1f43f36a
Move screen package to client package 2021-03-25 22:15:46 -04:00
Shadowfacts f12965fc6c
Bump version 2021-03-24 17:28:40 -04:00
Shadowfacts 28e14ae8bf
Add terminal settings API 2021-03-24 17:28:03 -04:00
Shadowfacts e41c9e3ccb
Fix not being able to close just terminal amount dialog 2021-03-23 18:20:59 -04:00
Shadowfacts b87a36caa4
Add TechReborn plugin 2021-03-22 22:08:08 -04:00
Shadowfacts 93b082ee55
Fix REI not receiving keyboard shortcuts while focused 2021-03-22 17:50:48 -04:00
Shadowfacts 2466923d96
Fix crash when opening Inserter/Redstone Emitter GUI in non-dev env 2021-03-21 11:48:45 -04:00
Shadowfacts 84e2c6d6e9
Convert Terminal screen to Cacao 2021-03-20 22:47:57 -04:00
Shadowfacts 2774cabfcc
Cacao: Misc things 2021-03-20 14:48:59 -04:00
Shadowfacts 81ce590231
Tweak Cable recipes 2021-03-20 12:15:47 -04:00
Shadowfacts f0fe1e4a3d
Cacao: Add layout guides 2021-03-20 11:40:00 -04:00
Shadowfacts f375d157b0
Improve Terminal GUI network amount rendering 2021-03-18 22:30:00 -04:00
Shadowfacts cbea57006a
Add basic Mouse Wheelie integration 2021-03-18 18:24:01 -04:00
Shadowfacts a1df6cda25
Fix kiwi not being included in build 2021-03-17 22:53:40 -04:00
Shadowfacts 47ff975449
Fix cable shape cache not working
There are different BlockState objects for each color of cable :S
2021-03-15 21:44:54 -04:00
Shadowfacts 369dcebe1b
Add glowing-screen Terminal model 2021-03-15 19:58:53 -04:00
Shadowfacts ee6fb1e725
Fix cable recipes, advancements, loot tables, and face device drops 2021-03-15 19:55:54 -04:00
Shadowfacts bc50017b4a
Add cable colors to face device blocks 2021-03-15 18:30:35 -04:00
Shadowfacts 611c4bb0ae
Optimize images 2021-03-14 15:54:12 -04:00
Shadowfacts 4ab6dbbf38
Add cable colors 2021-03-14 15:48:09 -04:00
Shadowfacts f536bf72a9
Convert Cable block model to code 2021-03-14 14:07:56 -04:00
Shadowfacts a86058f8bd
Add copy IP/MAC buttons 2021-03-13 15:48:33 -05:00
388 changed files with 6224 additions and 2067 deletions

View File

@ -1,8 +1,7 @@
plugins {
id "fabric-loom" version "0.6.49"
id "fabric-loom" version "0.12.9"
id "maven-publish"
id "org.jetbrains.kotlin.jvm" version "1.4.30"
id "com.github.johnrengelman.shadow" version "4.0.4"
id "org.jetbrains.kotlin.jvm" version "1.6.10"
}
archivesBaseName = project.archives_base_name
@ -11,8 +10,8 @@ group = project.maven_group
allprojects {
pluginManager.withPlugin("java") {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
@ -44,6 +43,8 @@ configure(allprojects.findAll { it.name != "kiwi-java" }) {
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
// Minecraft 1.18 (1.18-pre2) upwards uses Java 17.
options.release = 17
}
java {
@ -56,20 +57,23 @@ configure(allprojects.findAll { it.name != "kiwi-java" }) {
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
compileKotlin {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
kotlinOptions.jvmTarget = JavaVersion.VERSION_17
}
}
}
minecraft {
loom {
log4jConfigs.from "PhyConDebugLogging.xml"
}
repositories {
maven { url "https://mod-buildcraft.com/maven" }
maven { url "http://server.bbkr.space:8081/artifactory/libs-release/" }
// maven { url "https://server.bbkr.space:8081/artifactory/libs-release/" }
maven { url "https://maven.terraformersmc.com/releases" }
jcenter()
maven { url "https://maven.shedaniel.me/" }
maven { url "https://maven.siphalor.de/" }
maven { url "https://jitpack.io" }
mavenCentral()
}
dependencies {
@ -79,16 +83,28 @@ dependencies {
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-items:${project.libblockattributes_version}"
compile project(":kiwi-java")
shadow project(":kiwi-java")
implementation project(":kiwi-java")
include project(":kiwi-java")
runtimeOnly project(":plugin:rei")
include project(":plugin:rei")
// modRuntimeOnly "de.siphalor:mousewheelie-1.18:${project.mousewheelie_version}"
// runtimeOnly(project(":plugin:mousewheelie")) {
// transitive = false
// }
// include project(":plugin:mousewheelie")
//
// modRuntimeOnly "me.shedaniel:RoughlyEnoughItems-fabric:${project.rei_version}"
// runtimeOnly(project(":plugin:rei")) {
// transitive = false
// }
// include project(":plugin:rei")
modRuntime("io.github.cottonmc:cotton-resources:${project.cotton_resources_version}") {
exclude group: "net.fabricmc.fabric-api"
}
modRuntime("com.terraformersmc:modmenu:${project.modmenu_version}") {
// runtimeOnly project(":plugin:techreborn")
// include project(":plugin:techreborn")
// modRuntime("io.github.cottonmc:cotton-resources:${project.cotton_resources_version}") {
// exclude group: "net.fabricmc.fabric-api"
// }
modRuntimeOnly("com.terraformersmc:modmenu:${project.modmenu_version}") {
exclude group: "net.fabricmc.fabric-api"
}
@ -102,22 +118,22 @@ jar {
}
// configure the maven publication
publishing {
publications {
mavenJava(MavenPublication) {
// add all the jars that should be included when publishing to maven
artifact(remapJar) {
builtBy remapJar
}
artifact(sourcesJar) {
builtBy remapSourcesJar
}
}
}
// publishing {
// publications {
// mavenJava(MavenPublication) {
// // add all the jars that should be included when publishing to maven
// artifact(remapJar) {
// builtBy remapJar
// }
// artifact(sourcesJar) {
// builtBy remapSourcesJar
// }
// }
// }
// select the repositories you want to publish to
repositories {
// uncomment to publish to the local maven
// mavenLocal()
}
}
// // select the repositories you want to publish to
// repositories {
// // uncomment to publish to the local maven
// // mavenLocal()
// }
// }

35
colorize-cables.sh Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
base_color=blue
modulate() {
echo $1, $2
mkdir -p src/main/resources/assets/phycon/textures/block/cable/$1
for f in src/main/resources/assets/phycon/textures/block/cable/$base_color/*; do
echo $f
convert $f -modulate $2 src/main/resources/assets/phycon/textures/block/cable/$1/$(basename $f)
done
echo "---"
}
# modulate <color> <brightness>,<saturation>,<hue>
# brightness: 100 is initial, +/- 100
# saturnation: 100 is initial, +/- 100
# hue: (<gimp hue> / 180) * 100 + 100
modulate white 200,0,100
modulate orange 100,120,200
modulate magenta 100,100,146
modulate light_blue 140,65,100
modulate yellow 100,109,8
modulate lime 100,100,53
modulate pink 172,50,172
modulate gray 60,10,100
modulate light_gray 105,3,39
modulate cyan 112,65,84
modulate purple 109,76,133
modulate brown 60,45,190
modulate green 60,50,30
modulate red 100,100,176
modulate black 30,0,100

View File

@ -1,19 +1,21 @@
org.gradle.jvmargs=-Xmx1G
org.gradle.daemon=false
minecraft_version=1.16.5
yarn_mappings=1.16.5+build.4
loader_version=0.11.1
minecraft_version=1.18.2
yarn_mappings=1.18.2+build.3
loader_version=0.14.8
mod_version=0.1.0
mod_version=0.2.0
maven_group=net.shadowfacts
archives_base_name=PhysicalConnectivity
fabric_version=0.30.0+1.16
fabric_kotlin_version=1.4.30+build.2
fabric_version=0.56.0+1.18.2
fabric_kotlin_version=1.8.0+kotlin.1.7.0
libblockattributes_version=0.8.5
cotton_resources_version=1.7.4
modmenu_version=1.16.8
libblockattributes_version=0.10.2
# cotton_resources_version=1.7.4
modmenu_version=3.2.2
rei_version=8.2.481
mousewheelie_version=1.9.0+mc1.18.2
junit_version = 5.4.0

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,32 @@
plugins {
id "fabric-loom"
id "org.jetbrains.kotlin.jvm"
}
archivesBaseName = "PhyCon-Plugin-MouseWheelie"
version = project.mod_version
group = project.maven_group
repositories {
maven {
url = "https://maven.siphalor.de/"
}
maven {
url = "https://jitpack.io"
}
mavenCentral()
}
dependencies {
implementation(project(":")) {
transitive = false
}
modImplementation("de.siphalor:mousewheelie-1.18:${project.mousewheelie_version}") {
exclude group: "net.fabricmc"
exclude group: "net.fabricmc.fabric-api"
exclude module: "modmenu"
exclude group: "me.shedaniel.cloth"
}
}

View File

View File

@ -0,0 +1,41 @@
package net.shadowfacts.phycon.plugin.mousewheelie.mixin;
import de.siphalor.mousewheelie.client.util.ScrollAction;
import de.siphalor.mousewheelie.client.util.accessors.IContainerScreen;
import de.siphalor.mousewheelie.client.util.accessors.ISpecialScrollableScreen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.slot.Slot;
import net.minecraft.text.Text;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.spongepowered.asm.mixin.Mixin;
/**
* @author shadowfacts
*/
@Mixin(AbstractTerminalScreen.class)
public abstract class MixinTerminalScreen extends HandledScreen<AbstractTerminalScreenHandler> implements ISpecialScrollableScreen, IContainerScreen {
private MixinTerminalScreen(AbstractTerminalScreenHandler screenHandler, PlayerInventory playerInventory, Text text) {
super(screenHandler, playerInventory, text);
}
@Override
public ScrollAction mouseWheelie_onMouseScrolledSpecial(double mouseX, double mouseY, double scrollAmount) {
Slot slot = mouseWheelie_getSlotAt(mouseX, mouseY);
if (slot == null) {
return ScrollAction.PASS;
}
if (slot.id < handler.getBufferSlotsEnd()) {
// scrolling in the network inventory is never allowed
// scrolling out of the buffer is theoretically possible, but there isn't a straightforward way
// of telling the server not to mark the buffer slot as TO_NETWORK after mouse wheelie's song and dance
return ScrollAction.ABORT;
} else {
return ScrollAction.PASS;
}
}
}

View File

@ -0,0 +1,36 @@
{
"schemaVersion": 1,
"id": "phycon_mousewheelie",
"version": "${version}",
"name": "PhyCon Mouse Wheelie Integration",
"description": "",
"authors": [
"Shadowfacts"
],
"contact": {
"homepage": "https://git.shadowfacts.net/minecraft/PhysicalConnectivity"
},
"license": "LGPL-3.0",
"environment": "client",
"entrypoints": {
},
"mixins": [
{
"config": "phycon-mousewheelie-client.mixins.json",
"environment": "client"
}
],
"depends": {
"fabricloader": ">=0.4.0",
"fabric": "*",
"fabric-language-kotlin": ">=1.3.50",
"phycon": "*",
"mousewheelie": "*"
},
"custom": {
"modmenu:parent": "phycon"
}
}

View File

@ -0,0 +1,11 @@
{
"required": true,
"package": "net.shadowfacts.phycon.plugin.mousewheelie.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"MixinTerminalScreen"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@ -3,7 +3,7 @@ plugins {
id "org.jetbrains.kotlin.jvm"
}
archivesBaseName = project.archives_base_name
archivesBaseName = "PhyCon-Plugin-REI"
version = project.mod_version
group = project.maven_group
@ -11,16 +11,15 @@ repositories {
maven {
url = "https://maven.shedaniel.me/"
}
jcenter()
mavenCentral()
}
dependencies {
implementation project(":")
modCompileOnly("me.shedaniel:RoughlyEnoughItems-api:${project.rei_version}") {
exclude group: "net.fabricmc.fabric-api"
implementation(project(":")) {
transitive = false
}
modRuntime("me.shedaniel:RoughlyEnoughItems:${project.rei_version}") {
modCompileOnly("me.shedaniel:RoughlyEnoughItems-api-fabric:${project.rei_version}") {
exclude group: "net.fabricmc.fabric-api"
}
}

View File

@ -1,3 +0,0 @@
archives_base_name=PhyCon-Plugin-REI
rei_version=5.10.184

View File

@ -1,27 +0,0 @@
package net.shadowfacts.phycon.plugin.rei
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.BaseBoundsHandler
import me.shedaniel.rei.api.DisplayHelper
import me.shedaniel.rei.api.plugins.REIPluginV0
import net.minecraft.client.MinecraftClient
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.block.terminal.TerminalScreen
/**
* @author shadowfacts
*/
object PhyConPlugin: REIPluginV0 {
const val MODID = "phycon_rei"
override fun getPluginIdentifier() = Identifier(MODID, "rei_plugin")
override fun registerBounds(helper: DisplayHelper) {
BaseBoundsHandler.getInstance().registerExclusionZones(TerminalScreen::class.java) {
val screen = MinecraftClient.getInstance().currentScreen as TerminalScreen
listOf(
Rectangle(screen.sortButtonX, screen.sortButtonY, 20, 20)
)
}
}
}

View File

@ -0,0 +1,87 @@
package net.shadowfacts.phycon.plugin.rei
import dev.architectury.event.EventResult
import dev.architectury.event.events.client.ClientScreenInputEvent
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.client.REIRuntime
import me.shedaniel.rei.api.client.plugins.REIClientPlugin
import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry
import net.fabricmc.api.ClientModInitializer
import net.minecraft.client.MinecraftClient
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen
import org.apache.logging.log4j.LogManager
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
/**
* @author shadowfacts
*/
object PhyConPluginClient: ClientModInitializer, REIClientPlugin, AbstractTerminalScreen.SearchQueryListener {
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()
}
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 terminalSearchQueryChanged(newValue: String) {
if (shouldSync()) {
REIRuntime.getInstance().searchTextField?.text = newValue
}
}
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
}
}
}
}
}

View File

@ -0,0 +1,68 @@
package net.shadowfacts.phycon.plugin.rei
import me.shedaniel.rei.api.common.category.CategoryIdentifier
import me.shedaniel.rei.api.common.display.SimpleGridMenuDisplay
import me.shedaniel.rei.api.common.plugins.REIServerPlugin
import me.shedaniel.rei.api.common.transfer.info.MenuInfoContext
import me.shedaniel.rei.api.common.transfer.info.MenuInfoRegistry
import me.shedaniel.rei.api.common.transfer.info.simple.SimpleGridMenuInfo
import me.shedaniel.rei.api.common.transfer.info.simple.SimpleMenuInfoProvider
import me.shedaniel.rei.api.common.transfer.info.stack.SlotAccessor
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.api.PhyConAPI
import net.shadowfacts.phycon.api.PhyConPlugin
import net.shadowfacts.phycon.api.TerminalSettingKey
import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreenHandler
import java.util.stream.IntStream
/**
* @author shadowfacts
*/
object PhyConPluginCommon: REIServerPlugin, PhyConPlugin {
const val MODID = "phycon_rei"
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 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> {
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 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 getCraftingHeight(menu: CraftingTerminalScreenHandler): Int {
return 3
}
override fun getDisplay(): D {
return display
}
}
}

View File

@ -0,0 +1,29 @@
package net.shadowfacts.phycon.plugin.rei
import net.minecraft.text.LiteralText
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.TerminalSetting
/**
* @author shadowfacts
*/
enum class REISyncMode: TerminalSetting {
OFF,
ON,
HIGHLIGHT_ONLY;
override fun getIconTexture() = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png")
override fun getUV() = when (this) {
OFF -> intArrayOf(0, 240)
ON -> intArrayOf(16, 240)
HIGHLIGHT_ONLY -> intArrayOf(32, 240)
}
override fun getTooltip() = when (this) {
OFF -> LiteralText("Don't sync with REI")
ON -> LiteralText("Sync with REI")
HIGHLIGHT_ONLY -> LiteralText("Sync in highlight mode")
}
}

View File

@ -12,12 +12,27 @@
"homepage": "https://git.shadowfacts.net/minecraft/PhysicalConnectivity"
},
"license": "LGPL-3.0",
"environment": "client",
"entrypoints": {
"rei_plugins": [
"client": [
{
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.plugin.rei.PhyConPlugin"
"value": "net.shadowfacts.phycon.plugin.rei.PhyConPluginClient"
}
],
"rei": [
{
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.plugin.rei.PhyConPluginClient"
},
{
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.plugin.rei.PhyConPluginCommon"
}
],
"phycon": [
{
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.plugin.rei.PhyConPluginCommon"
}
]
},
@ -28,7 +43,7 @@
"fabric": "*",
"fabric-language-kotlin": ">=1.3.50",
"phycon": "*",
"roughlyenoughitems": "*"
"roughlyenoughitems": ["^7.0.0", "^8.0.0"]
},
"custom": {

View File

@ -0,0 +1,26 @@
plugins {
id "fabric-loom"
id "org.jetbrains.kotlin.jvm"
}
archivesBaseName = project.archives_base_name
version = project.mod_version
group = project.maven_group
repositories {
maven {
url = "https://maven.modmuss50.me/"
}
mavenCentral()
}
dependencies {
implementation(project(":")) {
transitive = false
}
modImplementation("TechReborn:TechReborn-1.16:${project.techreborn_version}") {
exclude group: "net.fabricmc.fabric-api"
}
}

View File

@ -0,0 +1,4 @@
archives_base_name=PhyCon-Plugin-TechReborn
techreborn_version=3.8.2+build.222

View File

@ -0,0 +1,31 @@
package net.shadowfacts.phycon.plugin.techreborn
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeSourceType
import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.ItemAttributes
import net.fabricmc.api.ModInitializer
import net.minecraft.block.BlockState
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import techreborn.blockentity.storage.item.StorageUnitBaseBlockEntity
import techreborn.init.TRContent
/**
* @author shadowfacts
*/
object PhyConTR: ModInitializer {
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))
}
}
}

View File

@ -0,0 +1,81 @@
package net.shadowfacts.phycon.plugin.techreborn
import alexiil.mc.lib.attributes.Simulation
import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import alexiil.mc.lib.attributes.item.filter.ItemFilter
import net.minecraft.item.ItemStack
import net.shadowfacts.phycon.util.copyWithCount
import techreborn.blockentity.storage.item.StorageUnitBaseBlockEntity
import kotlin.math.min
/**
* @author shadowfacts
*/
class StorageUnitWrapper(
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 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 attemptInsertion(filter: ItemStack, simulation: Simulation): ItemStack {
if (simulation.isAction) {
return be.processInput(filter)
}
if (be.storedStack.isEmpty) {
return ItemStack.EMPTY
}
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)
}
}
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())
if (simulation.isAction) {
be.storedStack.decrement(extracted)
}
return be.storedStack.copyWithCount(extracted)
}
}

View File

@ -0,0 +1,37 @@
{
"schemaVersion": 1,
"id": "phycon_techreborn",
"version": "${version}",
"name": "PhyCon TechReborn Integration",
"description": "",
"authors": [
"Shadowfacts"
],
"contact": {
"homepage": "https://git.shadowfacts.net/minecraft/PhysicalConnectivity"
},
"license": "LGPL-3.0",
"entrypoints": {
"main": [
{
"adapter": "kotlin",
"value": "net.shadowfacts.phycon.plugin.techreborn.PhyConTR"
}
]
},
"mixins": [
],
"depends": {
"fabricloader": ">=0.4.0",
"fabric": "*",
"fabric-language-kotlin": ">=1.3.50",
"phycon": "*",
"techreborn": "*"
},
"custom": {
"modmenu:parent": "phycon"
}
}

View File

@ -1,6 +1,6 @@
pluginManagement {
repositories {
jcenter()
mavenCentral()
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
@ -10,4 +10,6 @@ pluginManagement {
}
include("kiwi-java")
include("plugin:mousewheelie")
include("plugin:rei")
// include("plugin:techreborn")

View File

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

View File

@ -0,0 +1,14 @@
package net.shadowfacts.phycon.api;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
/**
* @author shadowfacts
*/
public interface PhyConAPI {
@NotNull
<E extends Enum<E> & TerminalSetting> TerminalSettingKey<E> registerTerminalSetting(Identifier id, E defaultValue);
}

View File

@ -0,0 +1,12 @@
package net.shadowfacts.phycon.api;
import org.jetbrains.annotations.NotNull;
/**
* @author shadowfacts
*/
public interface PhyConPlugin {
void initializePhyCon(@NotNull PhyConAPI api);
}

View File

@ -0,0 +1,18 @@
package net.shadowfacts.phycon.api;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
/**
* @author shadowfacts
*/
public interface TerminalSetting {
Identifier getIconTexture();
int[] getUV();
@Nullable Text getTooltip();
}

View File

@ -0,0 +1,16 @@
package net.shadowfacts.phycon.api;
import net.minecraft.util.Identifier;
/**
* @author shadowfacts
*/
public interface TerminalSettingKey<E extends Enum<E> & TerminalSetting> {
Identifier getID();
E getValue();
void setPriority(int priority);
}

View File

@ -1,12 +1,18 @@
package net.shadowfacts.phycon.mixin.client;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.client.render.item.ItemRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.shadowfacts.phycon.block.terminal.TerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreen;
import net.shadowfacts.phycon.block.terminal.AbstractTerminalScreenHandler;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
@ -15,12 +21,29 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(HandledScreen.class)
public class MixinHandledScreen {
@Inject(method = "drawSlot(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/screen/slot/Slot;)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableDepthTest()V"))
private void drawSlot(MatrixStack matrixStack, Slot slot, CallbackInfo ci) {
if ((Object)this instanceof TerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this;
@Inject(
method = "drawSlot(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/screen/slot/Slot;)V",
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) {
self.drawSlotUnderlay(matrixStack, slot);
}
}
@Redirect(
method = "drawSlot(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/screen/slot/Slot;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/ItemRenderer;renderGuiItemOverlay(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V")
)
private void drawSlotAmount(ItemRenderer itemRenderer, TextRenderer textRenderer, ItemStack stack, int x, int y, @Nullable String countLabel, MatrixStack matrixStack, Slot slot) {
if ((Object)this instanceof AbstractTerminalScreen<?, ?> self) {
AbstractTerminalScreenHandler<?> handler = self.getScreenHandler();
if (slot.id < handler.getNetworkSlotsEnd() && stack.getCount() > 1) {
self.drawNetworkSlotAmount(stack, x, y);
return;
}
}
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, countLabel);
}
}

View File

@ -0,0 +1,28 @@
package net.shadowfacts.phycon.mixin.client;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.shadowfacts.cacao.AbstractCacaoScreen;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* @author shadowfacts
*/
@Mixin(MinecraftClient.class)
public class MixinMinecraftClient {
@Inject(
method = "setScreen(Lnet/minecraft/client/gui/screen/Screen;)V",
at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;currentScreen:Lnet/minecraft/client/gui/screen/Screen;", opcode = Opcodes.PUTFIELD, shift = At.Shift.AFTER)
)
private void setScreen(Screen screen, CallbackInfo ci) {
if (screen instanceof AbstractCacaoScreen cacaoScreen) {
cacaoScreen.screenWillAppear();
}
}
}

View File

@ -14,4 +14,6 @@ interface AbstractCacaoScreen {
fun removeWindow(window: Window)
}
fun screenWillAppear()
}

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
@ -12,6 +13,8 @@ import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import org.lwjgl.glfw.GLFW
import org.lwjgl.opengl.GL11
import java.util.*
/**
@ -27,10 +30,16 @@ open class CacaoHandledScreen<Handler: ScreenHandler>(
override val windows: List<Window> = _windows
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()
}
_windows.add(index, window)
@ -47,6 +56,15 @@ open class CacaoHandledScreen<Handler: ScreenHandler>(
override fun removeWindow(window: Window) {
_windows.remove(window)
if (windows.isEmpty()) {
close()
}
}
override fun screenWillAppear() {
windows.forEach {
it.viewController.viewWillAppear()
}
}
override fun init() {
@ -57,8 +75,18 @@ open class CacaoHandledScreen<Handler: ScreenHandler>(
}
}
override fun close() {
super.close()
windows.forEach {
it.viewController.viewWillDisappear()
it.viewController.viewDidDisappear()
it.firstResponder = null
}
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
renderBackground(matrixStack)
}
override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
@ -67,15 +95,28 @@ open class CacaoHandledScreen<Handler: ScreenHandler>(
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val mouse = Point(mouseX, mouseY)
windows.forEachIndexed { index, it ->
matrixStack.push()
matrixStack.translate(0.0, 0.0, -350.0)
for (i in windows.indices) {
val it = windows[i]
if (i == windows.size - 1) {
renderBackground(matrixStack)
}
if (it is ScreenHandlerWindow) {
if (index == windows.size - 1) {
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()
}
it.draw(matrixStack, mouse, delta)
}
@ -95,12 +136,49 @@ open class CacaoHandledScreen<Handler: ScreenHandler>(
}
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
return 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 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 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)
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun charTyped(char: Char, modifiers: Int): Boolean {
@ -111,4 +189,8 @@ open class CacaoHandledScreen<Handler: ScreenHandler>(
return super.charTyped(char, modifiers)
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
}

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.sound.SoundEvents
@ -10,6 +11,7 @@ import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.window.Window
import org.lwjgl.glfw.GLFW
import java.util.*
/**
@ -31,6 +33,8 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
*/
override val windows: List<Window> = _windows
private var hasAppeared = false
/**
* Adds the given window to this screen's window list at the given position.
*
@ -39,6 +43,10 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
* @return The window that was added, as a convenience.
*/
override fun <T: Window> addWindow(window: T, index: Int): T {
if (hasAppeared) {
window.viewController.viewWillAppear()
}
_windows.add(index, window)
window.screen = this
@ -60,7 +68,15 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
*/
override fun removeWindow(window: Window) {
_windows.remove(window)
// todo: VC callbacks
if (windows.isEmpty()) {
close()
}
}
override fun screenWillAppear() {
windows.forEach {
it.viewController.viewWillAppear()
}
}
override fun init() {
@ -71,11 +87,12 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
}
}
override fun onClose() {
super.onClose()
override fun close() {
super.close()
windows.forEach {
// todo: VC callbacks
it.viewController.viewWillDisappear()
it.viewController.viewDidDisappear()
// resign the current first responder (if any)
it.firstResponder = null
@ -119,12 +136,23 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
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 {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
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)
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun keyReleased(i: Int, j: Int, k: Int): Boolean {
@ -139,6 +167,10 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
return super.charTyped(char, modifiers)
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
}
fun AbstractCacaoScreen.findResponder(fn: (Responder) -> Boolean): Boolean {

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.util.LayoutGuide
import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Variable
@ -9,10 +10,23 @@ import no.birkett.kiwi.Variable
*
* @author shadowfacts
*/
class LayoutVariable(val owner: View, val property: String): Variable("LayoutVariable") {
class LayoutVariable(
val view: View?,
val layoutGuide: LayoutGuide?,
val property: String,
): Variable("LayoutVariable") {
override fun getName() = "$owner.$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")
}
}
override fun getName() = "${view ?: layoutGuide}.$property"
override fun toString() = "LayoutVariable(name=$name, value=$value)"
}
}

View File

@ -0,0 +1,33 @@
package net.shadowfacts.cacao.util
import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.view.View
/**
* A layout guide is a non-view object that represents a rectangular area for the purposes of constraint-based layout.
* It allows you to define complex layouts without needing empty container views.
*
* A layout guide is always owned by a view. The owning view's dimensions are not necessarily tied to the layout guide's.
*
* To create a layout guide, call [View.addLayoutGuide] on the owning view.
*
* @author shadowfacts
*/
class LayoutGuide(
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 frame: Rect
get() = Rect(leftAnchor.value - owningView.leftAnchor.value, topAnchor.value - owningView.topAnchor.value, widthAnchor.value, heightAnchor.value)
}

View File

@ -1,20 +1,21 @@
package net.shadowfacts.cacao.util
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.render.*
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.sound.SoundEvent
import net.minecraft.text.LiteralText
import net.minecraft.text.OrderedText
import net.minecraft.text.Text
import net.minecraft.util.math.Matrix4f
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.texture.Texture
import org.lwjgl.opengl.GL11
import kotlin.math.roundToInt
/**
* Helper methods for rendering using Minecraft's utilities from Cacao views.
@ -45,18 +46,18 @@ object RenderHelper: DrawableHelper() {
*/
fun draw(matrixStack: MatrixStack, rect: Rect, texture: Texture) {
if (disabled) return
color(1f, 1f, 1f, 1f)
MinecraftClient.getInstance().textureManager.bindTexture(texture.location)
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
GlStateManager.lineWidth(width)
RenderSystem.lineWidth(width)
val tessellator = Tessellator.getInstance()
val buffer = tessellator.buffer
buffer.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR)
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()
@ -71,19 +72,19 @@ object RenderHelper: DrawableHelper() {
val uEnd = (u + width).toFloat() / textureWidth
val vStart = v.toFloat() / textureHeight
val vEnd = (v + height).toFloat() / textureHeight
drawTexturedQuad(matrixStack.peek().model, x, x + width, y, y + height, 0.0, uStart, uEnd, vStart, vEnd)
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(GL11.GL_QUADS, VertexFormats.POSITION_TEXTURE)
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()
RenderSystem.enableAlphaTest()
BufferRenderer.draw(bufferBuilder)
}
@ -95,110 +96,22 @@ object RenderHelper: DrawableHelper() {
drawTooltip(matrixStack, texts.map(Text::asOrderedText), mouse)
}
// Based on Screen.renderOrderedTooltip
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
val client = MinecraftClient.getInstance()
val textRenderer = client.textRenderer
val maxWidth = texts.maxOf(textRenderer::getWidth)
var x = mouse.x.toInt() + 12
var y = mouse.y.toInt() - 12
var p = 8
if (texts.size > 1) {
p += 2 + (texts.size - 1) * 8
}
if (x + maxWidth > client.window.scaledWidth) {
x -= 28 + maxWidth
}
if (y + p + 6 > client.window.scaledHeight) {
y = client.window.scaledHeight - p - 6
}
matrixStack.push()
val q = -267386864
val r = 1347420415
val s = 1344798847
val t = 1
val tessellator = Tessellator.getInstance()
val buffer = tessellator.buffer
buffer.begin(GL11.GL_QUADS, VertexFormats.POSITION_COLOR)
val matrix = matrixStack.peek().model
val z = 400
fillGradient(matrix, buffer, x - 3, y - 4, x + maxWidth + 3, y - 3, z, q, q)
fillGradient(matrix, buffer, x - 3, y + p + 3, x + maxWidth + 3, y + p + 4, z, q, q)
fillGradient(matrix, buffer, x - 3, y - 3, x + maxWidth + 3, y + p + 3, z, q, q)
fillGradient(matrix, buffer, x - 4, y - 3, x - 3, y + p + 3, z, q, q)
fillGradient(matrix, buffer, x + maxWidth + 3, y - 3, x + maxWidth + 4, y + p + 3, z, q, q)
fillGradient(matrix, buffer, x - 3, y - 3 + 1, x - 3 + 1, y + p + 3 - 1, z, r, s)
fillGradient(matrix, buffer, x + maxWidth + 2, y - 3 + 1, x + maxWidth + 3, y + p + 3 - 1, z, r, s)
fillGradient(matrix, buffer, x - 3, y - 3, x + maxWidth + 3, y - 3 + 1, z, r, r)
fillGradient(matrix, buffer, x - 3, y + p + 2, x + maxWidth + 3, y + p + 3, z, s, s)
RenderSystem.enableDepthTest()
RenderSystem.disableTexture()
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.shadeModel(7425)
buffer.end()
BufferRenderer.draw(buffer)
RenderSystem.shadeModel(7424)
RenderSystem.disableBlend()
RenderSystem.enableTexture()
val immediate = VertexConsumerProvider.immediate(buffer)
matrixStack.translate(0.0, 0.0, 400.0)
for (i in texts.indices) {
val text = texts[i]
textRenderer.draw(text, x.toFloat(), y.toFloat(), -1, true, matrix, immediate, false, 0, 15728880)
if (i == 0) {
y += 2
}
y += 10
}
immediate.draw()
matrixStack.pop()
}
/**
* @see org.lwjgl.opengl.GL11.glPushMatrix
*/
fun pushMatrix() {
if (disabled) return
RenderSystem.pushMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glPopMatrix
*/
fun popMatrix() {
if (disabled) return
RenderSystem.popMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glTranslated
*/
fun translate(x: Double, y: Double, z: Double = 0.0) {
if (disabled) return
RenderSystem.translated(x, y, z)
}
/**
* @see org.lwjgl.opengl.GL11.glScaled
*/
fun scale(x: Double, y: Double, z: Double = 1.0) {
if (disabled) return
RenderSystem.scaled(x, y, z)
dummyScreen.width = client.window.scaledWidth
dummyScreen.height = client.window.scaledHeight
dummyScreen.renderOrderedTooltip(matrixStack, texts, mouse.x.roundToInt(), mouse.y.roundToInt())
}
/**
@ -206,7 +119,7 @@ object RenderHelper: DrawableHelper() {
*/
fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return
RenderSystem.color4f(r, g, b, alpha)
RenderSystem.setShaderColor(r, g, b, alpha)
}
private fun VertexConsumer.color(color: Color): VertexConsumer {

View File

@ -26,11 +26,13 @@ class BezierCurveView(val curve: BezierCurve): View() {
var lineColor = Color.BLACK
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.scale(bounds.width, bounds.height)
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

@ -29,7 +29,7 @@ class DialogView(
CANCEL, CONFIRM, OK, CLOSE;
override val localizedName: Text
get() = LiteralText(name.toLowerCase().capitalize()) // todo: actually localize me
get() = LiteralText(name.lowercase().replaceFirstChar(Char::titlecase)) // todo: actually localize me
}
private lateinit var background: NinePatchView

View File

@ -6,6 +6,7 @@ import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.properties.ResettableLazyProperty
import kotlin.math.roundToInt
/**
* A helper class for drawing a [NinePatchTexture] in a view.
@ -91,22 +92,22 @@ open class NinePatchView(val ninePatch: NinePatchTexture): View() {
private fun drawEdges(matrixStack: MatrixStack) {
// Horizontal
for (i in 0 until (topMiddle.width.toInt() / ninePatch.centerWidth)) {
for (i in 0 until (topMiddle.width.roundToInt() / ninePatch.centerWidth)) {
RenderHelper.draw(matrixStack, topMiddle.left + i * ninePatch.centerWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, ninePatch.centerWidth.toDouble(), topMiddle.height, ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(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.toInt() % ninePatch.centerWidth
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.toInt() / ninePatch.centerHeight)) {
for (i in 0 until (leftMiddle.height.roundToInt() / ninePatch.centerHeight)) {
RenderHelper.draw(matrixStack, leftMiddle.left, leftMiddle.top + i * ninePatch.centerHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(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.toInt() % ninePatch.centerHeight
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)
@ -114,20 +115,20 @@ open class NinePatchView(val ninePatch: NinePatchTexture): View() {
}
private fun drawCenter(matrixStack: MatrixStack) {
for (i in 0 until (center.height.toInt() / ninePatch.centerHeight)) {
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.toInt() % ninePatch.centerHeight
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.toInt() / ninePatch.centerWidth)) {
for (i in 0 until (center.width.roundToInt() / ninePatch.centerWidth)) {
RenderHelper.draw(matrixStack, center.left + i * ninePatch.centerWidth, y, ninePatch.center.u, ninePatch.center.v, ninePatch.centerWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height)
}
val remWidth = center.width.toInt() % ninePatch.centerWidth
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

@ -11,10 +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) {
RenderHelper.draw(matrixStack, bounds, texture)
texture?.also {
RenderHelper.draw(matrixStack, bounds, it)
}
}
}

View File

@ -12,7 +12,7 @@ import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import java.lang.RuntimeException
import java.util.*
import kotlin.collections.HashSet
import kotlin.math.floor
/**
* The base Cacao View class. Provides layout anchors, properties, and helper methods.
@ -84,6 +84,17 @@ open class View(): Responder {
*/
val centerYAnchor = LayoutVariable(this, "centerY")
private val _layoutGuides = LinkedList<LayoutGuide>()
/**
* All the layout guides attached to this view.
*
* To add a layout guide, call [addLayoutGuide].
*
* @see LayoutGuide
*/
val layoutGuides: List<LayoutGuide> = _layoutGuides
/**
* 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.
@ -229,7 +240,7 @@ open class View(): Responder {
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].owner, variables[b].owner, View::superview)
val ancestor = LowestCommonAncestor.find(variables[a].viewOrLayoutGuideView, variables[b].viewOrLayoutGuideView, View::superview)
if (ancestor == null) {
return@filter true
}
@ -247,6 +258,18 @@ open class View(): Responder {
// 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
}
/**
* Removes this view from its superview, if it has one.
*/
@ -283,6 +306,10 @@ open class View(): Responder {
open fun wasAdded() {
createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
layoutGuides.forEach {
it.attachTo(solver)
}
}
/**
@ -340,8 +367,8 @@ open class View(): Responder {
* @param delta The time since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top)
matrixStack.push()
matrixStack.translate(frame.left, frame.top, 0.0)
RenderHelper.fill(matrixStack, bounds, backgroundColor)
@ -352,7 +379,7 @@ open class View(): Responder {
it.draw(matrixStack, mouseInView, delta)
}
RenderHelper.popMatrix()
matrixStack.pop()
}
/**
@ -419,6 +446,23 @@ open class View(): Responder {
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 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.
*
@ -463,3 +507,15 @@ open class View(): Responder {
}
}
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)
}
}
private val LayoutVariable.viewOrLayoutGuideView: View
get() = view ?: layoutGuide!!.owningView

View File

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

View File

@ -41,7 +41,9 @@ class EnumButton<E: Enum<E>>(
value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value)
else -> value
else -> {
return false
}
}
}

View File

@ -5,12 +5,11 @@ import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.LiteralText
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.View
import net.shadowfacts.phycon.mixin.client.TextFieldWidgetAccessor
import org.lwjgl.glfw.GLFW
/**
* An abstract text field class. Cannot be constructed directly, use for creating other text fields with more specific
@ -68,7 +67,7 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
var drawBackground = true
set(value) {
field = value
minecraftWidget.setHasBorder(value)
minecraftWidget.setDrawsBackground(value)
}
private lateinit var originInWindow: Point
@ -77,7 +76,7 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setHasBorder(drawBackground)
minecraftWidget.setDrawsBackground(drawBackground)
}
/**
@ -101,14 +100,14 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(-originInWindow.x, -originInWindow.y)
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)
RenderHelper.popMatrix()
matrixStack.pop()
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
@ -133,12 +132,12 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
override fun didBecomeFirstResponder() {
super.didBecomeFirstResponder()
minecraftWidget.setSelected(true)
minecraftWidget.setTextFieldFocused(true)
}
override fun didResignFirstResponder() {
super.didResignFirstResponder()
minecraftWidget.setSelected(false)
minecraftWidget.setTextFieldFocused(false)
}
override fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
@ -159,7 +158,11 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result
return result || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE)
}
fun tick() {
minecraftWidget.tick()
}
// todo: label for the TextFieldWidget?

View File

@ -3,7 +3,7 @@ package net.shadowfacts.cacao.view.textfield
/**
* @author shadowfacts
*/
class NumberField(
open class NumberField(
initialValue: Int,
handler: ((NumberField) -> Unit)? = null,
): AbstractTextField<NumberField>(initialValue.toString()) {

View File

@ -7,7 +7,7 @@ package net.shadowfacts.cacao.view.textfield
* @param initialText The initial value of this text field.
* @param handler A function that is invoked when the value of the text field changes.
*/
class TextField(
open class TextField(
initialText: String,
handler: ((TextField) -> Unit)? = null
): AbstractTextField<TextField>(initialText) {

View File

@ -147,11 +147,6 @@ class TabViewController<T: TabViewController.Tab>(
currentTabController.viewWillAppear()
}
override fun viewDidAppear() {
super.viewDidAppear()
currentTabController.viewDidAppear()
}
override fun viewWillDisappear() {
super.viewWillDisappear()
currentTabController.viewWillDisappear()
@ -225,7 +220,6 @@ class TabViewController<T: TabViewController.Tab>(
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
currentTabController.viewWillAppear()
currentTabController.viewDidAppear()
onTabChange?.invoke(currentTab)

View File

@ -122,12 +122,12 @@ abstract class ViewController {
children.forEach(ViewController::viewWillAppear)
}
/**
* Called immediately after the VC's view has first been displayed on screen.
*/
open fun viewDidAppear() {
children.forEach(ViewController::viewDidAppear)
}
// /**
// * Called immediately after the VC's view has first been displayed on screen.
// */
// open fun viewDidAppear() {
// children.forEach(ViewController::viewDidAppear)
// }
/**
* Called before the view will disappear from the screen, either because the VC has been removed from it's parent/screen

View File

@ -234,7 +234,8 @@ open class Window(
fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
return currentlyDraggedView.mouseDragged(startPoint, delta, mouseButton)
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)
@ -246,7 +247,12 @@ open class Window(
view = view.subviewsAtPoint(pointInView).maxByOrNull(View::zIndex)
}
this.currentDragReceiver = view ?: prevView
return this.currentDragReceiver?.mouseDragged(startPoint, delta, mouseButton) ?: false
return if (this.currentDragReceiver != null) {
val pointInView = viewController.view.convert(startPoint, to = this.currentDragReceiver!!)
this.currentDragReceiver!!.mouseDragged(pointInView, delta, mouseButton)
} else {
false
}
}
return false
}
@ -254,10 +260,16 @@ open class Window(
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

@ -100,4 +100,4 @@ class KiwiContext(val solver: Solver) {
fun Solver.dsl(init: KiwiContext.() -> Unit): Solver {
KiwiContext(this).init()
return this
}
}

View File

@ -0,0 +1,22 @@
package net.shadowfacts.phycon
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.api.PhyConAPI
import net.shadowfacts.phycon.api.PhyConPlugin
import net.shadowfacts.phycon.api.TerminalSettingKey
import net.shadowfacts.phycon.util.SortMode
/**
* @author shadowfacts
*/
object DefaultPlugin: PhyConPlugin {
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)
}
}

View File

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

View File

@ -2,6 +2,10 @@ package net.shadowfacts.phycon
import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage
import net.fabricmc.loader.api.FabricLoader
import net.shadowfacts.phycon.api.PhyConPlugin
import net.shadowfacts.phycon.block.p2p.P2PReceiverBlockEntity
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyItems
@ -24,9 +28,16 @@ object PhysicalConnectivity: ModInitializer {
PhyItems.init()
PhyScreens.init()
registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
registerGlobalReceiver(C2SConfigureDevice)
ItemStorage.SIDED.registerForBlockEntity(P2PReceiverBlockEntity::provideItemStorage, PhyBlockEntities.P2P_RECEIVER)
for (it in FabricLoader.getInstance().getEntrypoints("phycon", PhyConPlugin::class.java)) {
it.initializePhyCon(PhyConAPIImpl)
}
}
private fun registerGlobalReceiver(receiver: ServerReceiver) {

View File

@ -4,23 +4,44 @@ import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
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.redstone_emitter.RedstoneEmitterScreen
import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreen
import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.block.terminal.TerminalScreen
import net.shadowfacts.phycon.client.PhyExtendedModelProvider
import net.shadowfacts.phycon.client.PhyModelProvider
import net.shadowfacts.phycon.networking.ClientReceiver
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.TerminalSettings
/**
* @author shadowfacts
*/
object PhysicalConnectivityClient: ClientModInitializer {
val terminalSettings = TerminalSettings()
var screenMaterial: RenderMaterial? = null
private set
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()
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)

View File

@ -2,15 +2,20 @@ package net.shadowfacts.phycon.block
import net.minecraft.block.Block
import net.minecraft.block.BlockEntityProvider
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntity
import net.minecraft.block.entity.BlockEntityTicker
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.util.math.BlockPos
import net.minecraft.world.BlockView
import net.minecraft.world.World
/**
* @author shadowfacts
*/
abstract class BlockWithEntity<T: BlockEntity>(settings: Settings): Block(settings), BlockEntityProvider {
abstract override fun createBlockEntity(world: BlockView): T?
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) {
@ -19,4 +24,4 @@ abstract class BlockWithEntity<T: BlockEntity>(settings: Settings): Block(settin
null
}
}
}
}

View File

@ -1,6 +1,9 @@
package net.shadowfacts.phycon.block
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntity
import net.minecraft.block.entity.BlockEntityTicker
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
@ -25,4 +28,14 @@ abstract class DeviceBlock<T: DeviceBlockEntity>(settings: Settings): BlockWithE
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()
}
}
}
}

View File

@ -1,34 +1,34 @@
package net.shadowfacts.phycon.block
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntity
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.nbt.CompoundTag
import net.minecraft.util.Tickable
import net.minecraft.nbt.NbtCompound
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket
import net.minecraft.util.math.BlockPos
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.PacketSink
import net.shadowfacts.phycon.api.PacketSource
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.api.PacketSink
import net.shadowfacts.phycon.api.PacketSource
import net.shadowfacts.phycon.api.frame.EthernetFrame
import net.shadowfacts.phycon.api.frame.PacketFrame
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.api.util.MACAddress
import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.frame.ARPQueryFrame
import net.shadowfacts.phycon.frame.ARPResponseFrame
import net.shadowfacts.phycon.frame.BasePacketFrame
import net.shadowfacts.phycon.frame.NetworkSplitFrame
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.NetworkUtil
import java.lang.ref.WeakReference
import java.util.*
/**
* @author shadowfacts
*/
abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
BlockEntityClientSerializable,
Tickable,
abstract class DeviceBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): BlockEntity(type, pos, state),
PacketSink,
PacketSource,
Interface {
@ -45,6 +45,7 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
private val arpTable = mutableMapOf<IPAddress, MACAddress>()
private val packetQueue = LinkedList<PendingPacket>()
private var cachedDestination: WeakReference<Interface>? = null
var counter: Long = 0
@ -59,6 +60,9 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
is DeviceRemovedPacket -> {
arpTable.remove(packet.source)
}
is PingPacket -> {
sendPacket(PongPacket(ipAddress, packet.source))
}
}
handle(packet)
}
@ -72,6 +76,7 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
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)
@ -104,6 +109,11 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
packetQueue.removeAll(toRemove)
}
protected open fun handleNetworkSplit() {
arpTable.clear()
cachedDestination = null
}
override fun sendPacket(packet: Packet) {
if (packet.destination.isBroadcast) {
send(BasePacketFrame(packet, macAddress, MACAddress.BROADCAST))
@ -118,15 +128,29 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
}
open fun findDestination(): Interface? {
val cachedDestination = this.cachedDestination?.get()
if (cachedDestination != null) {
return cachedDestination
}
val sides = (cachedState.block as NetworkComponentBlock).getNetworkConnectedSides(cachedState, world!!, pos)
return when (sides.size) {
0 -> null
1 -> NetworkUtil.findConnectedInterface(world!!, pos, sides.first())
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")
}
}
override fun tick() {
override fun cableDisconnected() {
cachedDestination = null
handleNetworkSplit()
}
open fun tick() {
counter++
if (!world!!.isClient) {
@ -148,33 +172,50 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
}
}
protected open fun toCommonTag(tag: CompoundTag) {
protected open fun toCommonTag(tag: NbtCompound) {
tag.putInt("IPAddress", ipAddress.address)
tag.putLong("MACAddress", macAddress.address)
}
protected open fun fromCommonTag(tag: CompoundTag) {
protected open fun fromCommonTag(tag: NbtCompound) {
ipAddress = IPAddress(tag.getInt("IPAddress"))
macAddress = MACAddress(tag.getLong("MACAddress"))
}
override fun toTag(tag: CompoundTag): CompoundTag {
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
toCommonTag(tag)
return super.toTag(tag)
}
override fun fromTag(state: BlockState, tag: CompoundTag) {
super.fromTag(state, tag)
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
fromCommonTag(tag)
if (tag.getBoolean("_SyncPacket")) {
fromClientTag(tag)
}
}
override fun toClientTag(tag: CompoundTag): CompoundTag {
override fun toUpdatePacket(): BlockEntityUpdateS2CPacket {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putBoolean("_SyncPacket", true)
return toClientTag(tag)
}
open fun toClientTag(tag: NbtCompound): NbtCompound {
toCommonTag(tag)
return tag
}
override fun fromClientTag(tag: CompoundTag) {
fromCommonTag(tag)
open fun fromClientTag(tag: NbtCompound) {
}
fun markUpdate() {
markDirty()
world!!.updateListeners(pos, cachedState, cachedState, 3)
}
fun onBreak() {

View File

@ -4,20 +4,23 @@ import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.ShapeContext
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.server.world.ServerWorld
import net.minecraft.state.StateManager
import net.minecraft.state.property.EnumProperty
import net.minecraft.state.property.Properties
import net.minecraft.util.DyeColor
import net.minecraft.util.StringIdentifiable
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.cable.CableBlock
import net.shadowfacts.phycon.init.PhyItems
import java.util.*
@ -28,6 +31,7 @@ abstract class FaceDeviceBlock<T: DeviceBlockEntity>(settings: Settings): Device
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 {
@ -100,44 +104,56 @@ abstract class FaceDeviceBlock<T: DeviceBlockEntity>(settings: Settings): Device
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
val cableConnection = FaceCableConnection.from(getCableConnectedSide(context.world, context.blockPos, facing))
// 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): Direction? {
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)
val block = state.block
if (block is NetworkComponentBlock && block.getNetworkConnectedSides(state, world, offsetPos).contains(side.opposite)) {
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)
}
}
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 (neighborState.block is NetworkComponentBlock) {
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]))
newConnection = FaceCableConnection.from(getCableConnectedSide(world, pos, state[FACING], state[COLOR]))
}
}
return state.with(CABLE_CONNECTION, newConnection)
@ -146,4 +162,11 @@ abstract class FaceDeviceBlock<T: DeviceBlockEntity>(settings: Settings): Device
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)
val cableStack = ItemStack(PhyItems.CABLES[state[COLOR]])
dropStack(world, pos, cableStack)
}
}

View File

@ -2,12 +2,14 @@ package net.shadowfacts.phycon.block.cable
import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings
import net.minecraft.block.*
import net.minecraft.block.piston.PistonBehavior
import net.minecraft.block.entity.BlockEntity
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.state.StateManager
import net.minecraft.state.property.EnumProperty
import net.minecraft.util.ActionResult
import net.minecraft.util.DyeColor
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
import net.minecraft.util.hit.BlockHitResult
@ -23,24 +25,27 @@ import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.NetworkCableBlock
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.init.PhyItems
import net.shadowfacts.phycon.item.FaceDeviceBlockItem
import net.shadowfacts.phycon.util.CableConnection
import net.shadowfacts.phycon.util.NetworkUtil
import net.shadowfacts.phycon.util.containsInclusive
import java.util.*
/**
* @author shadowfacts
*/
class CableBlock: Block(
class CableBlock(
val color: DyeColor,
): Block(
FabricBlockSettings.of(CABLE_MATERIAL)
.strength(0.3f)
.nonOpaque()
.breakByHand(true)
), NetworkCableBlock {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "cable")
val CABLE_MATERIAL = Material.Builder(MaterialColor.BLUE).build()
val CABLE_MATERIAL = Material.Builder(MapColor.BLUE).build()
val CENTER_SHAPE = createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 10.0)
val SIDE_SHAPES = mapOf<Direction, VoxelShape>(
Direction.DOWN to createCuboidShape(6.0, 0.0, 6.0, 10.0, 6.0, 10.0),
@ -50,19 +55,25 @@ class CableBlock: Block(
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 = mutableMapOf<BlockState, VoxelShape>()
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 {
return SHAPE_CACHE.getOrPut(state) {
var shape = CENTER_SHAPE
for ((side, prop) in CONNECTIONS) {
if (state[prop] == CableConnection.ON) {
shape = VoxelShapes.union(shape, SIDE_SHAPES[side])
}
val key = Direction.values().foldIndexed(0) { i, acc, dir ->
if (state[CONNECTIONS[dir]] == CableConnection.ON) {
acc or (1 shl i)
} else {
acc
}
return shape
}
return SHAPE_CACHE[key]
}
}
@ -112,22 +123,20 @@ class CableBlock: Block(
val offsetPos = pos.offset(direction)
val state = world.getBlockState(offsetPos)
val block = state.block
return when (block) {
this -> {
val prop = CONNECTIONS[direction.opposite]
when (state[prop]) {
CableConnection.DISABLED -> CableConnection.DISABLED
else -> CableConnection.ON
}
return if (block == this) {
val prop = CONNECTIONS[direction.opposite]
when (state[prop]) {
CableConnection.DISABLED -> CableConnection.DISABLED
else -> CableConnection.ON
}
is NetworkComponentBlock -> {
if (block.getNetworkConnectedSides(state, world, offsetPos).contains(direction.opposite)) {
CableConnection.ON
} else {
CableConnection.OFF
}
} 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
} else {
CableConnection.OFF
}
}
@ -190,4 +199,18 @@ class CableBlock: Block(
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()
}
}
}
}
}

View File

@ -15,6 +15,8 @@ import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.FaceDeviceBlock
import kotlin.math.max
import kotlin.math.min
/**
* @author shadowfacts
@ -57,7 +59,7 @@ class ExtractorBlock: FaceDeviceBlock<ExtractorBlockEntity>(
arr[i] = 16.0 - arr[i]
}
}
createCuboidShape(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5])
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) }
}
@ -67,7 +69,7 @@ class ExtractorBlock: FaceDeviceBlock<ExtractorBlockEntity>(
override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = EXTRACTOR_SHAPES
override fun createBlockEntity(world: BlockView) = ExtractorBlockEntity()
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) {

View File

@ -5,8 +5,10 @@ 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.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities
@ -26,7 +28,7 @@ import kotlin.properties.Delegates
/**
* @author shadowfacts
*/
class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR),
class ExtractorBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.EXTRACTOR, pos, state),
NetworkStackDispatcher<ExtractorBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
@ -106,21 +108,21 @@ class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR),
return false
}
override fun toCommonTag(tag: CompoundTag) {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: CompoundTag) {
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
}
override fun loadDeviceConfiguration(tag: CompoundTag) {
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
}

View File

@ -25,6 +25,8 @@ import net.minecraft.world.World
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.FaceDeviceBlock
import java.util.*
import kotlin.math.max
import kotlin.math.min
/**
* @author shadowfacts
@ -66,7 +68,7 @@ class InserterBlock: FaceDeviceBlock<InserterBlockEntity>(
arr[i] = 16.0 - arr[i]
}
}
createCuboidShape(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5])
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) }
}
@ -76,7 +78,7 @@ class InserterBlock: FaceDeviceBlock<InserterBlockEntity>(
override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = INSERTER_SHAPES
override fun createBlockEntity(world: BlockView) = InserterBlockEntity()
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) {
@ -94,7 +96,7 @@ class InserterBlock: FaceDeviceBlock<InserterBlockEntity>(
if (!world.isClient) {
val be = getBlockEntity(world, pos)!!
be.sync()
be.markUpdate()
val factory = object: ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler {

View File

@ -5,8 +5,10 @@ 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.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.block.DeviceBlockEntity
@ -19,15 +21,17 @@ 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 kotlin.math.min
/**
* @author shadowfacts
*/
class InserterBlockEntity: DeviceBlockEntity(PhyBlockEntities.INSERTER),
class InserterBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.INSERTER, pos, state),
ItemStackPacketHandler,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
ClientConfigurableDevice,
GhostInv {
companion object {
val SLEEP_TIME = 40L
@ -40,6 +44,9 @@ class InserterBlockEntity: DeviceBlockEntity(PhyBlockEntities.INSERTER),
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)
@ -127,24 +134,24 @@ class InserterBlockEntity: DeviceBlockEntity(PhyBlockEntities.INSERTER),
currentRequest = null
}
override fun toCommonTag(tag: CompoundTag) {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
tag.put("StackToExtract", stackToExtract.toTag(CompoundTag()))
tag.put("StackToExtract", stackToExtract.writeNbt(NbtCompound()))
}
override fun fromCommonTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
stackToExtract = ItemStack.fromTag(tag.getCompound("StackToExtract"))
stackToExtract = ItemStack.fromNbt(tag.getCompound("StackToExtract"))
}
override fun writeDeviceConfiguration(tag: CompoundTag) {
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("AmountToExtract", amountToExtract)
}
override fun loadDeviceConfiguration(tag: CompoundTag) {
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
amountToExtract = tag.getInt("AmountToExtract")
}

View File

@ -3,6 +3,7 @@ package net.shadowfacts.phycon.block.inserter
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.slot.Slot
@ -44,9 +45,9 @@ class InserterScreen(
amountField = TextFieldWidget(textRenderer, x + 57, y + 24, 80, 9, LiteralText("Amount"))
amountField.text = handler.inserter.amountToExtract.toString()
amountField.setHasBorder(false)
amountField.setDrawsBackground(false)
amountField.isVisible = true
amountField.setSelected(true)
amountField.setTextFieldFocused(true)
amountField.setEditableColor(0xffffff)
amountField.setTextPredicate {
if (it.isEmpty()) {
@ -60,7 +61,7 @@ class InserterScreen(
}
}
}
addChild(amountField)
addDrawableChild(amountField)
}
fun amountUpdated() {
@ -70,16 +71,16 @@ class InserterScreen(
}
}
override fun tick() {
super.tick()
override fun handledScreenTick() {
super.handledScreenTick()
amountField.tick()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
renderBackground(matrixStack)
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(BACKGROUND)
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)
@ -96,7 +97,7 @@ class InserterScreen(
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, slotActionType: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, slotActionType)
amountField.setSelected(true)
amountField.setTextFieldFocused(true)
}
override fun charTyped(c: Char, i: Int): Boolean {

View File

@ -37,7 +37,7 @@ class InserterScreenHandler(
init {
// fake slot
addSlot(GhostSlot(inserter::stackToExtract, 31, 20))
addSlot(GhostSlot(inserter, 31, 20))
// player inv
for (y in 0 until 3) {
@ -60,17 +60,17 @@ class InserterScreenHandler(
return true
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity) {
// fake slot
if (slotId == 0) {
if (player.inventory.cursorStack.isEmpty) {
if (cursorStack.isEmpty) {
inserter.stackToExtract = ItemStack.EMPTY
} else {
inserter.stackToExtract = player.inventory.cursorStack.copyWithCount(1)
inserter.stackToExtract = cursorStack.copyWithCount(1)
}
stackToExtractChanged()
}
return super.onSlotClick(slotId, clickData, actionType, player)
super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {

View File

@ -51,7 +51,7 @@ class MinerBlock: DeviceBlock<MinerBlockEntity>(
builder.add(FACING)
}
override fun createBlockEntity(world: BlockView) = MinerBlockEntity()
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

View File

@ -7,7 +7,7 @@ import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.BlockPos
@ -16,7 +16,7 @@ import net.minecraft.world.World
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode
@ -27,7 +27,7 @@ import kotlin.math.min
/**
* @author shadowfacts
*/
class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
class MinerBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.MINER, pos, state),
NetworkStackProvider,
NetworkStackDispatcher<MinerBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
@ -39,7 +39,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
private val invProxy = MinerInvProxy(this)
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = TerminalBlockEntity.INSERTION_TIMEOUT
override val dispatchStackTimeout = AbstractTerminalBlockEntity.INSERTION_TIMEOUT
override val controller = ActivationController(40L, this)
override var providerPriority = 0
@ -57,10 +57,10 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
if (minerMode != MinerMode.ON_DEMAND || packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
sendPacket(ReadInventoryPacket(invProxy, ipAddress, packet.source))
sendPacket(ReadGroupedInventoryPacket(invProxy, ipAddress, packet.source))
}
private fun handleLocateStack(packet: LocateStackPacket) {
@ -156,23 +156,23 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
return minerMode == MinerMode.ON_DEMAND
}
override fun toCommonTag(tag: CompoundTag) {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: CompoundTag) {
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("MinerMode", minerMode.name)
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("ProviderPriority", providerPriority)
}
override fun loadDeviceConfiguration(tag: CompoundTag) {
override fun loadDeviceConfiguration(tag: NbtCompound) {
minerMode = MinerMode.valueOf(tag.getString("MinerMode"))
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
providerPriority = tag.getInt("ProviderPriority")
@ -181,7 +181,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
enum class MinerMode {
ON_DEMAND, AUTOMATIC;
val friendlyName = TranslatableText("gui.phycon.miner_mode.${name.toLowerCase()}")
val friendlyName = TranslatableText("gui.phycon.miner_mode.${name.lowercase()}")
}
class MinerInvProxy(val miner: MinerBlockEntity): GroupedItemInvView {
@ -205,7 +205,8 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
// todo: does BlockState.equals actually work or is reference equality fine for BlockStates?
if (cachedDrops == null || realState != cachedState || recalculate) {
cachedState = realState
val be = if (realState.block.hasBlockEntity()) world.getBlockEntity(targetPos) else null
val be = if (realState.hasBlockEntity()) world.getBlockEntity(targetPos) else null
cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be, null, TOOL)
}
return cachedDrops!!

View File

@ -40,7 +40,7 @@ class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>(
Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0)
)
override fun createBlockEntity(world: BlockView) = InterfaceBlockEntity()
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) {

View File

@ -6,7 +6,8 @@ import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.ItemAttributes
import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.init.PhyBlockEntities
@ -18,12 +19,13 @@ 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 kotlin.math.min
/**
* @author shadowfacts
*/
class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE),
class InterfaceBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.INTERFACE, pos, state),
ItemStackPacketHandler,
NetworkStackProvider,
NetworkStackReceiver,
@ -36,20 +38,21 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE),
override var receiverPriority = 0
var syncPriorities = true
// todo: should this be a weak ref?
private var inventory: GroupedItemInv? = null
private var inventory: WeakReference<GroupedItemInv>? = null
fun updateInventory() {
val offsetPos = pos.offset(facing)
val option = SearchOptions.inDirection(facing)
inventory = ItemAttributes.GROUPED_INV.getFirstOrNull(world, offsetPos, option)
inventory = ItemAttributes.GROUPED_INV.getFirstOrNull(world, offsetPos, option).let {
WeakReference(it)
}
}
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 == null) updateInventory()
return inventory
if (inventory?.get() == null) updateInventory()
return inventory?.get()
}
override fun handle(packet: Packet) {
@ -63,8 +66,11 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE),
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
getInventory()?.also { inv ->
sendPacket(ReadInventoryPacket(inv, ipAddress, packet.source))
sendPacket(ReadGroupedInventoryPacket(inv, ipAddress, packet.source))
}
}
@ -109,23 +115,23 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE),
}
}
override fun toCommonTag(tag: CompoundTag) {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: CompoundTag) {
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("ProviderPriority", providerPriority)
tag.putInt("ReceiverPriority", receiverPriority)
tag.putBoolean("SyncPriorities", syncPriorities)
}
override fun loadDeviceConfiguration(tag: CompoundTag) {
override fun loadDeviceConfiguration(tag: NbtCompound) {
providerPriority = tag.getInt("ProviderPriority")
receiverPriority = tag.getInt("ReceiverPriority")
syncPriorities = tag.getBoolean("SyncPriorities")

View File

@ -4,6 +4,9 @@ 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
import net.minecraft.block.entity.BlockEntityTicker
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
@ -40,7 +43,17 @@ class SwitchBlock: BlockWithEntity<SwitchBlockEntity>(
return getBlockEntity(world, pos)?.interfaces?.find { it.side == side }
}
override fun createBlockEntity(world: BlockView) = SwitchBlockEntity()
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))

View File

@ -1,12 +1,14 @@
package net.shadowfacts.phycon.block.netswitch
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntity
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.util.Tickable
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtList
import net.minecraft.network.Packet
import net.minecraft.network.listener.ClientPlayPacketListener
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.Interface
@ -15,6 +17,7 @@ import net.shadowfacts.phycon.api.frame.PacketFrame
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.api.util.MACAddress
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.NetworkUtil
@ -25,9 +28,7 @@ import java.util.LinkedList
/**
* @author shadowfacts
*/
class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
BlockEntityClientSerializable,
Tickable {
class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockEntities.SWITCH, pos, state) {
companion object {
var SWITCHING_CAPACITY = 256 // 256 packets/tick
@ -36,6 +37,7 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
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()
@ -65,12 +67,12 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) forwarding {} to side {}", this, fromItf.side, fromItf.macAddress, frame, dir)
interfaceForSide(dir).send(frame)
} else {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) flooding {}", this, fromItf.side, fromItf.macAddress, frame)
flood(frame, fromItf)
}
}
private fun flood(frame: EthernetFrame, source: Interface) {
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)
@ -79,10 +81,23 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
private fun findDestination(fromItf: Interface): Interface? {
val side = (fromItf as SwitchInterface).side
return NetworkUtil.findConnectedInterface(world!!, pos, side)
return destinationCache[side.ordinal]?.get()
?: NetworkUtil.findConnectedInterface(world!!, pos, side)?.also {
destinationCache[side.ordinal] = WeakReference(it)
}
}
override fun tick() {
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)
}
fun tick() {
packetsHandledThisTick = 0
while (delayedPackets.isNotEmpty() && packetsHandledThisTick <= SWITCHING_CAPACITY) {
@ -91,39 +106,41 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
}
}
override fun toTag(tag: CompoundTag): CompoundTag {
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
val list = ListTag()
val list = NbtList()
for ((frame, fromItf) in delayedPackets) {
val packet = frame.packet
if (packet !is ItemStackPacket) continue
val compound = CompoundTag()
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.toTag(CompoundTag()))
compound.put("Stack", packet.stack.writeNbt(NbtCompound()))
list.add(compound)
}
tag.put("DelayedStackPackets", list)
return super.toTag(tag)
}
override fun fromTag(state: BlockState, tag: CompoundTag) {
super.fromTag(state, tag)
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
tag.getLongArray("InterfaceAddresses")?.forEachIndexed { i, l ->
interfaces[i].macAddress = MACAddress(l)
}
tag.getList("DelayedStackPackets", 10).forEach { it ->
val compound = it as CompoundTag
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.fromTag(compound.getCompound("Stack"))
val stack = ItemStack.fromNbt(compound.getCompound("Stack"))
if (!stack.isEmpty) {
val packet = ItemStackPacket(stack, sourceIP, destinationIP)
val frame = BasePacketFrame(packet, sourceMAC, destinationMAC)
@ -132,15 +149,14 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
}
}
override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
return tag
override fun toUpdatePacket(): Packet<ClientPlayPacketListener>? {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun fromClientTag(tag: CompoundTag) {
tag.getLongArray("InterfaceAddresses")?.forEachIndexed { i, l ->
interfaces[i].macAddress = MACAddress(l)
}
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
return tag
}
class SwitchInterface(
@ -157,6 +173,10 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH),
override fun send(frame: EthernetFrame) {
switch.get()?.findDestination(this)?.receive(frame)
}
override fun cableDisconnected() {
switch.get()?.cableDisconnected(this)
}
}
}

View File

@ -0,0 +1,37 @@
package net.shadowfacts.phycon.block.p2p
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class P2PInterfaceBlock: FaceDeviceBlock<P2PInterfaceBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "p2p_interface")
}
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)
)
override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PInterfaceBlockEntity(pos, state)
}

View File

@ -0,0 +1,47 @@
package net.shadowfacts.phycon.block.p2p
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.minecraft.block.BlockState
import net.minecraft.util.math.BlockPos
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.FaceDeviceBlock
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.ReadItemStoragePacket
import net.shadowfacts.phycon.packet.RequestInventoryPacket
/**
* @author shadowfacts
*/
class P2PInterfaceBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.P2P_INTERFACE, pos, state) {
private var inventory: Storage<ItemVariant>? = null
private fun updateInventory() {
val facing = cachedState[FaceDeviceBlock.FACING]
inventory = ItemStorage.SIDED.find(world!!, pos.offset(facing), facing.opposite)
}
private fun getInventory(): Storage<ItemVariant>? {
if (inventory == null) updateInventory()
return inventory
}
override fun handle(packet: Packet) {
when (packet) {
is RequestInventoryPacket -> handleRequestInventory(packet)
}
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
if (packet.kind != RequestInventoryPacket.Kind.SIDED) {
return
}
getInventory()?.also {
sendPacket(ReadItemStoragePacket(it, ipAddress, packet.source))
}
}
}

View File

@ -0,0 +1,40 @@
package net.shadowfacts.phycon.block.p2p
import net.minecraft.block.BlockState
import net.minecraft.block.InventoryProvider
import net.minecraft.block.Material
import net.minecraft.inventory.SidedInventory
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.util.shape.VoxelShape
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class P2PReceiverBlock: FaceDeviceBlock<P2PReceiverBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "p2p_receiver")
}
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)
)
override fun createBlockEntity(pos: BlockPos, state: BlockState) = P2PReceiverBlockEntity(pos, state)
}

View File

@ -0,0 +1,144 @@
package net.shadowfacts.phycon.block.p2p
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.nbt.NbtCompound
import net.minecraft.text.Text
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.FaceDeviceBlock
import net.shadowfacts.phycon.frame.NetworkSplitFrame
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import java.lang.ref.WeakReference
/**
* @author shadowfacts
*/
class P2PReceiverBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.P2P_RECEIVER, pos, state),
ClientConfigurableDevice {
enum class Status {
OK,
NO_TARGET,
WAITING_FOR_RESPONSE;
val displayName: Text
get() = when (this) {
OK -> TranslatableText("gui.phycon.p2p_receiver.status.ok")
NO_TARGET -> TranslatableText("gui.phycon.p2p_receiver.status.no_target")
WAITING_FOR_RESPONSE -> TranslatableText("gui.phycon.p2p_receiver.status.waiting_for_response")
}
}
companion object {
const val REQUEST_INVENTORY_TIMEOUT: Long = 100 // ticks
fun provideItemStorage(be: P2PReceiverBlockEntity, side: Direction): Storage<ItemVariant>? {
if (side == be.cachedState[FaceDeviceBlock.FACING]) {
return be.getTargetInventory()
}
return null
}
}
var target: IPAddress? = null
var status = Status.NO_TARGET
private var requestTimestamp: Long = 0
var clientObserver: (() -> Unit)? = null
private var isFirstTick = true
private var targetInventory: WeakReference<Storage<ItemVariant>>? = null
override fun handle(packet: Packet) {
when (packet) {
is PongPacket -> if (packet.source == target) status = Status.OK
is ReadItemStoragePacket -> targetInventory = WeakReference(packet.inventory)
is DeviceRemovedPacket -> if (packet.source == target) targetInventory = null
}
}
override fun handleNetworkSplit() {
super.handleNetworkSplit()
targetInventory = null
}
override fun tick() {
super.tick()
if (isFirstTick) {
isFirstTick = false
updateStatus()
}
}
fun getTargetInventory(): Storage<ItemVariant>? {
if (target == null) {
return null
}
if (targetInventory?.get() == null && (counter - requestTimestamp) >= REQUEST_INVENTORY_TIMEOUT) {
status = Status.WAITING_FOR_RESPONSE
requestTimestamp = counter
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.SIDED, ipAddress, target!!))
}
return targetInventory?.get()
}
private fun updateStatus() {
if (world?.isClient != false) {
return
}
assert(!world!!.isClient)
if (target == null) {
status = Status.NO_TARGET
} else {
status = Status.WAITING_FOR_RESPONSE
sendPacket(PingPacket(ipAddress, target!!))
}
markUpdate()
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun toClientTag(tag: NbtCompound): NbtCompound {
tag.putInt("Status", status.ordinal)
return super.toClientTag(tag)
}
override fun fromClientTag(tag: NbtCompound) {
super.fromClientTag(tag)
status = Status.values()[tag.getInt("Status")]
clientObserver?.invoke()
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
target?.address?.let { tag.putInt("Target", it) }
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
target = if (tag.contains("Target")) {
IPAddress(tag.getInt("Target"))
} else {
null
}
updateStatus()
}
}

View File

@ -44,7 +44,7 @@ class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>(
builder.add(POWERED)
}
override fun createBlockEntity(world: BlockView) = RedstoneControllerBlockEntity()
override fun createBlockEntity(pos: BlockPos, state: BlockState) = RedstoneControllerBlockEntity(pos, state)
override fun getPlacementState(context: ItemPlacementContext): BlockState {
val state = super.getPlacementState(context)

View File

@ -1,6 +1,8 @@
package net.shadowfacts.phycon.block.redstone_controller
import net.minecraft.nbt.CompoundTag
import net.minecraft.block.BlockState
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.math.BlockPos
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.init.PhyBlockEntities
@ -12,7 +14,7 @@ import net.shadowfacts.phycon.util.RedstoneMode
/**
* @author shadowfacts
*/
class RedstoneControllerBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE_CONTROLLER),
class RedstoneControllerBlockEntity(pos: BlockPos, state: BlockState): DeviceBlockEntity(PhyBlockEntities.REDSTONE_CONTROLLER, pos, state),
ClientConfigurableDevice {
var managedDevices = Array<IPAddress?>(5) { null }
@ -53,22 +55,22 @@ class RedstoneControllerBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE
}
}
override fun toCommonTag(tag: CompoundTag) {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: CompoundTag) {
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address })
tag.putString("RedstoneMode", redstoneMode.name)
}
override fun loadDeviceConfiguration(tag: CompoundTag) {
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

@ -45,7 +45,7 @@ class RedstoneEmitterBlock: FaceDeviceBlock<RedstoneEmitterBlockEntity>(
Direction.EAST to createCuboidShape(13.0, 0.0, 0.0, 16.0, 16.0, 16.0)
)
override fun createBlockEntity(world: BlockView) = RedstoneEmitterBlockEntity()
override fun createBlockEntity(pos: BlockPos, state: BlockState) = RedstoneEmitterBlockEntity(pos, state)
override fun emitsRedstonePower(state: BlockState): Boolean {
return true
@ -67,7 +67,7 @@ class RedstoneEmitterBlock: FaceDeviceBlock<RedstoneEmitterBlockEntity>(
if (!world.isClient) {
val be = getBlockEntity(world, pos)!!
be.sync()
be.markUpdate()
val factory = object: ExtendedScreenHandlerFactory {
override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler {

View File

@ -1,31 +1,38 @@
package net.shadowfacts.phycon.block.redstone_emitter
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtCompound
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.BlockPos
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
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.ReadInventoryPacket
import net.shadowfacts.phycon.packet.ReadGroupedInventoryPacket
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: DeviceBlockEntity(PhyBlockEntities.REDSTONE_EMITTER),
ClientConfigurableDevice {
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
var stackToMonitor: ItemStack = ItemStack.EMPTY
override var ghostSlotStack: ItemStack
get() = stackToMonitor
set(value) { stackToMonitor = value }
var maxAmount = 64
var mode = Mode.ANALOG
set(value) {
@ -35,12 +42,12 @@ class RedstoneEmitterBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE_EM
override fun handle(packet: Packet) {
when (packet) {
is ReadInventoryPacket -> handleReadInventory(packet)
is ReadGroupedInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
}
}
private fun handleReadInventory(packet: ReadInventoryPacket) {
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
recalculateRedstone()
}
@ -63,7 +70,7 @@ class RedstoneEmitterBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE_EM
}
private fun updateInventories() {
sendPacket(RequestInventoryPacket(ipAddress))
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
}
private fun recalculateRedstone() {
@ -95,26 +102,26 @@ class RedstoneEmitterBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE_EM
world!!.updateNeighborsAlways(pos.offset(cachedState[FaceDeviceBlock.FACING]), cachedState.block)
}
override fun toCommonTag(tag: CompoundTag) {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.putInt("CachedEmittedPower", cachedEmittedPower)
tag.put("StackToMonitor", stackToMonitor.toTag(CompoundTag()))
tag.put("StackToMonitor", stackToMonitor.writeNbt(NbtCompound()))
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
cachedEmittedPower = tag.getInt("CachedEmittedPower")
stackToMonitor = ItemStack.fromTag(tag.getCompound("StackToMonitor"))
stackToMonitor = ItemStack.fromNbt(tag.getCompound("StackToMonitor"))
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: CompoundTag) {
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("MaxAmount", maxAmount)
tag.putString("Mode", mode.name)
}
override fun loadDeviceConfiguration(tag: CompoundTag) {
override fun loadDeviceConfiguration(tag: NbtCompound) {
maxAmount = tag.getInt("MaxAmount")
mode = Mode.valueOf(tag.getString("Mode"))
}
@ -122,7 +129,7 @@ class RedstoneEmitterBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE_EM
enum class Mode {
ANALOG, DIGITAL;
val friendlyName = TranslatableText("gui.phycon.redstone_emitter_mode.${name.toLowerCase()}")
val friendlyName = TranslatableText("gui.phycon.redstone_emitter_mode.${name.lowercase()}")
}
}

View File

@ -2,6 +2,7 @@ package net.shadowfacts.phycon.block.redstone_emitter
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.text.Text
@ -49,8 +50,8 @@ class RedstoneEmitterScreen(
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(BACKGROUND)
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)
@ -129,10 +130,9 @@ class RedstoneEmitterScreen(
inv.leftAnchor equalTo (minX + 8)
inv.topAnchor equalTo (minY + 72)
// offset by 1 on each edge to account for MC border
field.widthAnchor equalTo 82
field.heightAnchor equalTo 11
field.leftAnchor equalTo (minX + 56)
field.leftAnchor equalTo (minX + 57)
field.topAnchor equalTo (minY + 23)
hStack.centerXAnchor equalTo view.centerXAnchor

View File

@ -36,7 +36,7 @@ class RedstoneEmitterScreenHandler(
init {
// fake slot
addSlot(GhostSlot(emitter::stackToMonitor, 31, 20))
addSlot(GhostSlot(emitter, 31, 20))
// player inv
for (y in 0 until 3) {
@ -55,16 +55,16 @@ class RedstoneEmitterScreenHandler(
return true
}
override fun onSlotClick(slotId: Int, clickData: Int, slotActionType: SlotActionType, player: PlayerEntity): ItemStack {
override fun onSlotClick(slotId: Int, clickData: Int, slotActionType: SlotActionType, player: PlayerEntity) {
// fake slot
if (slotId == 0) {
if (player.inventory.cursorStack.isEmpty) {
if (cursorStack.isEmpty) {
emitter.stackToMonitor = ItemStack.EMPTY
} else {
emitter.stackToMonitor = player.inventory.cursorStack.copyWithCount(1)
emitter.stackToMonitor = cursorStack.copyWithCount(1)
}
}
return super.onSlotClick(slotId, clickData, slotActionType, player)
super.onSlotClick(slotId, clickData, slotActionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {

View File

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

View File

@ -0,0 +1,283 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.inventory.Inventory
import net.minecraft.inventory.InventoryChangedListener
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.ItemScatterer
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.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 java.lang.ref.WeakReference
import java.util.*
import java.util.function.IntBinaryOperator
import kotlin.math.min
import kotlin.properties.Delegates
/**
* @author shadowfacts
*/
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
}
protected val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
val internalBuffer = TerminalBufferInventory(18)
protected val pendingRequests = LinkedList<StackLocateRequest>()
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = INSERTION_TIMEOUT
val cachedNetItems = ItemStackCollections.intMap()
private var requestInventoryTimestamp: Long? = null
// todo: multiple players could have the terminal open simultaneously
var netItemObserver: WeakReference<NetItemObserver>? = null
init {
internalBuffer.addListener(this)
}
override fun findDestination(): Interface? {
for (dir in Direction.values()) {
val itf = NetworkUtil.findConnectedInterface(world!!, pos, dir)
if (itf != null) {
return itf
}
}
return null
}
override fun 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)
}
}
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
updateAndSync()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
updateAndSync()
}
private fun handleStackLocation(packet: StackLocationPacket) {
val request = pendingRequests.firstOrNull {
ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack)
}
if (request != null) {
request.results.add(packet.amount to packet.stackProvider)
if (request.isFinishable(counter)) {
stackLocateRequestCompleted(request)
}
}
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val mode =
if (packet.bounceCount > 0) {
// if this stack bounced from an inventory, that means we previously tried to send it to the network, so retry
TerminalBufferInventory.Mode.TO_NETWORK
} else {
TerminalBufferInventory.Mode.FROM_NETWORK
}
val remaining = internalBuffer.insert(packet.stack, mode)
// this happens outside the normal update loop because by receiving the item stack packet
// we "know" how much the count in the source inventory has changed
updateAndSync()
return remaining
}
protected fun updateAndSync() {
updateNetItems()
// syncs the internal buffer to the client
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 beginInsertions() {
if (world!!.isClient) return
for (slot in 0 until internalBuffer.size()) {
if (internalBuffer.getMode(slot) != TerminalBufferInventory.Mode.TO_NETWORK) continue
if (pendingInsertions.any { it.bufferSlot == slot }) continue
val stack = internalBuffer.getStack(slot)
dispatchItemStack(stack) { insertion ->
insertion.bufferSlot = slot
}
}
}
private fun finishPendingRequests() {
if (world!!.isClient) return
if (pendingRequests.isEmpty()) return
val finishable = pendingRequests.filter { it.isFinishable(counter) }
// stackLocateRequestCompleted removes the object from pendingRequests
finishable.forEach(::stackLocateRequestCompleted)
}
override fun tick() {
super.tick()
if (!world!!.isClient) {
finishPendingRequests()
finishTimedOutPendingInsertions()
if (counter % 20 == 0L) {
beginInsertions()
}
if (requestInventoryTimestamp != null && (counter - requestInventoryTimestamp!!) >= REQUEST_INVENTORY_TIMEOUT) {
updateAndSync()
requestInventoryTimestamp = null
}
}
}
open fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) {
updateAndSync()
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))
}
protected open fun stackLocateRequestCompleted(request: StackLocateRequest) {
pendingRequests.remove(request)
val sortedResults = request.results.toMutableList()
sortedResults.sortWith { a, b ->
// sort results first by provider priority, and then by the count that it can provide
if (a.second.providerPriority == b.second.providerPriority) {
b.first - a.first
} else {
b.second.providerPriority - a.second.providerPriority
}
}
var amountRequested = 0
while (amountRequested < request.amount && sortedResults.isNotEmpty()) {
val (sourceAmount, sourceInterface) = sortedResults.removeAt(0)
val amountToRequest = min(sourceAmount, request.amount - amountRequested)
amountRequested += amountToRequest
sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, sourceInterface.ipAddress))
}
}
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun finishInsertion(insertion: PendingInsertion): ItemStack {
val remaining = super.finishInsertion(insertion)
internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateAndSync()
// 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()
}
}
open fun dropItems() {
ItemScatterer.spawn(world, pos, internalBuffer)
}
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"))
}
interface NetItemObserver {
fun netItemsChanged()
}
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
var bufferSlot by Delegates.notNull<Int>()
}
open class StackLocateRequest(
val stack: ItemStack,
val amount: Int,
val timestamp: Long,
) {
var results: MutableSet<Pair<Int, NetworkStackProvider>> = mutableSetOf()
val totalResultAmount: Int
get() = results.fold(0) { acc, (amount, _) -> acc + amount }
fun isFinishable(currentTimestamp: Long): Boolean {
// we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to
// correctly sort by priority
return currentTimestamp - timestamp >= LOCATE_REQUEST_TIMEOUT
}
}
}

View File

@ -0,0 +1,204 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.Element
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoHandledScreen
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.ceil
import kotlin.math.min
/**
* @author shadowfacts
*/
abstract class AbstractTerminalScreen<BE: AbstractTerminalBlockEntity, T: AbstractTerminalScreenHandler<BE>>(
handler: T,
playerInv: PlayerInventory,
title: Text,
val terminalBackgroundWidth: Int,
val terminalBackgroundHeight: Int,
): CacaoHandledScreen<T>(handler, playerInv, title) {
interface SearchQueryListener {
fun terminalSearchQueryChanged(newValue: String)
fun requestTerminalSearchFieldUpdate(): String?
}
companion object {
var searchQueryListener: SearchQueryListener? = null
}
abstract val backgroundTexture: Identifier
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
init {
backgroundWidth = terminalBackgroundWidth
backgroundHeight = terminalBackgroundHeight
terminalVC = createViewController()
addWindow(ScreenHandlerWindow(handler, terminalVC))
requestUpdatedItems()
}
abstract fun createViewController(): AbstractTerminalViewController<*, *, *>
fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchQuery, scrollPosition.toFloat()))
}
private fun showRequestAmountDialog(stack: ItemStack) {
val vc = TerminalRequestAmountViewController(this, stack)
addWindow(Window(vc))
amountVC = vc
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private val DECIMAL_FORMAT = DecimalFormat("#.#").apply { roundingMode = RoundingMode.HALF_UP }
private val FORMAT = DecimalFormat("##").apply { roundingMode = RoundingMode.HALF_UP }
fun drawNetworkSlotAmount(stack: ItemStack, x: Int, y: Int) {
val amount = stack.count
val s = when {
amount < 1_000 -> amount.toString()
amount < 1_000_000 -> {
val format = if (amount < 10_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000.0) + "K"
}
amount < 1_000_000_000 -> {
val format = if (amount < 10_000_000) DECIMAL_FORMAT else FORMAT
format.format(amount / 1_000_000.0) + "M"
}
else -> {
DECIMAL_FORMAT.format(amount / 1000000000.0).toString() + "B"
}
}
// draw damage bar
// empty string for label because vanilla renders the count behind the damage bar
itemRenderer.renderGuiItemOverlay(textRenderer, stack, x, y, "")
// 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()
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
super.drawBackground(matrixStack, delta, mouseX, mouseY)
drawBackgroundTexture(matrixStack)
}
open fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.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 handledScreenTick() {
super.handledScreenTick()
if (amountVC != null) {
amountVC!!.field.tick()
} else {
terminalVC.searchField.tick()
}
val newSearchQuery = searchQueryListener?.requestTerminalSearchFieldUpdate()
if (newSearchQuery != null && searchQuery != newSearchQuery) {
searchQuery = newSearchQuery
terminalVC.searchField.text = newSearchQuery
requestUpdatedItems()
}
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id) && 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

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

View File

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

View File

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

View File

@ -0,0 +1,138 @@
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.block.BlockState
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.SimpleInventory
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.TranslatableText
import net.minecraft.util.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.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) {
val craftingInv = SimpleInventory(9)
private val completedCraftingStackRequests = LinkedList<CraftingStackLocateRequest>()
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)
}
override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal")
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos)
}
}
player.openHandledScreen(factory)
}
}
fun requestItemsForCrafting(maxAmount: Int) {
val amounts = ItemStackCollections.map<IntArray>()
for (i in 0 until craftingInv.size()) {
val craftingInvStack = craftingInv.getStack(i)
if (craftingInvStack.isEmpty) continue
if (craftingInvStack.count >= craftingInvStack.maxCount) continue
if (craftingInvStack !in amounts) amounts[craftingInvStack] = IntArray(9) { 0 }
amounts[craftingInvStack]!![i] = min(maxAmount, craftingInvStack.maxCount - craftingInvStack.count)
}
for ((stack, amountPerSlot) in amounts) {
val total = amountPerSlot.sum()
val request = CraftingStackLocateRequest(stack, total, counter, amountPerSlot)
pendingRequests.add(request)
sendPacket(LocateStackPacket(stack, ipAddress))
}
}
override fun stackLocateRequestCompleted(request: StackLocateRequest) {
if (request is CraftingStackLocateRequest) {
completedCraftingStackRequests.add(request)
}
super.stackLocateRequestCompleted(request)
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val craftingReq = completedCraftingStackRequests.find { ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) }
if (craftingReq != null) {
var remaining = packet.stack.copy()
for (i in 0 until craftingInv.size()) {
val currentStack = craftingInv.getStack(i)
if (currentStack.count >= currentStack.maxCount) continue
if (!ItemStackUtil.areEqualIgnoreAmounts(currentStack, remaining)) continue
val toInsert = minOf(remaining.count, currentStack.maxCount - currentStack.count, craftingReq.amountPerSlot[i])
currentStack.count += toInsert
remaining.count -= toInsert
craftingReq.amountPerSlot[i] -= toInsert
craftingReq.received += toInsert
if (remaining.isEmpty) {
break
}
}
if (craftingReq.amountPerSlot.sum() == 0 || craftingReq.received >= craftingReq.totalResultAmount) {
completedCraftingStackRequests.remove(craftingReq)
}
if (!remaining.isEmpty) {
remaining = internalBuffer.insert(remaining, TerminalBufferInventory.Mode.FROM_NETWORK)
}
updateAndSync()
return remaining
} else {
return super.doHandleItemStack(packet)
}
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.put("CraftingInv", craftingInv.toTag())
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
craftingInv.fromTag(tag.getList("CraftingInv", 10))
}
class CraftingStackLocateRequest(
stack: ItemStack,
amount: Int,
timestamp: Long,
val amountPerSlot: IntArray,
): StackLocateRequest(stack, amount, timestamp) {
var received: Int = 0
}
}

View File

@ -0,0 +1,49 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
/**
* @author shadowfacts
*/
class CraftingTerminalScreen(
handler: CraftingTerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
): AbstractTerminalScreen<CraftingTerminalBlockEntity, CraftingTerminalScreenHandler>(
handler,
playerInv,
title,
259,
252,
) {
companion object {
private val BACKGROUND_1 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_1.png")
private val BACKGROUND_2 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_2.png")
}
override val backgroundTexture = BACKGROUND_1
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return CraftingTerminalViewController(this, handler)
}
override fun drawBackgroundTexture(matrixStack: MatrixStack) {
RenderSystem.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)
}
}

View File

@ -0,0 +1,165 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.CraftingInventory
import net.minecraft.inventory.CraftingResultInventory
import net.minecraft.inventory.Inventory
import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf
import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket
import net.minecraft.recipe.RecipeMatcher
import net.minecraft.recipe.RecipeType
import net.minecraft.screen.slot.CraftingResultSlot
import net.minecraft.screen.slot.Slot
import net.minecraft.server.network.ServerPlayerEntity
import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens
/**
* @author shadowfacts
*/
class CraftingTerminalScreenHandler(
syncId: Int,
playerInv: PlayerInventory,
terminal: CraftingTerminalBlockEntity,
): AbstractTerminalScreenHandler<CraftingTerminalBlockEntity>(PhyScreens.CRAFTING_TERMINAL, syncId, playerInv, terminal) {
val craftingInv = CraftingInv(this)
val result = CraftingResultInventory()
val resultSlot: CraftingResultSlot
val craftingSlotsStart: Int
val craftingSlotsEnd: Int
get() = craftingSlotsStart + 9
override val xOffset: Int
get() = 5
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf):
this(
syncId,
playerInv,
PhyBlocks.CRAFTING_TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!
)
init {
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)
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))
}
}
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)
}
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)
if (!insertItem(craftingResult, playerSlotsStart, playerSlotsEnd, true)) {
return ItemStack.EMPTY
}
resultSlot.onQuickTransfer(craftingResult, originalResult)
if (craftingResult.isEmpty) {
resultSlot.stack = ItemStack.EMPTY
}
if (craftingResult.count == originalResult.count) {
return ItemStack.EMPTY
}
resultSlot.onTakeItem(player, craftingResult)
player.dropItem(craftingResult, false)
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
override fun isEmpty(): Boolean {
return backing.isEmpty
}
override fun getStack(i: Int): ItemStack {
return backing.getStack(i)
}
override fun removeStack(i: Int): ItemStack {
return backing.removeStack(i)
}
override fun removeStack(i: Int, j: Int): ItemStack {
val res = backing.removeStack(i, j)
if (!res.isEmpty) {
handler.onContentChanged(this)
}
return res
}
override fun setStack(i: Int, itemStack: ItemStack?) {
backing.setStack(i, itemStack)
handler.onContentChanged(this)
}
override fun clear() {
backing.clear()
}
override fun provideRecipeInputs(finder: RecipeMatcher) {
TODO()
}
}
}

View File

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

View File

@ -0,0 +1,62 @@
package net.shadowfacts.phycon.block.terminal
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.EnumHelper
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.button.AbstractButton
import net.shadowfacts.phycon.PhysicalConnectivityClient
import net.shadowfacts.phycon.api.TerminalSetting
import net.shadowfacts.phycon.util.TerminalSettings
import java.util.EnumMap
/**
* @author shadowfacts
*/
class SettingButton<E>(
val key: TerminalSettings.SettingKey<E>,
): AbstractButton<SettingButton<E>>(
TextureView(null).apply {
intrinsicContentSize = Size(16.0, 16.0)
},
padding = 2.0
) where E: Enum<E>, E: TerminalSetting {
private val textureCache = EnumMap<E, Texture>(key.clazz)
private val textureView: TextureView
get() = content as TextureView
init {
update()
}
private fun update() {
textureView.texture = textureCache.getOrPut(key.value) {
val uv = key.value.uv
Texture(key.value.iconTexture, uv[0], uv[1])
}
tooltip = key.value.tooltip
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
val newValue = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(key.value)
MouseButton.RIGHT -> EnumHelper.previous(key.value)
else -> {
return false
}
}
PhysicalConnectivityClient.terminalSettings[key] = newValue
update()
}
return super.mouseClicked(point, mouseButton)
}
}

View File

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

View File

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

View File

@ -3,8 +3,8 @@ package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.minecraft.inventory.SimpleInventory
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.IntArrayTag
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtIntArray
import net.shadowfacts.phycon.util.fromTag
import net.shadowfacts.phycon.util.toTag
import kotlin.math.min
@ -21,25 +21,25 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) {
var modes = Array(size) { Mode.UNASSIGNED }
private set
fun toTag(): CompoundTag {
val compound = CompoundTag()
fun toTag(): NbtCompound {
val compound = NbtCompound()
compound.put("Inventory", (this as SimpleInventory).toTag())
compound.put("Modes", IntArrayTag(modes.map(Mode::ordinal)))
compound.put("Modes", NbtIntArray(modes.map(Mode::ordinal)))
return compound
}
fun fromTag(tag: CompoundTag) {
fun fromTag(tag: NbtCompound) {
val inventory = tag.getList("Inventory", 10)
(this as SimpleInventory).fromTag(inventory)
tag.getIntArray("Modes").forEachIndexed { i, it -> modes[i] = Mode.values()[it] }
}
fun insertFromNetwork(stack: ItemStack): ItemStack {
fun insert(stack: ItemStack, mode: Mode): ItemStack {
var remaining = stack.copy()
for (slot in 0 until size()) {
if (modes[slot] == Mode.TO_NETWORK) continue
if (modes[slot] != mode && modes[slot] != Mode.UNASSIGNED) continue
remaining = insertFromNetwork(stack, slot)
remaining = tryInsert(stack, slot, mode)
if (remaining.isEmpty) {
break
@ -48,19 +48,17 @@ class TerminalBufferInventory(size: Int): SimpleInventory(size) {
return remaining
}
private fun insertFromNetwork(stack: ItemStack, slot: Int): ItemStack {
val mode = modes[slot]
if (mode == Mode.TO_NETWORK) return stack
private fun tryInsert(stack: ItemStack, slot: Int, mode: Mode): ItemStack {
val current = getStack(slot)
if (current.isEmpty) {
setStack(slot, stack)
modes[slot] = Mode.FROM_NETWORK
markSlot(slot, mode)
return ItemStack.EMPTY
} else if (ItemStackUtil.areEqualIgnoreAmounts(stack, current)) {
val toTransfer = min(current.maxCount - current.count, stack.count)
current.count += toTransfer
stack.count -= toTransfer
modes[slot] = Mode.FROM_NETWORK
markSlot(slot, mode)
return stack
} else {
return stack

View File

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

View File

@ -0,0 +1,151 @@
package net.shadowfacts.phycon.block.terminal
import net.minecraft.item.ItemStack
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.cacao.view.textfield.NumberField
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.PhysicalConnectivity
import org.lwjgl.glfw.GLFW
import kotlin.math.ceil
import kotlin.math.floor
/**
* @author shadowfacts
*/
class TerminalRequestAmountViewController(
val screen: AbstractTerminalScreen<*, *>,
val stack: ItemStack,
): ViewController() {
companion object {
private val BACKGROUND = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal_amount.png"), 0, 0)
}
lateinit var field: NumberField
private set
override fun viewDidLoad() {
super.viewDidLoad()
val pane = view.addLayoutGuide()
view.solver.dsl {
pane.widthAnchor equalTo 158
pane.heightAnchor equalTo 62
pane.centerXAnchor equalTo view.centerXAnchor
pane.centerYAnchor equalTo view.centerYAnchor
}
val background = view.addSubview(TextureView(BACKGROUND)).apply {
zIndex = -1.0
}
field = view.addSubview(AmountField(this)).apply {
drawBackground = false
}
field.becomeFirstResponder()
val requestLabel = Label("Request", shadow = true)
val request = view.addSubview(Button(requestLabel) {
doRequest()
})
val plusOneLabel = Label("+1", shadow = true)
val plusOne = view.addSubview(Button(plusOneLabel) {
field.number = (field.number ?: 1) + 1
})
val plusTenLabel = Label("+10", shadow = true)
val plusTen = view.addSubview(Button(plusTenLabel) {
val old = field.number ?: 1
field.number = ceil((old + 1) / 10.0).toInt() * 10
})
val plusHundredLabel = Label("+100", shadow = true)
val plusHundred = view.addSubview(Button(plusHundredLabel) {
val old = field.number ?: 1
field.number = ceil((old + 1) / 100.0).toInt() * 100
})
val minusOneLabel = Label("-1", shadow = true)
val minusOne = view.addSubview(Button(minusOneLabel) {
field.number = (field.number ?: 1) - 1
})
val minusTenLabel = Label("-10", shadow = true)
val minusTen = view.addSubview(Button(minusTenLabel) {
val old = field.number ?: 1
field.number = floor((old - 1) / 10.0).toInt() * 10
})
val minusHundredLabel = Label("-100", shadow = true)
val minusHundred = view.addSubview(Button(minusHundredLabel) {
val old = field.number ?: 1
field.number = floor((old - 1) / 100.0).toInt() * 100
})
view.solver.dsl {
background.leftAnchor equalTo pane.leftAnchor
background.rightAnchor equalTo pane.rightAnchor
background.topAnchor equalTo pane.topAnchor
background.bottomAnchor equalTo pane.bottomAnchor
field.leftAnchor equalTo (pane.leftAnchor + 8)
field.topAnchor equalTo (pane.topAnchor + 27)
field.widthAnchor equalTo 80
field.heightAnchor equalTo 9
request.leftAnchor equalTo (pane.leftAnchor + 101)
request.centerYAnchor equalTo field.centerYAnchor
request.widthAnchor equalTo 50
request.heightAnchor equalTo 20
plusOne.leftAnchor equalTo (pane.leftAnchor + 7)
plusTen.leftAnchor equalTo (plusOne.rightAnchor + 3)
plusHundred.leftAnchor equalTo (plusTen.rightAnchor + 3)
plusHundred.rightAnchor equalTo (pane.leftAnchor + 97)
plusOne.widthAnchor equalTo plusTen.widthAnchor
plusOne.widthAnchor equalTo plusHundred.widthAnchor
plusOne.topAnchor equalTo (pane.topAnchor + 7)
plusTen.topAnchor equalTo (pane.topAnchor + 7)
plusHundred.topAnchor equalTo (pane.topAnchor + 7)
plusOne.heightAnchor equalTo 14
plusTen.heightAnchor equalTo 14
plusHundred.heightAnchor equalTo 14
minusOne.leftAnchor equalTo (pane.leftAnchor + 7)
minusTen.leftAnchor equalTo (minusOne.rightAnchor + 3)
minusHundred.leftAnchor equalTo (minusTen.rightAnchor + 3)
minusHundred.rightAnchor equalTo (pane.leftAnchor + 97)
minusOne.widthAnchor equalTo minusTen.widthAnchor
minusOne.widthAnchor equalTo minusHundred.widthAnchor
minusOne.topAnchor equalTo (pane.topAnchor + 41)
minusTen.topAnchor equalTo (pane.topAnchor + 41)
minusHundred.topAnchor equalTo (pane.topAnchor + 41)
minusOne.heightAnchor equalTo 14
minusTen.heightAnchor equalTo 14
minusHundred.heightAnchor equalTo 14
}
}
private fun doRequest() {
screen.requestItem(stack, field.number ?: 1)
window!!.removeFromScreen()
screen.amountVC = null
}
class AmountField(val vc: TerminalRequestAmountViewController): NumberField(1) {
override fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return if (keyCode == GLFW.GLFW_KEY_ENTER) {
vc.doRequest()
true
} else {
super.keyPressed(keyCode, modifiers)
}
}
}
}

View File

@ -1,495 +1,33 @@
package net.shadowfacts.phycon.block.terminal
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.gui.widget.AbstractButtonWidget
import net.minecraft.client.gui.widget.AbstractPressableButtonWidget
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.text.TranslatableText
import net.minecraft.util.Identifier
import net.minecraft.util.math.MathHelper
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.next
import net.shadowfacts.phycon.util.prev
import org.lwjgl.glfw.GLFW
import java.lang.NumberFormatException
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.min
import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
// todo: translate title
class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): HandledScreen<TerminalScreenHandler>(handler, playerInv, title) {
class TerminalScreen(
handler: TerminalScreenHandler,
playerInv: PlayerInventory,
title: Text,
): AbstractTerminalScreen<TerminalBlockEntity, TerminalScreenHandler>(
handler,
playerInv,
title,
252,
222
) {
companion object {
private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png")
private val DIALOG = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal_amount.png")
}
private lateinit var searchBox: TextFieldWidget
private lateinit var sortButton: SortButton
var sortButtonX: Int = 0
private set
var sortButtonY = 0
private set
private lateinit var amountBox: TextFieldWidget
private var dialogStack = ItemStack.EMPTY
private var showingAmountDialog = false
set(value) {
val oldValue = field
field = value
for (e in dialogChildren) {
e.visible = value
}
amountBox.isVisible
searchBox.setSelected(!value)
amountBox.setSelected(value)
if (value && !oldValue) {
amountBox.text = "1"
}
updateFocusedElement()
}
private var dialogChildren = mutableListOf<AbstractButtonWidget>()
override val backgroundTexture = BACKGROUND
private var scrollPosition = 0f
private var isDraggingScrollThumb = false
private val trackMinY = 18
private val trackHeight = 106
private val thumbHeight = 15
private val thumbWidth = 12
private val scrollThumbTop: Int
get() = trackMinY + (scrollPosition * (trackHeight - thumbHeight)).roundToInt()
private val dialogWidth = 158
private val dialogHeight = 62
init {
backgroundWidth = 252
backgroundHeight = 222
}
override fun init() {
super.init()
children.clear()
dialogChildren.clear()
client!!.keyboard.setRepeatEvents(true)
searchBox = TextFieldWidget(textRenderer, x + 138, y + 6, 80, 9, LiteralText("Search"))
searchBox.setMaxLength(50)
// setHasBorder is actually setDrawsBackground
searchBox.setHasBorder(false)
searchBox.isVisible = true
searchBox.setSelected(true)
searchBox.setEditableColor(0xffffff)
addChild(searchBox)
sortButtonX = x + 256
sortButtonY = y
sortButton = SortButton(sortButtonX, sortButtonY, handler.sortMode, {
requestUpdatedItems()
}, ::renderTooltip)
addButton(sortButton)
val dialogMinX = width / 2 - dialogWidth / 2
val dialogMinY = height / 2 - dialogHeight / 2
amountBox = TextFieldWidget(textRenderer, dialogMinX + 8, dialogMinY + 27, 80, 9, LiteralText("Amount"))
amountBox.setHasBorder(false)
amountBox.isVisible = false
amountBox.setSelected(false)
amountBox.setEditableColor(0xffffff)
amountBox.setTextPredicate {
if (it.isEmpty()) {
true
} else {
try {
Integer.parseInt(it) > 0
} catch (e: NumberFormatException) {
false
}
}
}
dialogChildren.add(amountBox)
val plusOne = SmallButton(dialogMinX + 7, dialogMinY + 7, 28, LiteralText("+1")) {
amountBox.intValue += 1
}
dialogChildren.add(plusOne)
val plusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 7, 28, LiteralText("+10")) {
amountBox.intValue = ceil((amountBox.intValue + 1) / 10.0).toInt() * 10
}
dialogChildren.add(plusTen)
val plusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 7, 28, LiteralText("+100")) {
amountBox.intValue = ceil((amountBox.intValue + 1) / 100.0).toInt() * 100
}
dialogChildren.add(plusHundred)
val minusOne = SmallButton(dialogMinX + 7, dialogMinY + 39, 28, LiteralText("-1")) {
amountBox.intValue -= 1
}
dialogChildren.add(minusOne)
val minusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 39, 28, LiteralText("-10")) {
amountBox.intValue = floor((amountBox.intValue - 1) / 10.0).toInt() * 10
}
dialogChildren.add(minusTen)
val minusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 39, 28, LiteralText("-100")) {
amountBox.intValue = floor((amountBox.intValue - 1) / 100.0).toInt() * 100
}
dialogChildren.add(minusHundred)
// 101,25
val request = ButtonWidget(dialogMinX + 101, dialogMinY + 21, 50, 20, LiteralText("Request")) {
doDialogRequest()
}
dialogChildren.add(request)
updateFocusedElement()
requestUpdatedItems()
}
private fun updateFocusedElement() {
focused = if (showingAmountDialog) {
amountBox
} else if (searchBox.isFocused) {
searchBox
} else {
null
}
}
private fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchBox.text, sortButton.mode, scrollPosition))
}
override fun tick() {
super.tick()
searchBox.tick()
amountBox.tick()
}
override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
textRenderer.draw(matrixStack, title, 65f, 6f, 0x404040)
textRenderer.draw(matrixStack, playerInventory.displayName, 65f, backgroundHeight - 94f, 0x404040)
textRenderer.draw(matrixStack, TranslatableText("gui.phycon.terminal_buffer"), 7f, 6f, 0x404040)
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
// if the dialog is open, the background gradient will be drawn in front of the main terminal gui
if (!showingAmountDialog) {
renderBackground(matrixStack)
}
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(BACKGROUND)
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight)
// scroll thumb
drawTexture(matrixStack, x + 232, y + scrollThumbTop, 52, 230, thumbWidth, thumbHeight)
}
@ExperimentalUnsignedTypes
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
if (showingAmountDialog) {
RenderSystem.pushMatrix()
// items are rendered at some stupidly high z offset. item amounts at an even higher one
RenderSystem.translatef(0f, 0f, -350f)
// fake the mouse x/y while showing a dialog so slot mouseover highlights aren't drawn
super.render(matrixStack, -1, -1, delta)
RenderSystem.popMatrix()
} else {
super.render(matrixStack, mouseX, mouseY, delta)
}
searchBox.render(matrixStack, mouseX, mouseY, delta)
if (showingAmountDialog) {
renderBackground(matrixStack)
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(DIALOG)
val dialogMinX = width / 2 - dialogWidth / 2
val dialogMinY = height / 2 - dialogHeight / 2
drawTexture(matrixStack, dialogMinX, dialogMinY, 0, 0, dialogWidth, dialogHeight)
for (e in dialogChildren) {
e.render(matrixStack, mouseX, mouseY, delta)
}
} else {
drawMouseoverTooltip(matrixStack, mouseX, mouseY)
}
}
@ExperimentalUnsignedTypes
fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) {
if (!handler.isBufferSlot(slot.id)) {
return
}
val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart)
val color: UInt = when (mode) {
TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u
TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u
else -> return
}
DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt())
}
private fun isPointInsScrollThumb(mouseX: Double, mouseY: Double): Boolean {
val x = (width - backgroundWidth) / 2
val y = (height - backgroundHeight) / 2
val thumbMinX = x + 232
val thumbMaxX = thumbMinX + thumbWidth
val thumbMinY = y + scrollThumbTop
val thumbMaxY = thumbMinY + thumbHeight
return mouseX >= thumbMinX && mouseX < thumbMaxX && mouseY >= thumbMinY && mouseY < thumbMaxY
}
override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) {
super.onMouseClick(slot, invSlot, clickData, type)
updateFocusedElement()
if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) {
val stack = slot.stack
if (type == SlotActionType.QUICK_MOVE) {
// shift click, request full stack
requestItem(stack, min(stack.count, stack.maxCount))
} else if (type == SlotActionType.PICKUP) {
if (clickData == 1) {
// right click, request half stack
requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt())
} else {
dialogStack = stack
showingAmountDialog = true
searchBox.setSelected(false)
}
}
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (showingAmountDialog) {
for (e in dialogChildren) {
if (e.mouseClicked(mouseX, mouseY, button)) {
return true
}
}
return false
} else {
if (isPointInsScrollThumb(mouseX, mouseY)) {
isDraggingScrollThumb = true
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (showingAmountDialog) {
return false
} else if (isDraggingScrollThumb) {
scrollPosition = (mouseY.toFloat() - (y + trackMinY) - 7.5f) / (trackHeight - 15)
scrollPosition = MathHelper.clamp(scrollPosition, 0f, 1f)
requestUpdatedItems()
return true
} else {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
}
override fun mouseMoved(d: Double, e: Double) {
if (showingAmountDialog) {
} else {
super.mouseMoved(d, e)
}
}
override fun mouseReleased(d: Double, e: Double, i: Int): Boolean {
if (showingAmountDialog) {
return false
} else {
isDraggingScrollThumb = false
return super.mouseReleased(d, e, i)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
if (showingAmountDialog) {
return false
} else {
var newOffsetInRows = handler.currentScrollOffsetInRows() - amount.toInt()
newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, handler.maxScrollOffsetInRows())
val newScrollPosition = newOffsetInRows / handler.maxScrollOffsetInRows().toFloat()
scrollPosition = newScrollPosition
requestUpdatedItems()
return super.mouseScrolled(mouseX, mouseY, amount)
}
}
override fun charTyped(c: Char, i: Int): Boolean {
if (showingAmountDialog) {
return amountBox.charTyped(c, i)
} else {
val oldText = searchBox.text
if (searchBox.charTyped(c, i)) {
if (searchBox.text != oldText) {
scrollPosition = 0f
requestUpdatedItems()
}
return true
}
return super.charTyped(c, i)
}
}
override fun keyPressed(key: Int, j: Int, k: Int): Boolean {
if (showingAmountDialog) {
return when (key) {
GLFW.GLFW_KEY_ESCAPE -> {
showingAmountDialog = false
true
}
GLFW.GLFW_KEY_ENTER -> {
doDialogRequest()
true
}
else -> {
amountBox.keyPressed(key, j, k)
}
}
} else {
val oldText = searchBox.text
if (searchBox.keyPressed(key, j, k)) {
if (searchBox.text != oldText) {
scrollPosition = 0f
requestUpdatedItems()
}
return true
}
return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) {
true
} else {
super.keyPressed(key, j, k)
}
}
}
private fun doDialogRequest() {
showingAmountDialog = false
requestItem(dialogStack, amountBox.intValue)
}
private fun requestItem(stack: ItemStack, amount: Int) {
val netHandler = MinecraftClient.getInstance().player!!.networkHandler
val packet = C2STerminalRequestItem(handler.terminal, stack, amount)
netHandler.sendPacket(packet)
}
private var TextFieldWidget.intValue: Int
get() = if (text.isEmpty()) 0 else Integer.parseInt(text)
set(value) {
text = value.toString()
setSelected(true)
}
class SmallButton(x: Int, y: Int, width: Int, title: Text, action: PressAction): ButtonWidget(x, y, width, 14, title, action) {
@ExperimentalUnsignedTypes
override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val client = MinecraftClient.getInstance()
client.textureManager.bindTexture(DIALOG)
RenderSystem.color4f(1f, 1f, 1f, 1f)
val v = if (isHovered) 142 else 128
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.enableDepthTest()
drawTexture(matrixStack, x, y, 0, v, width / 2, height)
drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, v, width / 2, height)
drawCenteredText(matrixStack, client.textRenderer, message, x + width / 2, y + (height - 8) / 2, 0xffffffffu.toInt())
}
}
class SortButton(
x: Int,
y: Int,
var mode: SortMode,
val onChange: (SortMode) -> Unit,
val doRenderTooltip: (MatrixStack, Text, Int, Int) -> Unit
): AbstractPressableButtonWidget(x, y, 20, 20, LiteralText("")) {
override fun onPress() {}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if ((button == 0 || button == 1) && clicked(mouseX, mouseY)) {
val newVal = if (button == 0) mode.next else mode.prev
mode = newVal
onChange(mode)
playDownSound(MinecraftClient.getInstance().soundManager)
return true
}
return false
}
override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
val client = MinecraftClient.getInstance()
RenderSystem.color4f(1f, 1f, 1f, 1f)
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.enableDepthTest()
client.textureManager.bindTexture(WIDGETS_LOCATION)
val k = getYImage(isHovered)
drawTexture(matrixStack, x, y, 0, 46 + k * 20, width / 2, height)
drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, 46 + k * 20, width / 2, height)
client.textureManager.bindTexture(BACKGROUND)
val u: Int = when (mode) {
SortMode.COUNT_HIGH_FIRST -> 0
SortMode.COUNT_LOW_FIRST -> 16
SortMode.ALPHABETICAL -> 32
}
drawTexture(matrixStack, x + 2, y + 2, u, 230, 16, 16)
if (isHovered) {
renderToolTip(matrixStack, mouseX, mouseY)
}
}
override fun renderToolTip(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
val text = LiteralText("")
text.append("Sort by: ")
text.append(mode.tooltip)
doRenderTooltip(matrixStack, text, mouseX, mouseY)
}
override fun createViewController(): AbstractTerminalViewController<*, *, *> {
return TerminalViewController(this, handler)
}
}

View File

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

View File

@ -0,0 +1,13 @@
package net.shadowfacts.phycon.block.terminal
/**
* @author shadowfacts
*/
class TerminalViewController(
screen: TerminalScreen,
handler: TerminalScreenHandler,
): AbstractTerminalViewController<TerminalBlockEntity, TerminalScreen, TerminalScreenHandler>(
screen,
handler,
) {
}

View File

@ -0,0 +1,29 @@
package net.shadowfacts.phycon.client
import net.fabricmc.fabric.api.client.model.ModelProviderContext
import net.fabricmc.fabric.api.client.model.ModelResourceProvider
import net.minecraft.client.render.model.UnbakedModel
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.client.model.ScreenDeviceModel
/**
* @author shadowfacts
*/
class PhyExtendedModelProvider(resourceManager: ResourceManager): ModelResourceProvider {
companion object {
val TERMINAL = Identifier(PhysicalConnectivity.MODID, "block/terminal")
val CRAFTING_TERMINAL = Identifier(PhysicalConnectivity.MODID, "block/crafting_terminal")
}
override fun loadModelResource(resourceId: Identifier, context: ModelProviderContext): UnbakedModel? {
return when (resourceId) {
TERMINAL -> ScreenDeviceModel(TERMINAL)
CRAFTING_TERMINAL -> ScreenDeviceModel(CRAFTING_TERMINAL)
else -> null
}
}
}

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