Compare commits

...

120 Commits
cacao ... 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
Shadowfacts 9305b1e0ef
Remove duplicate Gradle configurations from plugin 2021-03-13 11:53:17 -05:00
Shadowfacts 396e811109
Fix excessive network traffic from Extractor 2021-03-13 11:28:50 -05:00
Shadowfacts f56ab5dc7e
Fix enum button label width not updating 2021-03-12 17:15:36 -05:00
Shadowfacts f792513151
Use correct item block for Extractor/Inserter 2021-03-12 17:15:20 -05:00
Shadowfacts 427574928d
Fix face texture being upside down when facing north 2021-03-12 17:14:49 -05:00
Shadowfacts a184060e77
Add Extractor textures 2021-03-12 17:14:24 -05:00
Shadowfacts e99ed35e6b
Add Inserter textures 2021-03-12 17:14:11 -05:00
Shadowfacts 0674b4a56e
Add Switch textures 2021-03-12 17:14:02 -05:00
Shadowfacts 8b24eeb398
Add Interface textures 2021-03-12 17:13:50 -05:00
Shadowfacts 0d629914c4
Add Kat's textures 2021-03-10 23:15:08 -05:00
Shadowfacts f33bb74cfd
Nest REI plugin under PhyCon in ModMenu 2021-03-10 22:47:10 -05:00
Shadowfacts 381a7b96aa
Fix compiling REI plugin 2021-03-10 22:46:33 -05:00
Shadowfacts ae27a53814
Add recipes for Switch, Interface, Terminal, Inserter, Extractor, Miner,
Redstone Controller, Redstone Emitter, and Screwdriver
2021-03-10 18:38:46 -05:00
Shadowfacts 8b1705f0a0
Fix crash when loading Redstone Controller/Emitter from NBT 2021-03-10 18:08:57 -05:00
Shadowfacts c2f1f606f2
Add Silicon Wafer, Item Processor, Redstone Processor 2021-03-10 18:08:23 -05:00
Shadowfacts 2035874baa
Add some advancements 2021-03-09 18:36:55 -05:00
Shadowfacts fab5dcdc35
Don't include extra copies of the Fabric API 2021-03-09 18:36:40 -05:00
Shadowfacts 7170c3482b
Fix Terminal screen not setting focused element
REI expects Screen.getFocused() to be non-null to skip its input
handling
2021-03-09 18:35:57 -05:00
Shadowfacts cd6299b876
Set Console and Screwdriver stack sizes to 1 2021-03-09 18:35:28 -05:00
Shadowfacts 1bbb193183
Add Redstone Emitter digital mode 2021-03-09 18:34:57 -05:00
Shadowfacts e646e1e7c2
Fix Redstone Controller not updating when mode is changed 2021-03-09 18:34:13 -05:00
Shadowfacts b78eb192de
Fix Miner not inserting things 2021-03-09 18:25:59 -05:00
Shadowfacts 815a9ab8af
Setup Gradle multi-project and add REI plugin 2021-03-08 20:53:25 -05:00
Shadowfacts 6f55282537
Add Screen and Serial Cable and Console recipe 2021-03-08 18:32:20 -05:00
Shadowfacts 8100532678
Add Terminal GUI scrolling 2021-03-07 14:02:39 -05:00
Shadowfacts 6e4d1e63a9
Convert Extractor and Inserter to FaceDeviceBlocks 2021-03-07 14:02:24 -05:00
Shadowfacts 73cb991d21
Include Cotton Resources in dev env 2021-03-07 11:09:00 -05:00
Shadowfacts e6faa34355
Allow screwdrivering cables and switches 2021-03-07 10:58:34 -05:00
Shadowfacts 9acceeae3c
Place face devices directly on cables 2021-03-07 10:38:22 -05:00
Shadowfacts eb1aa6cf19
Allow breaking cables by hand 2021-03-07 10:38:09 -05:00
Shadowfacts c9bcfd29d5
Add Twisted Pair and Cable recipe 2021-03-06 20:04:32 -05:00
Shadowfacts 2198b5e10d
Fix weird tab appearance at certain resolutions
There's 1 physical pixel of error creeping in somewhere which was being
displayed below the selected tab. Workaround it by using a tab texture
which has transparent pixels below the bottom of the select tabs.
2021-03-06 19:10:24 -05:00
Shadowfacts 1c405f8623
Tweak terminal placement behavior 2021-03-06 19:09:35 -05:00
Shadowfacts 89e91796a5
Fix not being able to open Redstone Emitter GUI 2021-03-06 15:53:25 -05:00
Shadowfacts 219033476c
Fix crash showing tooltip when mouse was on the edge between two tabs 2021-03-06 15:47:27 -05:00
Shadowfacts 32d87fbd9d
Convert Redstone Controller and Emitter to use FaceDeviceModel 2021-03-06 15:36:06 -05:00
Shadowfacts 72b8435834
Add unconnected interface cable 2021-03-06 15:01:22 -05:00
Shadowfacts 5542f088f9
Add programatic model for interface block 2021-03-06 14:23:41 -05:00
Shadowfacts 7cc96d78ad
Delay packets after switching capacity is reached 2021-03-06 14:09:56 -05:00
Shadowfacts da8b600f31
Fix extractors not extracting 2021-03-04 22:24:40 -05:00
Shadowfacts 74aae99b36
Fix screwdriver duplicating terminal buffered items 2021-03-04 22:15:18 -05:00
Shadowfacts f321b2a06a
Add interface priority syncing 2021-03-04 19:44:31 -05:00
Shadowfacts 385e36918f
Fix weird terminal behavior when inserting multiple equivalent stacks 2021-03-03 23:16:31 -05:00
Shadowfacts 8dbccc541e
Add network stack receiver priority 2021-03-03 23:13:09 -05:00
Shadowfacts 868b0d42f9
Fix screwdrivered block stacking, tweak sound 2021-03-03 22:29:19 -05:00
Shadowfacts 170e50755a
Add network stack provider priorities 2021-03-03 22:00:21 -05:00
Shadowfacts 2d3ac4538d
Drop Terminal items on block break 2021-03-03 19:16:51 -05:00
Shadowfacts 5b314120a7
Add using screwdriver to dismantle devices 2021-03-03 18:43:49 -05:00
Shadowfacts ca090d0924
Add Miner automatic mode 2021-03-03 17:23:57 -05:00
Shadowfacts 2958fa295a
Unify client configuration packets 2021-03-02 22:40:41 -05:00
Shadowfacts 3cd4a7aa0d
Add Redstone Emitter 2021-03-02 22:20:25 -05:00
Shadowfacts 4fe6391e69
Use powered property for Redstone Controller instead of lit 2021-03-01 21:30:36 -05:00
Shadowfacts e2fa31b680
Change screen handler constant names 2021-03-01 21:30:13 -05:00
Shadowfacts 9aa1077977
Add copyWithCount helper 2021-03-01 21:29:14 -05:00
Shadowfacts c976c3f607
Add DeviceBlockEntity common tag helpers 2021-02-28 22:47:54 -05:00
Shadowfacts e4e8dde2fb
Fix inserter screen not drawing world overlay 2021-02-28 22:21:08 -05:00
Shadowfacts ccefb8ae2f
Change Redstone Controller package 2021-02-28 22:03:29 -05:00
Shadowfacts 4c2148b30f
Extract GhostSlot to separate class 2021-02-28 22:02:26 -05:00
Shadowfacts f5268aef51
Fix extractor duplicating items 2021-02-28 19:01:59 -05:00
Shadowfacts 2084a749fb
Add loot tables for everything 2021-02-28 18:56:04 -05:00
Shadowfacts e13154943e
Add Inserter 2021-02-28 18:06:26 -05:00
Shadowfacts 9b4fc548e5
Reorganize code 2021-02-28 13:48:39 -05:00
Shadowfacts a8f0387577
Remove test blocks 2021-02-28 13:45:56 -05:00
489 changed files with 10451 additions and 2617 deletions

BIN
assets/interface.xcf Normal file

Binary file not shown.

BIN
assets/processor base.xcf Normal file

Binary file not shown.

BIN
assets/silicon wafer.xcf Normal file

Binary file not shown.

BIN
assets/small chest icon.xcf Normal file

Binary file not shown.

View File

@ -1,71 +1,116 @@
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"
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
archivesBaseName = project.archives_base_name
version = project.mod_version
group = project.maven_group
minecraft {
allprojects {
pluginManager.withPlugin("java") {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
configure(allprojects.findAll { it.name != "kiwi-java" }) {
pluginManager.withPlugin("fabric-loom") {
dependencies {
//to change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}"
}
}
pluginManager.withPlugin("java") {
processResources {
inputs.property "version", project.version
filesMatching("fabric.mod.json") {
expand "version": project.version
}
}
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// 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 {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
}
}
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
compileKotlin {
kotlinOptions.jvmTarget = JavaVersion.VERSION_17
}
}
}
loom {
log4jConfigs.from "PhyConDebugLogging.xml"
}
repositories {
maven {
url = "https://mod-buildcraft.com/maven"
}
jcenter()
maven { url "https://mod-buildcraft.com/maven" }
// maven { url "https://server.bbkr.space:8081/artifactory/libs-release/" }
maven { url "https://maven.terraformersmc.com/releases" }
maven { url "https://maven.shedaniel.me/" }
maven { url "https://maven.siphalor.de/" }
maven { url "https://jitpack.io" }
mavenCentral()
}
dependencies {
//to change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}"
// PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
// You may need to force-disable transitiveness on them.
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-items:${project.libblockattributes_version}"
shadow project(":kiwi-java")
implementation project(":kiwi-java")
include project(":kiwi-java")
// 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")
// 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"
}
testImplementation "org.junit.jupiter:junit-jupiter:${project.junit_version}"
}
processResources {
inputs.property "version", project.version
filesMatching("fabric.mod.json") {
expand "version": project.version
}
}
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
}
jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}" }
@ -73,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,17 +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
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
}
}

25
plugin/rei/build.gradle Normal file
View File

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

View File

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

@ -0,0 +1,52 @@
{
"schemaVersion": 1,
"id": "phycon_rei",
"version": "${version}",
"name": "PhyCon REI Integration",
"description": "",
"authors": [
"Shadowfacts"
],
"contact": {
"homepage": "https://git.shadowfacts.net/minecraft/PhysicalConnectivity"
},
"license": "LGPL-3.0",
"entrypoints": {
"client": [
{
"adapter": "kotlin",
"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"
}
]
},
"mixins": [
],
"depends": {
"fabricloader": ">=0.4.0",
"fabric": "*",
"fabric-language-kotlin": ">=1.3.50",
"phycon": "*",
"roughlyenoughitems": ["^7.0.0", "^8.0.0"]
},
"custom": {
"modmenu:parent": "phycon"
}
}

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,3 +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.network.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
@ -7,16 +8,19 @@ import net.minecraft.screen.ScreenHandler
import net.minecraft.sound.SoundEvents
import net.minecraft.text.Text
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.KeyModifiers
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.*
/**
* @author shadowfacts
*/
class CacaoHandledScreen<Handler: ScreenHandler>(
open class CacaoHandledScreen<Handler: ScreenHandler>(
handler: Handler,
playerInv: PlayerInventory,
title: Text,
@ -26,10 +30,16 @@ 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)
@ -46,6 +56,15 @@ 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() {
@ -56,25 +75,52 @@ 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) {
}
override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) {
// no-op
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrixStack)
val mouse = Point(mouseX, mouseY)
windows.forEachIndexed { index, it ->
it.draw(matrixStack, mouse, delta)
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)
}
drawMouseoverTooltip(matrixStack, mouseX, mouseY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
@ -90,4 +136,61 @@ class CacaoHandledScreen<Handler: ScreenHandler>(
}
}
}
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)
}
}
override fun charTyped(char: Char, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.charTyped(char, modifiersSet) }) {
return true
}
return super.charTyped(char, modifiers)
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
}

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,15 +167,19 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
return super.charTyped(char, modifiers)
}
private fun findResponder(fn: (Responder) -> Boolean): Boolean {
var responder = windows.lastOrNull()?.firstResponder
while (responder != null) {
if (fn(responder)) {
return true
}
responder = responder.nextResponder
}
override fun shouldCloseOnEsc(): Boolean {
return false
}
}
fun AbstractCacaoScreen.findResponder(fn: (Responder) -> Boolean): Boolean {
var responder = windows.lastOrNull()?.firstResponder
while (responder != null) {
if (fn(responder)) {
return true
}
responder = responder.nextResponder
}
return false
}

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

@ -35,7 +35,7 @@ data class Rect(val left: Double, val top: Double, val width: Double, val height
}
operator fun contains(point: Point): Boolean {
return point.x in left..right && point.y in top..bottom
return point.x >= left && point.x < right && point.y >= top && point.y < bottom
}
}
}

View File

@ -29,6 +29,7 @@ data class Color(val red: Int, val green: Int, val blue: Int, val alpha: Int = 2
val RED = Color(0xff0000)
val GREEN = Color(0x00ff00)
val BLUE = Color(0x0000ff)
val MAGENTA = Color(0xfc46e4)
val TEXT = Color(0x404040)
}

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

@ -10,6 +10,7 @@ import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.RenderHelper
import kotlin.math.min
/**
* A simple View that displays text. Allows for controlling the color and shadow of the text. Label cannot be used
@ -26,10 +27,10 @@ import net.shadowfacts.cacao.util.RenderHelper
*/
class Label(
text: Text,
val shadow: Boolean = false,
var shadow: Boolean = false,
val maxLines: Int = 0,
val wrappingMode: WrappingMode = WrappingMode.WRAP,
val textAlignment: TextAlignment = TextAlignment.LEFT
var textAlignment: TextAlignment = TextAlignment.LEFT
): View() {
companion object {
@ -59,7 +60,8 @@ class Label(
var text: Text = text
set(value) {
field = value
updateIntrinsicContentSize()
// todo: uhhhh
updateIntrinsicContentSize(true)
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
@ -75,15 +77,24 @@ class Label(
override fun wasAdded() {
super.wasAdded()
updateIntrinsicContentSize()
updateIntrinsicContentSize(false)
}
private fun updateIntrinsicContentSize() {
if (RenderHelper.disabled) return
private fun updateIntrinsicContentSize(canWrap: Boolean, isFromDidLayout: Boolean = false): Boolean {
if (RenderHelper.disabled) return false
val width = textRenderer.getWidth(text)
val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble())
val oldSize = intrinsicContentSize
// don't wrap until we've laid out without wrapping to ensure the current bounds reflect the maximum available space
if (wrappingMode == WrappingMode.WRAP && canWrap && hasSolver && isFromDidLayout) {
val lines = textRenderer.wrapLines(text, bounds.width.toInt())
val height = (if (maxLines == 0) lines.size else min(lines.size, maxLines)) * textRenderer.fontHeight
intrinsicContentSize = Size(bounds.width, height.toDouble())
} else {
val width = textRenderer.getWidth(text)
val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble())
}
return oldSize != intrinsicContentSize
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
@ -94,7 +105,7 @@ class Label(
for (i in 0 until lines.size) {
val x = when (textAlignment) {
TextAlignment.LEFT -> 0.0
TextAlignment.CENTER -> (bounds.width + textRenderer.getWidth(lines[i])) / 2
TextAlignment.CENTER -> (bounds.width - textRenderer.getWidth(lines[i])) / 2
TextAlignment.RIGHT -> bounds.width - textRenderer.getWidth(lines[i])
}
val y = i * textRenderer.fontHeight
@ -110,14 +121,22 @@ class Label(
super.didLayout()
computeLines()
if (updateIntrinsicContentSize(true, true)) {
// if the intrinsic content size changes, relayout
window!!.layout()
}
}
private fun computeLines() {
var lines = textRenderer.wrapLines(text, bounds.width.toInt())
if (maxLines > 0 && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
if (wrappingMode == WrappingMode.WRAP) {
var lines = textRenderer.wrapLines(text, bounds.width.toInt())
if (maxLines > 0 && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
}
this.lines = lines
} else {
this.lines = listOf(text.asOrderedText())
}
this.lines = lines
}
}

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

@ -6,6 +6,7 @@ import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.AxisPosition
import net.shadowfacts.cacao.geometry.AxisPosition.*
import no.birkett.kiwi.Constraint
import java.lang.RuntimeException
import java.util.*
/**
@ -18,6 +19,7 @@ import java.util.*
* @param axis The primary axis that this stack lays out its children along.
* @param distribution The mode by which this stack lays out its children along the axis perpendicular to the
* primary [axis].
* @param spacing The distance between arranged subviews along the primary axis.
*/
open class StackView(
val axis: Axis,
@ -25,7 +27,7 @@ open class StackView(
val spacing: Double = 0.0
): View() {
// the internal mutable, list of arranged subviews
// the internal, mutable list of arranged subviews
private val _arrangedSubviews = LinkedList<View>()
/**
* The list of arranged subviews belonging to this stack view.
@ -57,6 +59,64 @@ open class StackView(
return view
}
/**
* Removes the given arranged subview from this stack view's arranged subviews.
*/
fun removeArrangedSubview(view: View) {
val index = arrangedSubviews.indexOf(view)
if (index < 0) {
throw RuntimeException("Cannot remove view that is not arranged subview")
}
if (index == 0) {
solver.removeConstraint(leadingConnection)
val next = arrangedSubviews.getOrNull(1)
if (next != null) {
solver.dsl {
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next)
}
} else {
leadingConnection = null
}
}
if (index == arrangedSubviews.size - 1) {
solver.removeConstraint(trailingConnection)
val prev = arrangedSubviews.getOrNull(arrangedSubviews.size - 2)
if (prev != null) {
solver.dsl {
trailingConnection = anchor(TRAILING) equalTo anchor(TRAILING, prev)
}
} else {
trailingConnection = null
}
}
// if the removed view is in the middle
if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) {
val prev = arrangedSubviews[index - 1]
val next = arrangedSubviews[index + 1]
solver.dsl {
solver.removeConstraint(arrangedSubviewConnections[index - 1])
solver.removeConstraint(arrangedSubviewConnections[index])
// todo: double check me
arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next)
arrangedSubviewConnections.removeAt(index)
}
}
_arrangedSubviews.remove(view)
removeSubview(view)
}
override fun removeSubview(view: View) {
if (arrangedSubviews.contains(view)) {
removeArrangedSubview(view)
} else {
super.removeSubview(view)
}
}
private fun addConstraintsForArrangedView(view: View, index: Int) {
if (index == 0) {
if (leadingConnection != null) {
@ -209,4 +269,4 @@ open class StackView(
*/
FILL
}
}
}

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.
@ -48,6 +48,9 @@ open class View(): Responder {
*/
var solver: Solver by solverDelegate
val hasSolver: Boolean
get() = solverDelegate.isInitialized
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
@ -81,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.
@ -92,17 +106,22 @@ open class View(): Responder {
/**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
* If using constraint based layout, this property is not initialized until [didLayout] called.
* If using constraint based layout, this property has zero dimensions until [didLayout] called.
* Otherwise, this must be set manually.
* Setting this property updates the [bounds].
*/
var frame: Rect by ObservableLateInitProperty { this.bounds = Rect(Point.ORIGIN, it.size) }
var frame = Rect(0.0, 0.0, 0.0, 0.0)
set(value) {
field = value
bounds = Rect(Point.ORIGIN, value.size)
}
/**
* The rectangle for this view in its own coordinate system.
* If using constraint based layout, this property is not initialized until [didLayout] called.
* If using constraint based layout, this property has zero dimensions until [didLayout] called.
* Otherwise, this will be initialized when [frame] is set.
*/
lateinit var bounds: Rect
var bounds = Rect(0.0, 0.0, 0.0, 0.0)
/**
* The position on the Z-axis of this view.
@ -114,7 +133,7 @@ open class View(): Responder {
* The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no
* intrinsic size.
*
* Setting this creates/updates [no.birkett.kiwi.Strength.WEAK] constraints on this view's width/height using
* Setting this creates/updates [no.birkett.kiwi.Strength.MEDIUM] constraints on this view's width/height using
* the size.
*/
var intrinsicContentSize: Size? = null
@ -182,7 +201,7 @@ open class View(): Responder {
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = this
if (solverDelegate.isInitialized) {
if (hasSolver) {
view.solver = solver
}
view.window = window
@ -197,10 +216,13 @@ open class View(): Responder {
* its children (recursively) to a view outside of the subview's hierarchy. Constraints internal to the subview's
* hierarchy (e.g., one between the subview and its child) will be left in place.
*
* This method may be overridden by layout-providing views (such as [StackView]) to update its layout when a managed
* subview is removed.
*
* @param view The view to removed as a child of this view.
* @throws RuntimeException If the given [view] is not a subview of this view.
*/
fun removeSubview(view: View) {
open fun removeSubview(view: View) {
if (view.superview !== this) {
throw RuntimeException("Cannot remove subview whose superview is not this view")
}
@ -218,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
}
@ -236,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.
*/
@ -272,6 +306,10 @@ open class View(): Responder {
open fun wasAdded() {
createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
layoutGuides.forEach {
it.attachTo(solver)
}
}
/**
@ -290,7 +328,7 @@ open class View(): Responder {
}
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!usesConstraintBasedLayout || !solverDelegate.isInitialized) return
if (!usesConstraintBasedLayout || !hasSolver) return
if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
@ -329,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)
@ -341,7 +379,7 @@ open class View(): Responder {
it.draw(matrixStack, mouseInView, delta)
}
RenderHelper.popMatrix()
matrixStack.pop()
}
/**
@ -408,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.
*
@ -452,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

@ -1,25 +1,31 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.View
/**
* A button for toggling between on/off states.
*
* @author shadowfacts
* @param initialState Whether the button starts as on or off.
* @param handler The handler function to invoke when this button is pressed.
*/
class ToggleButton(initialState: Boolean): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF).apply {
intrinsicContentSize = Size(19.0, 19.0)
}, padding = 0.0) {
class ToggleButton(
initialState: Boolean,
handler: ((ToggleButton) -> Unit)? = null,
): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF), padding = 0.0) {
companion object {
val ON = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 0)
val OFF = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 19)
val OFF = Texture(Identifier("textures/gui/checkbox.png"), 0, 0, 64, 64)
val OFF_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 0, 64, 64)
val ON = Texture(Identifier("textures/gui/checkbox.png"), 0, 20, 64, 64)
val ON_HOVERED = Texture(Identifier("textures/gui/checkbox.png"), 20, 20, 64, 64)
}
private val textureView: TextureView
@ -30,10 +36,15 @@ class ToggleButton(initialState: Boolean): AbstractButton<ToggleButton>(TextureV
* Updating this property updates the button's texture.
*/
var state: Boolean = initialState
set(value) {
field = value
textureView.texture = if (value) ON else OFF
}
init {
this.handler = handler
intrinsicContentSize = Size(20.0, 20.0)
background = null
disabledBackground = null
hoveredBackground = null
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
@ -43,4 +54,17 @@ class ToggleButton(initialState: Boolean): AbstractButton<ToggleButton>(TextureV
return super.mouseClicked(point, mouseButton)
}
}
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
val hovered = mouse in bounds
textureView.texture = if (state) {
if (hovered) ON_HOVERED else ON
} else {
if (hovered) OFF_HOVERED else OFF
}
super.draw(matrixStack, mouse, delta)
}
override fun getCurrentBackground(mouse: Point) = null
}

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
@ -62,12 +61,22 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
minecraftWidget.setMaxLength(value)
}
/**
* Whether the Minecraft builtin black background and border are drawn. Defaults to true.
*/
var drawBackground = true
set(value) {
field = value
minecraftWidget.setDrawsBackground(value)
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setDrawsBackground(drawBackground)
}
/**
@ -91,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 {
@ -123,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 {
@ -149,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

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

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

@ -7,6 +7,7 @@ import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
@ -61,6 +62,13 @@ class TabViewController<T: TabViewController.Tab>(
* may be reused or created from scratch each time.
*/
val controller: ViewController
/**
* Used by the tab view controller to determine whether the button for this tab should be displayed.
* If the conditions that control this change, call [TabViewController.visibleTabsChanged].
*/
val isVisible: Boolean
get() = true
}
/**
@ -68,12 +76,17 @@ class TabViewController<T: TabViewController.Tab>(
* @param tabView The view to display on the tab's button.
* @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none).
* @param controller The content view controller for this tab.
* @param visible A function that determines if the tab should currently be visible.
*/
class SimpleTab(
override val tabView: View,
override val tooltip: Text? = null,
override val controller: ViewController,
): Tab
private val visible: (() -> Boolean)? = null
): Tab {
override val isVisible: Boolean
get() = visible?.invoke() ?: true
}
/**
* The currently selected tab.
@ -85,6 +98,7 @@ class TabViewController<T: TabViewController.Tab>(
private lateinit var outerStack: StackView
private lateinit var tabStack: StackView
private lateinit var currentTabController: ViewController
// todo: this shouldn't be public, use layout guides
lateinit var tabVCContainer: View
private set
@ -100,8 +114,57 @@ class TabViewController<T: TabViewController.Tab>(
tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL)
tabStack.zIndex = 1.0
outerStack.addArrangedSubview(tabStack)
updateTabButtons()
val background = NinePatchView(NinePatchTexture.PANEL_BG)
outerStack.addArrangedSubview(background)
tabVCContainer = View()
tabVCContainer.zIndex = 1.0
view.addSubview(tabVCContainer)
currentTabController = currentTab.controller
currentTabController.willMoveTo(this)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
// will/did appear events for the initial VC are provided by this class' implementations of those
view.solver.dsl {
outerStack.leftAnchor equalTo view.leftAnchor
outerStack.rightAnchor equalTo view.rightAnchor
outerStack.topAnchor equalTo view.topAnchor
outerStack.bottomAnchor equalTo view.bottomAnchor
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
}
override fun viewWillAppear() {
super.viewWillAppear()
currentTabController.viewWillAppear()
}
override fun viewWillDisappear() {
super.viewWillDisappear()
currentTabController.viewWillDisappear()
}
override fun viewDidDisappear() {
super.viewDidDisappear()
currentTabController.viewDidDisappear()
}
private fun updateTabButtons() {
while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first())
tabButtons = tabs.mapNotNull { tab ->
if (!tab.isVisible) {
return@mapNotNull null
}
tabButtons = tabs.mapIndexed { index, tab ->
val btn = TabButton(tab)
btn.handler = { selectTab(it.tab) }
if (tab == currentTab) {
@ -115,26 +178,14 @@ class TabViewController<T: TabViewController.Tab>(
// spacer
tabStack.addArrangedSubview(View())
val background = NinePatchView(NinePatchTexture.PANEL_BG)
outerStack.addArrangedSubview(background)
window!!.layout()
}
tabVCContainer = View()
tabVCContainer.zIndex = 1.0
view.addSubview(tabVCContainer)
embedChild(currentTab.controller, tabVCContainer)
view.solver.dsl {
outerStack.leftAnchor equalTo view.leftAnchor
outerStack.rightAnchor equalTo view.rightAnchor
outerStack.topAnchor equalTo view.topAnchor
outerStack.bottomAnchor equalTo view.bottomAnchor
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
/**
* Call this method when the conditions that make the configured tabs visible change.
*/
fun visibleTabsChanged() {
updateTabButtons()
}
/**
@ -156,8 +207,19 @@ class TabViewController<T: TabViewController.Tab>(
tabButtons.forEach {
it.setSelected(it.tab === tab)
}
oldTab.controller.removeFromParent()
embedChild(currentTab.controller, tabVCContainer)
currentTabController.viewWillDisappear()
currentTabController.view.removeFromSuperview()
currentTabController.viewDidDisappear()
currentTabController.willMoveTo(null)
currentTabController.removeFromParent()
currentTabController.didMoveTo(null)
currentTabController = currentTab.controller
currentTabController.willMoveTo(this)
embedChild(currentTabController, tabVCContainer)
currentTabController.didMoveTo(this)
currentTabController.viewWillAppear()
onTabChange?.invoke(currentTab)
@ -172,7 +234,7 @@ class TabViewController<T: TabViewController.Tab>(
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("textures/gui/container/creative_inventory/tabs.png")
val BACKGROUND = Identifier("phycon:textures/gui/tabs.png")
}
private var selected = false
@ -226,7 +288,7 @@ class TabViewController<T: TabViewController.Tab>(
val u = when {
superview == null -> 0
frame.left == 0.0 -> 0
frame.right == superview!!.bounds.right -> 140
frame.right == superview!!.bounds.right -> 56
else -> 28
}
backgroundView.texture = Texture(BACKGROUND, u, v)

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,10 +28,16 @@ object PhysicalConnectivity: ModInitializer {
PhyItems.init()
PhyScreens.init()
registerGlobalReceiver(C2SConfigureDevice)
registerGlobalReceiver(C2STerminalCraftingButton)
registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
registerGlobalReceiver(C2SConfigureActivationMode)
registerGlobalReceiver(C2SConfigureRedstoneController)
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

@ -1,25 +1,49 @@
package net.shadowfacts.phycon
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap
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.minecraft.client.render.RenderLayer
import net.shadowfacts.phycon.init.PhyBlocks
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.network.block.terminal.TerminalScreen
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 {
override fun onInitializeClient() {
BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent())
val terminalSettings = TerminalSettings()
ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen)
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)
registerGlobalReceiver(S2CTerminalUpdateDisplayedItems)
}

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.network
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
@ -8,7 +11,6 @@ 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.BlockWithEntity
/**
* @author shadowfacts
@ -26,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,33 +1,34 @@
package net.shadowfacts.phycon.network
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.network.frame.ARPQueryFrame
import net.shadowfacts.phycon.network.frame.ARPResponseFrame
import net.shadowfacts.phycon.network.frame.BasePacketFrame
import net.shadowfacts.phycon.network.packet.*
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 {
@ -44,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
@ -58,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)
}
@ -71,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)
@ -103,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))
@ -117,14 +128,29 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
}
open fun findDestination(): Interface? {
val sides = (cachedState.block as NetworkComponentBlock).getNetworkConnectedSides(cachedState, world!!, pos)
if (sides.size != 1) {
throw RuntimeException("DeviceBlockEntity.findDestination must be overridden by devices which have more than 1 network connected side")
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())?.also {
this.cachedDestination = WeakReference(it)
}
}
else -> throw RuntimeException("DeviceBlockEntity.findDestination must be overridden by devices which have more than 1 network connected side")
}
return NetworkUtil.findConnectedInterface(world!!, pos, sides.first())
}
override fun tick() {
override fun cableDisconnected() {
cachedDestination = null
handleNetworkSplit()
}
open fun tick() {
counter++
if (!world!!.isClient) {
@ -146,27 +172,50 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type),
}
}
override fun toTag(tag: CompoundTag): CompoundTag {
protected open fun toCommonTag(tag: NbtCompound) {
tag.putInt("IPAddress", ipAddress.address)
tag.putLong("MACAddress", macAddress.address)
return super.toTag(tag)
}
override fun fromTag(state: BlockState, tag: CompoundTag) {
super.fromTag(state, tag)
protected open fun fromCommonTag(tag: NbtCompound) {
ipAddress = IPAddress(tag.getInt("IPAddress"))
macAddress = MACAddress(tag.getLong("MACAddress"))
}
override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.putInt("IPAddress", ipAddress.address)
tag.putLong("MACAddress", macAddress.address)
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
toCommonTag(tag)
}
override fun readNbt(tag: NbtCompound) {
super.readNbt(tag)
fromCommonTag(tag)
if (tag.getBoolean("_SyncPacket")) {
fromClientTag(tag)
}
}
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) {
ipAddress = IPAddress(tag.getInt("IPAddress"))
macAddress = MACAddress(tag.getLong("MACAddress"))
open fun fromClientTag(tag: NbtCompound) {
}
fun markUpdate() {
markDirty()
world!!.updateListeners(pos, cachedState, cachedState, 3)
}
fun onBreak() {

View File

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

@ -1,12 +1,15 @@
package net.shadowfacts.phycon.network.block.cable
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
@ -22,20 +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(
Settings.of(CABLE_MATERIAL)
class CableBlock(
val color: DyeColor,
): Block(
FabricBlockSettings.of(CABLE_MATERIAL)
.strength(0.3f)
.nonOpaque()
), NetworkCableBlock {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "cable")
val CABLE_MATERIAL = Material(MaterialColor.IRON, false, false, true, false, true, false, PistonBehavior.NORMAL)
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),
@ -45,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]
}
}
@ -84,12 +100,16 @@ class CableBlock: Block(
}
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
fun getInitialState(world: World, pos: BlockPos): BlockState {
return CONNECTIONS.entries.fold(defaultState, { acc, (dir, prop) ->
acc.with(prop, getConnectionStateInDirection(context.world, context.blockPos, dir))
acc.with(prop, getConnectionStateInDirection(world, pos, dir))
})
}
override fun getPlacementState(context: ItemPlacementContext): BlockState {
return getInitialState(context.world, context.blockPos)
}
override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, blockPos_1: BlockPos, blockPos_2: BlockPos): BlockState {
val prop = CONNECTIONS[side]
val current = state[prop]
@ -103,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
}
}
@ -138,8 +156,7 @@ class CableBlock: Block(
if (player.getStackInHand(hand).item == PhyItems.SCREWDRIVER) {
val hitPos = Vec3d(hitResult.pos.x - pos.x, hitResult.pos.y - pos.y, hitResult.pos.z - pos.z)
val hitConnection = SIDE_SHAPES.entries.firstOrNull { (_, shape) ->
val box = shape.boundingBox
hitPos.x >= box.minX && hitPos.x <= box.maxX && hitPos.y >= box.minY && hitPos.y <= box.maxY && hitPos.z >= box.minZ && hitPos.z <= box.maxZ
shape.boundingBox.containsInclusive(hitPos)
}
if (hitConnection != null) {
val side = hitConnection.key
@ -166,6 +183,10 @@ class CableBlock: Block(
return ActionResult.PASS
}
override fun canReplace(state: BlockState, context: ItemPlacementContext): Boolean {
return context.stack.item is FaceDeviceBlockItem
}
override fun isTranslucent(blockState_1: BlockState?, blockView_1: BlockView?, blockPos_1: BlockPos?): Boolean {
return true
}
@ -178,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

@ -1,14 +1,11 @@
package net.shadowfacts.phycon.network.block.extractor
package net.shadowfacts.phycon.block.extractor
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.block.ShapeContext
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
@ -16,20 +13,22 @@ 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.PhysicalConnectivity
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.network.DeviceBlock
import java.util.*
import net.shadowfacts.phycon.block.FaceDeviceBlock
import kotlin.math.max
import kotlin.math.min
/**
* @author shadowfacts
*/
class ExtractorBlock: DeviceBlock<ExtractorBlockEntity>(Settings.of(Material.METAL)) {
class ExtractorBlock: FaceDeviceBlock<ExtractorBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "extractor")
val FACING = Properties.FACING
private val EXTRACTOR_SHAPES = mutableMapOf<Direction, VoxelShape>()
init {
@ -37,7 +36,6 @@ class ExtractorBlock: DeviceBlock<ExtractorBlockEntity>(Settings.of(Material.MET
doubleArrayOf(0.0, 0.0, 0.0, 16.0, 2.0, 16.0),
doubleArrayOf(2.0, 2.0, 2.0, 14.0, 4.0, 14.0),
doubleArrayOf(4.0, 4.0, 4.0, 12.0, 6.0, 12.0),
doubleArrayOf(6.0, 6.0, 6.0, 10.0, 16.0, 10.0)
)
val directions = arrayOf(
Triple(Direction.DOWN, null, false),
@ -61,40 +59,17 @@ class ExtractorBlock: DeviceBlock<ExtractorBlockEntity>(Settings.of(Material.MET
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) }
}
}
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.of(state[FACING].opposite)
}
override val faceThickness = 6.0
override val faceShapes: Map<Direction, VoxelShape> = EXTRACTOR_SHAPES
override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? {
return if (side == state[FACING].opposite) {
getBlockEntity(world, pos)
} else {
null
}
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(FACING)
}
override fun createBlockEntity(world: BlockView) = ExtractorBlockEntity()
override fun getPlacementState(context: ItemPlacementContext): BlockState {
val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite
return defaultState.with(FACING, facing)
}
override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape {
return EXTRACTOR_SHAPES[state[FACING]]!!
}
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) {
@ -108,4 +83,4 @@ class ExtractorBlock: DeviceBlock<ExtractorBlockEntity>(Settings.of(Material.MET
}
}
}
}

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.network.block.extractor
package net.shadowfacts.phycon.block.extractor
import alexiil.mc.lib.attributes.SearchOptions
import alexiil.mc.lib.attributes.Simulation
@ -7,37 +7,42 @@ 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
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.component.ActivationController
import net.shadowfacts.phycon.network.component.NetworkStackDispatcher
import net.shadowfacts.phycon.network.component.handleItemStack
import net.shadowfacts.phycon.network.packet.CapacityPacket
import net.shadowfacts.phycon.network.packet.ItemStackPacket
import net.shadowfacts.phycon.network.packet.RemoteActivationPacket
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.FaceDeviceBlock
import net.shadowfacts.phycon.component.ActivationController
import net.shadowfacts.phycon.component.NetworkStackDispatcher
import net.shadowfacts.phycon.component.finishTimedOutPendingInsertions
import net.shadowfacts.phycon.component.handleItemStack
import net.shadowfacts.phycon.packet.CapacityPacket
import net.shadowfacts.phycon.packet.ItemStackPacket
import net.shadowfacts.phycon.packet.RemoteActivationPacket
import net.shadowfacts.phycon.util.ActivationMode
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import 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 {
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
companion object {
val SLEEP_TIME = 40L
}
private val facing: Direction
get() = cachedState[ExtractorBlock.FACING]
get() = cachedState[FaceDeviceBlock.FACING]
private var inventory: FixedItemInv? = null
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = 40L
override val dispatchStackTimeout = 1L
override val controller = ActivationController(SLEEP_TIME, this)
fun updateInventory() {
@ -82,6 +87,8 @@ class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR),
if (!world!!.isClient) {
controller.tick()
finishTimedOutPendingInsertions()
}
}
@ -90,7 +97,9 @@ class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR),
for (slot in 0 until inventory.slotCount) {
val slotStack = inventory.getInvStack(slot)
if (slotStack.isEmpty) continue
dispatchItemStack(slotStack) { insertion ->
val extractable = inventory.extractStack(slot, ExactItemStackFilter(slotStack), ItemStack.EMPTY, slotStack.count, Simulation.SIMULATE)
if (extractable.isEmpty) continue
dispatchItemStack(extractable) { insertion ->
insertion.inventory = inventory
insertion.inventorySlot = slot
}
@ -99,23 +108,21 @@ class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR),
return false
}
override fun toTag(tag: CompoundTag): CompoundTag {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("ActivationMode", controller.activationMode.name)
return super.toTag(tag)
}
override fun fromTag(state: BlockState, tag: CompoundTag) {
super.fromTag(state, tag)
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
}
override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.putString("ActivationMode", controller.activationMode.name)
return super.toClientTag(tag)
}
override fun fromClientTag(tag: CompoundTag) {
super.fromClientTag(tag)
override fun loadDeviceConfiguration(tag: NbtCompound) {
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.network.block.miner
package net.shadowfacts.phycon.block.miner
import net.minecraft.block.Block
import net.minecraft.block.BlockState
@ -6,6 +6,7 @@ import net.minecraft.block.Material
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.item.ItemStack
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.Identifier
@ -16,14 +17,17 @@ import net.minecraft.world.World
import net.minecraft.world.WorldAccess
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.Interface
import net.shadowfacts.phycon.network.DeviceBlock
import net.shadowfacts.phycon.network.block.extractor.ExtractorBlock
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.*
/**
* @author shadowfacts
*/
class MinerBlock: DeviceBlock<MinerBlockEntity>(Settings.of(Material.METAL)) {
class MinerBlock: DeviceBlock<MinerBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "miner")
@ -47,11 +51,11 @@ class MinerBlock: DeviceBlock<MinerBlockEntity>(Settings.of(Material.METAL)) {
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
return defaultState.with(ExtractorBlock.FACING, facing)
return defaultState.with(FACING, facing)
}
override fun onPlaced(world: World, pos: BlockPos, state: BlockState, entity: LivingEntity?, itemStack: ItemStack) {
@ -66,4 +70,4 @@ class MinerBlock: DeviceBlock<MinerBlockEntity>(Settings.of(Material.METAL)) {
}
}
}
}

View File

@ -1,4 +1,4 @@
package net.shadowfacts.phycon.network.block.miner
package net.shadowfacts.phycon.block.miner
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackUtil
@ -6,28 +6,32 @@ import alexiil.mc.lib.attributes.item.filter.ItemFilter
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.nbt.NbtCompound
import net.minecraft.server.world.ServerWorld
import net.minecraft.text.TranslatableText
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.World
import net.shadowfacts.phycon.api.packet.Packet
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.init.PhyBlockEntities
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.network.component.NetworkStackDispatcher
import net.shadowfacts.phycon.network.component.NetworkStackProvider
import net.shadowfacts.phycon.network.component.handleItemStack
import net.shadowfacts.phycon.network.component.spawnItemStack
import net.shadowfacts.phycon.network.packet.*
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.terminal.AbstractTerminalBlockEntity
import net.shadowfacts.phycon.component.*
import net.shadowfacts.phycon.packet.*
import net.shadowfacts.phycon.util.ActivationMode
import net.shadowfacts.phycon.util.ClientConfigurableDevice
import net.shadowfacts.phycon.util.copyWithCount
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> {
NetworkStackDispatcher<MinerBlockEntity.PendingInsertion>,
ActivationController.ActivatableDevice,
ClientConfigurableDevice {
private val facing: Direction
get() = cachedState[MinerBlock.FACING]
@ -35,7 +39,11 @@ 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
var minerMode = MinerMode.ON_DEMAND
override fun handle(packet: Packet) {
when (packet) {
@ -44,14 +52,21 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
is ExtractStackPacket -> handleExtractStack(packet)
is CapacityPacket -> handleCapacity(packet)
is ItemStackPacket -> handleItemStack(packet)
is RemoteActivationPacket -> controller.handleRemoteActivation(packet)
}
}
private fun handleRequestInventory(packet: RequestInventoryPacket) {
sendPacket(ReadInventoryPacket(invProxy, ipAddress, packet.source))
if (minerMode != MinerMode.ON_DEMAND || packet.kind != RequestInventoryPacket.Kind.GROUPED) {
return
}
sendPacket(ReadGroupedInventoryPacket(invProxy, ipAddress, packet.source))
}
private fun handleLocateStack(packet: LocateStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
return
}
val amount = invProxy.getAmount(packet.stack)
if (amount > 0) {
sendPacket(StackLocationPacket(packet.stack, amount, this, ipAddress, packet.source))
@ -59,6 +74,10 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
}
private fun handleExtractStack(packet: ExtractStackPacket) {
if (minerMode != MinerMode.ON_DEMAND) {
return
}
// always recalculate immediately before breaking
val drops = invProxy.getDrops(recalculate = true)
if (invProxy.getAmount(packet.stack) > 0) {
@ -73,14 +92,13 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
if (!ItemStackUtil.areEqualIgnoreAmounts(droppedStack, packet.stack)) {
continue
}
val copy = droppedStack.copy()
val toDecr = min(droppedStack.count, remaining)
val copy = droppedStack.copyWithCount(toDecr)
droppedStack.decrement(toDecr)
remaining -= toDecr
// todo: should this try to combine stacks and send as few packets as possible?
copy.count = toDecr
sendPacket(ItemStackPacket(copy, ipAddress, packet.source))
}
@ -99,7 +117,78 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter)
override fun tick() {
super.tick()
if (!world!!.isClient) {
if (minerMode == MinerMode.AUTOMATIC) {
controller.tick()
}
finishTimedOutPendingInsertions()
}
}
override fun activate(): Boolean {
if (minerMode == MinerMode.ON_DEMAND) {
return false
}
val drops = invProxy.getDrops(recalculate = true)
if (!world!!.getBlockState(pos.offset(facing)).isAir) {
world!!.breakBlock(pos.offset(facing), false)
for (stack in drops) {
if (stack.isEmpty) continue
dispatchItemStack(stack)
}
return true
} else {
return false
}
}
override fun canConfigureActivationController(): Boolean {
return minerMode == MinerMode.AUTOMATIC
}
override fun canConfigureProviderPriority(): Boolean {
return minerMode == MinerMode.ON_DEMAND
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putString("MinerMode", minerMode.name)
tag.putString("ActivationMode", controller.activationMode.name)
tag.putInt("ProviderPriority", providerPriority)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
minerMode = MinerMode.valueOf(tag.getString("MinerMode"))
controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode"))
providerPriority = tag.getInt("ProviderPriority")
}
enum class MinerMode {
ON_DEMAND, AUTOMATIC;
val friendlyName = TranslatableText("gui.phycon.miner_mode.${name.lowercase()}")
}
class MinerInvProxy(val miner: MinerBlockEntity): GroupedItemInvView {
companion object {
val TOOL = ItemStack(Items.DIAMOND_PICKAXE)
}
private var cachedState: BlockState? = null
private var cachedDrops: List<ItemStack>? = null
@ -116,13 +205,17 @@ 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
cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be)
val be = if (realState.hasBlockEntity()) world.getBlockEntity(targetPos) else null
cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be, null, TOOL)
}
return cachedDrops!!
}
override fun getStoredStacks(): Set<ItemStack> {
if (miner.minerMode != MinerMode.ON_DEMAND) {
return setOf()
}
return getDrops().toSet()
}
@ -132,7 +225,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic {
var totalCount = 0
for (s in getDrops()) {
for (s in storedStacks) {
if (filter.matches(s)) {
totalCount += s.count
}
@ -144,4 +237,4 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER),
class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion<PendingInsertion>(stack, timestamp) {
}
}
}

View File

@ -1,10 +1,11 @@
package net.shadowfacts.phycon.network.block.netinterface
package net.shadowfacts.phycon.block.netinterface
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.*
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
@ -12,12 +13,16 @@ import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.api.NetworkComponentBlock
import net.shadowfacts.phycon.network.FaceDeviceBlock
import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>(Settings.of(Material.METAL)),
class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
@ -35,7 +40,7 @@ class InterfaceBlock: FaceDeviceBlock<InterfaceBlockEntity>(Settings.of(Material
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

@ -1,47 +1,58 @@
package net.shadowfacts.phycon.network.block.netinterface
package net.shadowfacts.phycon.block.netinterface
import alexiil.mc.lib.attributes.SearchOptions
import alexiil.mc.lib.attributes.Simulation
import alexiil.mc.lib.attributes.item.GroupedItemInv
import alexiil.mc.lib.attributes.item.ItemAttributes
import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack
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
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.FaceDeviceBlock
import net.shadowfacts.phycon.network.component.ItemStackPacketHandler
import net.shadowfacts.phycon.network.component.NetworkStackProvider
import net.shadowfacts.phycon.network.component.NetworkStackReceiver
import net.shadowfacts.phycon.network.component.handleItemStack
import net.shadowfacts.phycon.network.packet.*
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.block.FaceDeviceBlock
import net.shadowfacts.phycon.component.ItemStackPacketHandler
import net.shadowfacts.phycon.component.NetworkStackProvider
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 {
NetworkStackReceiver,
ClientConfigurableDevice {
private val facing: Direction
get() = cachedState[FaceDeviceBlock.FACING]
// todo: should this be a weak ref?
private var inventory: GroupedItemInv? = null
override var providerPriority = 0
override var receiverPriority = 0
var syncPriorities = true
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) {
@ -55,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))
}
}
@ -101,4 +115,26 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE),
}
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("ProviderPriority", providerPriority)
tag.putInt("ReceiverPriority", receiverPriority)
tag.putBoolean("SyncPriorities", syncPriorities)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
providerPriority = tag.getInt("ProviderPriority")
receiverPriority = tag.getInt("ReceiverPriority")
syncPriorities = tag.getBoolean("SyncPriorities")
}
}

View File

@ -1,9 +1,13 @@
package net.shadowfacts.phycon.network.block.netswitch
package net.shadowfacts.phycon.block.netswitch
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.BlockState
import net.minecraft.block.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
import net.minecraft.util.math.Direction
@ -19,7 +23,11 @@ import java.util.*
/**
* @author shadowfacts
*/
class SwitchBlock: BlockWithEntity<SwitchBlockEntity>(Settings.of(Material.METAL)),
class SwitchBlock: BlockWithEntity<SwitchBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
@ -35,7 +43,17 @@ class SwitchBlock: BlockWithEntity<SwitchBlockEntity>(Settings.of(Material.METAL
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

@ -0,0 +1,182 @@
package net.shadowfacts.phycon.block.netswitch
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntity
import net.minecraft.item.ItemStack
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
import net.shadowfacts.phycon.api.frame.EthernetFrame
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
import java.lang.ref.WeakReference
import java.util.Deque
import java.util.LinkedList
/**
* @author shadowfacts
*/
class SwitchBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(PhyBlockEntities.SWITCH, pos, state) {
companion object {
var SWITCHING_CAPACITY = 256 // 256 packets/tick
}
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()
fun interfaceForSide(side: Direction): SwitchInterface {
return interfaces.find { it.side == side }!!
}
private fun handle(frame: EthernetFrame, fromItf: SwitchInterface) {
macTable[frame.source] = fromItf.side
if (frame is PacketFrame) {
if (packetsHandledThisTick > SWITCHING_CAPACITY) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} reached capacity, delaying forwarding {}", this, frame)
delayedPackets.addLast(frame to fromItf)
return
} else {
packetsHandledThisTick++
}
}
resend(frame, fromItf)
}
private fun resend(frame: EthernetFrame, fromItf: SwitchInterface) {
if (frame.destination.type != MACAddress.Type.BROADCAST && macTable.containsKey(frame.destination)) {
val dir = macTable[frame.destination]!!
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) forwarding {} to side {}", this, fromItf.side, fromItf.macAddress, frame, dir)
interfaceForSide(dir).send(frame)
} else {
flood(frame, fromItf)
}
}
private fun flood(frame: EthernetFrame, source: SwitchInterface) {
PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) flooding {}", this, source.side, source.macAddress, frame)
for (itf in interfaces) {
if (source == itf) continue
itf.send(frame)
}
}
private fun findDestination(fromItf: Interface): Interface? {
val side = (fromItf as SwitchInterface).side
return destinationCache[side.ordinal]?.get()
?: NetworkUtil.findConnectedInterface(world!!, pos, side)?.also {
destinationCache[side.ordinal] = WeakReference(it)
}
}
private fun 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) {
val (frame, fromItf) = delayedPackets.pop()
resend(frame, fromItf)
}
}
override fun writeNbt(tag: NbtCompound) {
super.writeNbt(tag)
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
val list = NbtList()
for ((frame, fromItf) in delayedPackets) {
val packet = frame.packet
if (packet !is ItemStackPacket) continue
val compound = NbtCompound()
compound.putInt("FromItfSide", fromItf.side.ordinal)
compound.putInt("SourceIP", packet.source.address)
compound.putInt("DestinationIP", packet.destination.address)
compound.putLong("SourceMAC", frame.source.address)
compound.putLong("DestinationMAC", frame.destination.address)
compound.put("Stack", packet.stack.writeNbt(NbtCompound()))
list.add(compound)
}
tag.put("DelayedStackPackets", list)
}
override fun 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 NbtCompound
val fromItfSide = Direction.values()[compound.getInt("FromItfSide")]
val fromItf = interfaces.find { it.side == fromItfSide }!!
val sourceIP = IPAddress(compound.getInt("SourceIP"))
val destinationIP = IPAddress(compound.getInt("DestinationIP"))
val sourceMAC = MACAddress(compound.getLong("SourceMAC"))
val destinationMAC = MACAddress(compound.getLong("DestinationMAC"))
val stack = ItemStack.fromNbt(compound.getCompound("Stack"))
if (!stack.isEmpty) {
val packet = ItemStackPacket(stack, sourceIP, destinationIP)
val frame = BasePacketFrame(packet, sourceMAC, destinationMAC)
delayedPackets.addLast(frame to fromItf)
}
}
}
override fun toUpdatePacket(): Packet<ClientPlayPacketListener>? {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun toInitialChunkDataNbt(): NbtCompound {
val tag = NbtCompound()
tag.putLongArray("InterfaceAddresses", interfaces.map { it.macAddress.address })
return tag
}
class SwitchInterface(
val side: Direction,
val switch: WeakReference<SwitchBlockEntity>,
@JvmField var macAddress: MACAddress,
): Interface {
override fun getMACAddress() = macAddress
override fun receive(frame: EthernetFrame) {
switch.get()?.handle(frame, this)
}
override fun send(frame: EthernetFrame) {
switch.get()?.findDestination(this)?.receive(frame)
}
override fun cableDisconnected() {
switch.get()?.cableDisconnected(this)
}
}
}

View File

@ -0,0 +1,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

@ -1,10 +1,10 @@
package net.shadowfacts.phycon.network.block.redstone
package net.shadowfacts.phycon.block.redstone_controller
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.item.ItemPlacementContext
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.Identifier
@ -12,22 +12,23 @@ 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.network.FaceDeviceBlock
import java.util.*
import net.shadowfacts.phycon.block.FaceDeviceBlock
/**
* @author shadowfacts
*/
class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>(Settings.of(Material.METAL)) {
class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
) {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "redstone_controller")
val LIT = Properties.LIT
val POWERED = Properties.POWERED
}
// todo: don't just copy this from the Interface block
override val faceThickness = 3.0
override val faceShapes = mapOf(
Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 3.0, 16.0),
@ -40,20 +41,20 @@ class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>(Se
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
super.appendProperties(builder)
builder.add(LIT)
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)
return state.with(LIT, isPowered(context.world, context.blockPos, state[FACING]))
return state.with(POWERED, isPowered(context.world, context.blockPos, state[FACING]))
}
override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighborBlock: Block, neighborPos: BlockPos, bl: Boolean) {
// this can't be done in getStateForNeighborUpdate because getEmittedRedstonePower is defined in World not WorldAccess
if (!world.isClient) {
val wasLit = state[LIT]
val wasLit = state[POWERED]
val isLit = isPowered(world, pos, state[FACING])
if (wasLit != isLit) {
toggleLit(state, world, pos)
@ -67,7 +68,7 @@ class RedstoneControllerBlock: FaceDeviceBlock<RedstoneControllerBlockEntity>(Se
}
private fun toggleLit(state: BlockState, world: World, pos: BlockPos) {
world.setBlockState(pos, state.cycle(LIT), 2)
world.setBlockState(pos, state.cycle(POWERED), 2)
getBlockEntity(world, pos)!!.redstoneStateChanged()
}

View File

@ -1,21 +1,28 @@
package net.shadowfacts.phycon.network.block.redstone
package net.shadowfacts.phycon.block.redstone_controller
import net.minecraft.block.BlockState
import net.minecraft.nbt.CompoundTag
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
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.packet.RemoteActivationPacket
import net.shadowfacts.phycon.block.DeviceBlockEntity
import net.shadowfacts.phycon.packet.RemoteActivationPacket
import net.shadowfacts.phycon.util.ClientConfigurableDevice
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 }
var redstoneMode = RedstoneMode.HIGH
set(value) {
field = value
redstoneStateChanged()
}
private var redstonePowered = false
@ -23,8 +30,10 @@ class RedstoneControllerBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE
}
fun redstoneStateChanged() {
if (world == null || world!!.isClient) return
val oldPowered = redstonePowered
redstonePowered = cachedState[RedstoneControllerBlock.LIT]
redstonePowered = cachedState[RedstoneControllerBlock.POWERED]
val mode: RemoteActivationPacket.Mode? = when (redstoneMode) {
RedstoneMode.TOGGLE -> if (oldPowered != redstonePowered) RemoteActivationPacket.Mode.SINGLE else null
@ -46,27 +55,22 @@ class RedstoneControllerBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE
}
}
override fun toTag(tag: CompoundTag): CompoundTag {
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address })
tag.putString("RedstoneMode", redstoneMode.name)
return super.toTag(tag)
}
override fun fromTag(state: BlockState, tag: CompoundTag) {
super.fromTag(state, tag)
val addresses = tag.getIntArray("ManagedDevices")
managedDevices = (0..4).map { if (it >= addresses.size) null else IPAddress(addresses[it]) }.toTypedArray()
redstoneMode = RedstoneMode.valueOf(tag.getString("RedstoneMode"))
}
override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address })
tag.putString("RedstoneMode", redstoneMode.name)
return super.toClientTag(tag)
}
override fun fromClientTag(tag: CompoundTag) {
super.fromClientTag(tag)
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

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

View File

@ -0,0 +1,135 @@
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.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.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(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) {
field = value
recalculateRedstone()
}
override fun handle(packet: Packet) {
when (packet) {
is ReadGroupedInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
}
}
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
recalculateRedstone()
}
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source)
recalculateRedstone()
}
override fun tick() {
super.tick()
if (!world!!.isClient && counter % 20 == 0L) {
if (counter % 80 == 0L) {
updateInventories()
} else if (counter % 20 == 0L) {
recalculateRedstone()
}
}
}
private fun updateInventories() {
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
}
private fun recalculateRedstone() {
if (world == null || world!!.isClient) return
if (stackToMonitor.isEmpty) {
cachedEmittedPower = 0
updateWorld()
return
}
val networkAmount = inventoryCache.values.fold(0) { acc, inv ->
acc + inv.getAmount(stackToMonitor)
}
cachedEmittedPower =
when (mode) {
Mode.ANALOG -> if (networkAmount == 0) {
0
} else {
1 + round(networkAmount / maxAmount.toDouble() * 14).toInt()
}
Mode.DIGITAL -> if (networkAmount >= maxAmount) 15 else 0
}
updateWorld()
}
private fun updateWorld() {
world!!.updateNeighborsAlways(pos, cachedState.block)
world!!.updateNeighborsAlways(pos.offset(cachedState[FaceDeviceBlock.FACING]), cachedState.block)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.putInt("CachedEmittedPower", cachedEmittedPower)
tag.put("StackToMonitor", stackToMonitor.writeNbt(NbtCompound()))
writeDeviceConfiguration(tag)
}
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
cachedEmittedPower = tag.getInt("CachedEmittedPower")
stackToMonitor = ItemStack.fromNbt(tag.getCompound("StackToMonitor"))
loadDeviceConfiguration(tag)
}
override fun writeDeviceConfiguration(tag: NbtCompound) {
tag.putInt("MaxAmount", maxAmount)
tag.putString("Mode", mode.name)
}
override fun loadDeviceConfiguration(tag: NbtCompound) {
maxAmount = tag.getInt("MaxAmount")
mode = Mode.valueOf(tag.getString("Mode"))
}
enum class Mode {
ANALOG, DIGITAL;
val friendlyName = TranslatableText("gui.phycon.redstone_emitter_mode.${name.lowercase()}")
}
}

View File

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

View File

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

View File

@ -1,13 +1,19 @@
package net.shadowfacts.phycon.network.block.terminal
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.AttributeList
import alexiil.mc.lib.attributes.AttributeProvider
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Material
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.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
@ -16,31 +22,53 @@ 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.network.DeviceBlock
import java.util.*
import net.shadowfacts.phycon.block.DeviceBlock
import java.util.EnumSet
/**
* @author shadowfacts
*/
class TerminalBlock: DeviceBlock<TerminalBlockEntity>(Settings.of(Material.METAL)),
abstract class AbstractTerminalBlock<T: AbstractTerminalBlockEntity>: DeviceBlock<T>(
Settings.of(Material.METAL)
.strength(1.5f)
.sounds(BlockSoundGroup.METAL)
),
NetworkComponentBlock,
AttributeProvider {
companion object {
val ID = Identifier(PhysicalConnectivity.MODID, "terminal")
val FACING = Properties.FACING
}
override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection<Direction> {
return EnumSet.allOf(Direction::class.java)
val set = EnumSet.allOf(Direction::class.java)
set.remove(state[FACING])
return set
}
override fun createBlockEntity(world: BlockView) = TerminalBlockEntity()
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

@ -1,62 +1,59 @@
package net.shadowfacts.phycon.network.block.terminal
package net.shadowfacts.phycon.block.terminal
import alexiil.mc.lib.attributes.item.GroupedItemInvView
import alexiil.mc.lib.attributes.item.ItemStackCollections
import alexiil.mc.lib.attributes.item.ItemStackUtil
import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.inventory.Inventory
import net.minecraft.inventory.InventoryChangedListener
import net.minecraft.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
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.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.init.PhyBlockEntities
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.NetworkUtil
import net.shadowfacts.phycon.network.component.*
import net.shadowfacts.phycon.network.packet.*
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
*/
class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: BlockState): DeviceBlockEntity(type, pos, state),
InventoryChangedListener,
BlockEntityClientSerializable,
Tickable,
ItemStackPacketHandler,
NetworkStackDispatcher<TerminalBlockEntity.PendingInsertion> {
NetworkStackDispatcher<AbstractTerminalBlockEntity.PendingInsertion> {
companion object {
val LOCATE_REQUEST_TIMEOUT: Long = 40 // ticks
val INSERTION_TIMEOUT: Long = 40
// 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
}
private val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
protected val inventoryCache = mutableMapOf<IPAddress, GroupedItemInvView>()
val internalBuffer = TerminalBufferInventory(18)
private val pendingRequests = LinkedList<StackLocateRequest>()
protected val pendingRequests = LinkedList<StackLocateRequest>()
override val pendingInsertions = mutableListOf<PendingInsertion>()
override val dispatchStackTimeout = INSERTION_TIMEOUT
private var observers = 0
val cachedNetItems = ItemStackCollections.intMap()
private var requestInventoryTimestamp: Long? = null
// todo: multiple players could have the terminal open simultaneously
var netItemObserver: WeakReference<NetItemObserver>? = null
init {
@ -73,9 +70,14 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
return null
}
override fun handleNetworkSplit() {
super.handleNetworkSplit()
inventoryCache.clear()
}
override fun handle(packet: Packet) {
when (packet) {
is ReadInventoryPacket -> handleReadInventory(packet)
is ReadGroupedInventoryPacket -> handleReadInventory(packet)
is DeviceRemovedPacket -> handleDeviceRemoved(packet)
is StackLocationPacket -> handleStackLocation(packet)
is ItemStackPacket -> handleItemStack(packet)
@ -83,7 +85,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
}
}
private fun handleReadInventory(packet: ReadInventoryPacket) {
private fun handleReadInventory(packet: ReadGroupedInventoryPacket) {
inventoryCache[packet.source] = packet.inventory
updateAndSync()
}
@ -106,7 +108,15 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
}
override fun doHandleItemStack(packet: ItemStackPacket): ItemStack {
val remaining = internalBuffer.insertFromNetwork(packet.stack)
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
@ -115,9 +125,11 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
return remaining
}
private fun updateAndSync() {
protected fun updateAndSync() {
updateNetItems()
sync()
// syncs the internal buffer to the client
markUpdate()
// syncs the open container (if any) to the client
netItemObserver?.get()?.netItemsChanged()
}
@ -126,7 +138,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
for (inventory in inventoryCache.values) {
for (stack in inventory.storedStacks) {
val amount = inventory.getAmount(stack)
cachedNetItems.mergeInt(stack, amount) { a, b -> a + b }
cachedNetItems.mergeInt(stack, amount, IntBinaryOperator { a, b -> a + b })
}
}
}
@ -153,50 +165,32 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
finishable.forEach(::stackLocateRequestCompleted)
}
fun addObserver() {
observers++
}
fun removeObserver() {
observers--
}
override fun tick() {
super.tick()
if (counter % 20 == 0L) {
if (!world!!.isClient) {
finishPendingRequests()
if (!world!!.isClient) {
finishPendingRequests()
finishTimedOutPendingInsertions()
if (counter % 20 == 0L) {
beginInsertions()
finishTimedOutPendingInsertions()
}
if (observers > 0 && !world!!.isClient) {
if (requestInventoryTimestamp != null && (counter - requestInventoryTimestamp!!) >= REQUEST_INVENTORY_TIMEOUT) {
updateAndSync()
requestInventoryTimestamp = null
}
}
}
fun onActivate(player: PlayerEntity) {
open 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)
}
override fun getDisplayName() = TranslatableText("block.phycon.terminal")
override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) {
buf.writeBlockPos(this@TerminalBlockEntity.pos)
}
}
player.openHandledScreen(factory)
sendPacket(RequestInventoryPacket(RequestInventoryPacket.Kind.GROUPED, ipAddress))
requestInventoryTimestamp = counter
}
addObserver()
}
fun requestItem(stack: ItemStack, amount: Int = stack.count) {
@ -207,11 +201,18 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
sendPacket(LocateStackPacket(stack, ipAddress))
}
private fun stackLocateRequestCompleted(request: StackLocateRequest) {
protected open fun stackLocateRequestCompleted(request: StackLocateRequest) {
pendingRequests.remove(request)
// todo: also sort results by interface priority
val sortedResults = request.results.sortedByDescending { it.first }.toMutableList()
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)
@ -230,32 +231,27 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateAndSync()
return remaining
// 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) {
markDirty()
sync()
markUpdate()
}
}
override fun toTag(tag: CompoundTag): CompoundTag {
open fun dropItems() {
ItemScatterer.spawn(world, pos, internalBuffer)
}
override fun toCommonTag(tag: NbtCompound) {
super.toCommonTag(tag)
tag.put("InternalBuffer", internalBuffer.toTag())
return super.toTag(tag)
}
override fun fromTag(state: BlockState, tag: CompoundTag) {
super.fromTag(state, tag)
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
}
override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.put("InternalBuffer", internalBuffer.toTag())
return tag
}
override fun fromClientTag(tag: CompoundTag) {
override fun fromCommonTag(tag: NbtCompound) {
super.fromCommonTag(tag)
internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
}
@ -267,18 +263,21 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
var bufferSlot by Delegates.notNull<Int>()
}
}
data class StackLocateRequest(
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 {
return totalResultAmount >= amount || currentTimestamp - timestamp >= TerminalBlockEntity.LOCATE_REQUEST_TIMEOUT
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

@ -1,6 +1,5 @@
package net.shadowfacts.phycon.network.block.terminal
package net.shadowfacts.phycon.block.terminal
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.entity.player.PlayerEntity
@ -8,79 +7,89 @@ 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.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode
import net.shadowfacts.phycon.util.TerminalSettings
import net.shadowfacts.phycon.util.copyWithCount
import java.lang.ref.WeakReference
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* @author shadowfacts
*/
class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId),
TerminalBlockEntity.NetItemObserver {
abstract class AbstractTerminalScreenHandler<T: AbstractTerminalBlockEntity>(
handlerType: ScreenHandlerType<*>,
syncId: Int,
val playerInv: PlayerInventory,
val terminal: T,
): ScreenHandler(handlerType, syncId),
AbstractTerminalBlockEntity.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 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 {
val stack = it.stack.copy()
stack.count = it.amount
stack
it.stack.copyWithCount(it.amount)
}
}
}
var itemsForDisplay = listOf<ItemStack>()
private set
constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf):
this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!)
open val xOffset: Int = 0
init {
if (!terminal.world!!.isClient) {
assert(terminal.netItemObserver?.get() === null)
terminal.netItemObserver = WeakReference(this)
netItemsChanged()
// 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, 66 + x * 18, 18 + y * 18))
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, 8 + x * 18, 18 + y * 18))
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, 66 + x * 18, 140 + y * 18))
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, 66 + x * 18, 198))
addSlot(Slot(playerInv, x, xOffset + 66 + x * 18, 198))
}
}
@ -100,29 +109,54 @@ class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val ter
it.key.name.string.contains(searchQuery, true)
}
totalEntries = filtered.size
val sorted =
when (sortMode) {
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 }
}
itemEntries = sorted.map { Entry(it.key, it.intValue) }
(player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, sortMode))
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 sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, sortMode: SortMode) {
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.sortMode = sortMode
this.settings = settings
this.scrollPosition = scrollPosition
netItemsChanged()
}
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, sortMode: SortMode) {
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, scrollPosition: Float, totalEntries: Int) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.sortMode = sortMode
this.scrollPosition = scrollPosition
this.totalEntries = totalEntries
itemEntries = entries
}
@ -133,19 +167,19 @@ class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val ter
override fun close(player: PlayerEntity) {
super.close(player)
terminal.removeObserver()
terminal.netItemObserver = null
}
override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack {
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) && !player.inventory.cursorStack.isEmpty) {
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)
}
}
return super.onSlotClick(slotId, clickData, actionType, player)
super.onSlotClick(slotId, clickData, actionType, player)
}
override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack {
@ -171,7 +205,7 @@ class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val ter
} 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) {
if (slotsInsertedInto.isEmpty()) {
return ItemStack.EMPTY
}
}
@ -193,7 +227,7 @@ class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val ter
slot.markDirty()
slotsInsertedInto.add(index)
} else if (canStacksCombine(slotStack, stack) && slotStack.count < slotStack.maxCount) {
} 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)
@ -206,7 +240,10 @@ class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val ter
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

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

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

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