├── logo.png ├── logo-cf.png ├── img ├── showcase0.png ├── showcase1.png ├── showcase2.png ├── showcase3.png ├── showcase4.png ├── showcase5.png ├── showcase6.png ├── showcase7.png └── showcase8.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ ├── pack.mcmeta │ ├── assets │ │ └── betterp2p │ │ │ ├── textures │ │ │ ├── logo.png │ │ │ ├── gui │ │ │ │ └── advanced_memory_card.png │ │ │ └── item │ │ │ │ └── advanced_memory_card.png │ │ │ ├── models │ │ │ └── item │ │ │ │ └── advanced_memory_card.json │ │ │ ├── shaders │ │ │ └── program │ │ │ │ └── outline.vsh │ │ │ └── lang │ │ │ ├── zh_cn.json │ │ │ ├── ja_jp.json │ │ │ ├── en_us.json │ │ │ ├── ru_ru.json │ │ │ └── zh_tw.json │ ├── data │ │ └── betterp2p │ │ │ └── recipe │ │ │ └── advanced_memory_card.json │ └── META-INF │ │ └── neoforge.mods.toml │ └── kotlin │ └── dev │ └── lasm │ └── betterp2p │ ├── network │ ├── packet │ │ ├── IMessage.kt │ │ ├── C2SCloseGui.kt │ │ ├── C2SRefreshP2PList.kt │ │ ├── C2SUpdateMemoryInfo.kt │ │ ├── C2SLinkP2P.kt │ │ ├── C2SUnlinkP2P.kt │ │ ├── C2SChangeP2PType.kt │ │ ├── S2CUpdateP2P.kt │ │ ├── S2COpenGui.kt │ │ └── C2SRenameP2P.kt │ ├── data │ │ ├── P2PInfo.kt │ │ ├── HashHelper.kt │ │ ├── P2PLocation.kt │ │ ├── MemoryInfo.kt │ │ ├── BetterP2PCodecs.kt │ │ └── GridServerCache.kt │ └── ModNetwork.kt │ ├── client │ ├── gui │ │ ├── InfoSortStrategy.kt │ │ ├── widget │ │ │ ├── GuiScale.kt │ │ │ ├── WidgetTypeIcon.kt │ │ │ ├── WidgetScrollBar.kt │ │ │ ├── IconButton.kt │ │ │ ├── WidgetTypeSelector.kt │ │ │ ├── P2PTypeButton.kt │ │ │ ├── WidgetP2PColumn.kt │ │ │ └── WidgetP2PDevice.kt │ │ ├── GuiHelper.kt │ │ ├── InfoFilter.kt │ │ ├── InfoWrapper.kt │ │ ├── Infolist.kt │ │ └── GuiAdvancedMemoryCard.kt │ ├── ClientCache.kt │ ├── AdvancedMemoryCardMenu.kt │ └── RenderBlockOutline.kt │ ├── util │ ├── p2p │ │ ├── P2PUtil.kt │ │ └── TunnelInfo.kt │ └── CableBusUtil.kt │ ├── item │ ├── BetterMemoryCardModes.kt │ └── ItemAdvancedMemoryCard.kt │ ├── BetterP2P.kt │ └── Proxy.kt ├── spotless └── spotless.importorder ├── crowdin.yml ├── .gitignore ├── .editorconfig ├── settings.gradle ├── .github └── workflows │ ├── build.yml │ └── publish_project.yml ├── README.md ├── gradle.properties ├── CHANGELOG.md ├── gradlew.bat └── gradlew /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/logo.png -------------------------------------------------------------------------------- /logo-cf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/logo-cf.png -------------------------------------------------------------------------------- /img/showcase0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase0.png -------------------------------------------------------------------------------- /img/showcase1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase1.png -------------------------------------------------------------------------------- /img/showcase2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase2.png -------------------------------------------------------------------------------- /img/showcase3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase3.png -------------------------------------------------------------------------------- /img/showcase4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase4.png -------------------------------------------------------------------------------- /img/showcase5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase5.png -------------------------------------------------------------------------------- /img/showcase6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase6.png -------------------------------------------------------------------------------- /img/showcase7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase7.png -------------------------------------------------------------------------------- /img/showcase8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/img/showcase8.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "description": "Example Mod", 4 | "pack_format": 15 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spotless/spotless.importorder: -------------------------------------------------------------------------------- 1 | #Organize Import Order 2 | #Sat Jan 28 17:57:48 GMT 2023 3 | 0=java 4 | 1=javax 5 | 2=net 6 | 3=org 7 | 4=com 8 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/textures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/src/main/resources/assets/betterp2p/textures/logo.png -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/textures/gui/advanced_memory_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/src/main/resources/assets/betterp2p/textures/gui/advanced_memory_card.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | bundles: 2 | - 2 3 | files: 4 | - source: /src/main/resources/assets/betterp2p/lang/en_us.json 5 | translation: /src/main/resources/assets/betterp2p/lang/%locale_with_underscore%.json 6 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/textures/item/advanced_memory_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasmGratel/BetterP2P/HEAD/src/main/resources/assets/betterp2p/textures/item/advanced_memory_card.png -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/models/item/advanced_memory_card.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "betterp2p:item/advanced_memory_card" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/shaders/program/outline.vsh: -------------------------------------------------------------------------------- 1 | #version 120 2 | 3 | uniform mat4 rotate_matrix; 4 | 5 | attribute vec4 position; 6 | 7 | void main() { 8 | gl_Position = rotate_matrix * position; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | runs/ 2 | 3 | build/ 4 | .gradle/ 5 | out/ 6 | .idea/ 7 | *.iws 8 | *.ipr 9 | *.iml 10 | output/ 11 | bin/ 12 | libs/ 13 | .classpath 14 | .project 15 | .kotlin/ 16 | classes/ 17 | .metadata 18 | .vscode 19 | .settings 20 | *.launch 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { url = 'https://maven.neoforged.net/releases' } 5 | } 6 | } 7 | 8 | plugins { 9 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' 10 | } 11 | 12 | rootProject.name = "betterp2p" 13 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/IMessage.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 4 | 5 | sealed interface IMessage : CustomPacketPayload {} 6 | 7 | interface IC2SMessage : IMessage {} 8 | 9 | interface IS2CMessage : IMessage {} 10 | -------------------------------------------------------------------------------- /src/main/resources/data/betterp2p/recipe/advanced_memory_card.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minecraft:crafting_shapeless", 3 | "category": "misc", 4 | "ingredients": [ 5 | { 6 | "item": "ae2:memory_card" 7 | }, 8 | { 9 | "item": "ae2:network_tool" 10 | } 11 | ], 12 | "result": { 13 | "count": 1, 14 | "id": "betterp2p:advanced_memory_card" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/InfoSortStrategy.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui 2 | 3 | enum class InfoSortStrategy(val comparator: (InfoWrapper, InfoWrapper) -> Int) : 4 | Comparator { 5 | DEFAULT({ o1, o2 -> o1.frequency.compareTo(o2.frequency) }), 6 | BY_FREQUENCY({ o1, o2 -> o1.frequency.compareTo(o2.frequency) }), 7 | ; 8 | 9 | override fun compare(o1: InfoWrapper, o2: InfoWrapper): Int = comparator(o1, o2) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/ClientCache.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client 2 | 3 | import net.minecraft.core.BlockPos 4 | import net.minecraft.core.Direction 5 | 6 | object ClientCache { 7 | val positions = mutableListOf>() 8 | var selectedPosition: BlockPos? = null 9 | var selectedFacing: Direction? = null 10 | var searchText: String = "" 11 | 12 | fun clear() { 13 | positions.clear() 14 | selectedPosition = null 15 | selectedFacing = null 16 | searchText = "" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/GuiScale.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | enum class GuiScale(val size: (Int) -> Int, val minHeight: Int, val unlocalizedName: String) { 4 | SMALL({ 4 }, 0, "gui.advanced_memory_card.gui_scale.small"), 5 | NORMAL({ 6 }, 326, "gui.advanced_memory_card.gui_scale.normal"), 6 | LARGE({ 8 }, 409, "gui.advanced_memory_card.gui_scale.large"), 7 | DYNAMIC( 8 | { availableHeight -> availableHeight / P2PEntryConstants.HEIGHT }, 9 | 0, 10 | "gui.advanced_memory_card.gui_scale.dynamic" 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | modLoader = "kotlinforforge" 2 | loaderVersion = "[5,)" 3 | issueTrackerURL = "https://github.com/LasmGratel/BetterP2P/issues" 4 | license = "GPL-3.0" 5 | 6 | [[mods]] 7 | modId = "${mod_id}" 8 | version = "${mod_version}" 9 | displayName = "${mod_name}" 10 | authors = "${mod_authors}" 11 | description = "${mod_description}" 12 | #logoFile = "" 13 | 14 | [[dependencies.${mod_id}]] 15 | modId = "neoforge" 16 | mandatory = true 17 | versionRange = "${neo_version_range}" 18 | ordering = "NONE" 19 | side = "BOTH" 20 | 21 | [[dependencies.${mod_id}]] 22 | modId = "minecraft" 23 | mandatory = true 24 | versionRange = "${minecraft_version_range}" 25 | ordering = "NONE" 26 | side = "BOTH" 27 | 28 | [[dependencies.${mod_id}]] 29 | modId = "kotlinforforge" 30 | mandatory = true 31 | versionRange = "[5,)" 32 | ordering = "AFTER" 33 | side = "BOTH" 34 | 35 | [[dependencies.${mod_id}]] 36 | modId = "ae2" 37 | mandatory = true 38 | versionRange = "[${ae2_version},)" 39 | ordering = "AFTER" 40 | side = "BOTH" 41 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/util/p2p/P2PUtil.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.util.p2p 2 | 3 | import appeng.parts.AEBasePart 4 | import appeng.parts.p2p.P2PTunnelPart 5 | import dev.lasm.betterp2p.BetterP2P 6 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 7 | import net.minecraft.network.chat.Component 8 | 9 | val P2PTunnelPart<*>.hasChannel 10 | get() = isPowered && isActive 11 | 12 | /** Get the type index or use TUNNEL_ANY */ 13 | fun P2PTunnelPart<*>.getTypeIndex() = 14 | BetterP2P.proxy.getP2PFromClass(this.javaClass)?.index ?: TUNNEL_ANY 15 | 16 | fun AEBasePart.setCustomName(value: Component?) { 17 | // FUCK YOUR MOM 18 | val field = AEBasePart::class.java.getDeclaredField("customName") 19 | field.isAccessible = true 20 | field.set(this, value) 21 | } 22 | 23 | fun P2PTunnelPart<*>.pleaseSetTheFuckingOutputState(output: Boolean) { 24 | val field = P2PTunnelPart::class.java.getDeclaredField("output") 25 | field.isAccessible = true 26 | field.set(this, output) 27 | host.markForSave() 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/WidgetTypeIcon.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import dev.lasm.betterp2p.client.gui.drawBlockIcon 4 | import net.minecraft.client.gui.GuiGraphics 5 | import net.minecraft.client.gui.components.AbstractWidget 6 | import net.minecraft.client.gui.components.Tooltip 7 | import net.minecraft.client.gui.narration.NarrationElementOutput 8 | import net.minecraft.network.chat.Component 9 | import net.minecraft.resources.ResourceLocation 10 | 11 | class WidgetTypeIcon( 12 | x: Int, 13 | y: Int, 14 | tooltipLiteral: String, 15 | val iconSupplier: () -> ResourceLocation 16 | ) : AbstractWidget(x, y, 18, 18, Component.empty()) { 17 | init { 18 | tooltip = Tooltip.create(Component.literal(tooltipLiteral)) 19 | } 20 | 21 | override fun renderWidget(graphics: GuiGraphics, i: Int, j: Int, f: Float) { 22 | if (isHovered) { 23 | graphics.fill(x, y, x + width, y + height, 0xFF00FF00.toInt()) 24 | } 25 | drawBlockIcon(graphics, iconSupplier(), x = x + 1, y = y + 1) 26 | } 27 | 28 | override fun updateWidgetNarration(arg: NarrationElementOutput) {} 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/AdvancedMemoryCardMenu.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.data.MemoryInfo 5 | import dev.lasm.betterp2p.network.data.P2PInfo 6 | import net.minecraft.world.entity.player.Inventory 7 | import net.minecraft.world.entity.player.Player 8 | import net.minecraft.world.inventory.AbstractContainerMenu 9 | import net.minecraft.world.inventory.DataSlot 10 | import net.minecraft.world.item.ItemStack 11 | 12 | class AdvancedMemoryCardMenu(id: Int, inv: Inventory?) : 13 | AbstractContainerMenu(BetterP2P.ADVANCED_MEMORY_CARD_MENU.get(), id) { 14 | var infos: List = emptyList() 15 | var memoryInfo: MemoryInfo = MemoryInfo() 16 | override fun quickMoveStack(player: Player, i: Int): ItemStack { 17 | return ItemStack.EMPTY 18 | } 19 | 20 | override fun stillValid(player: Player): Boolean { 21 | return true 22 | } 23 | 24 | override fun addDataSlot(dataSlot: DataSlot): DataSlot { 25 | return super.addDataSlot(dataSlot) 26 | } 27 | 28 | override fun sendAllDataToRemote() { 29 | super.sendAllDataToRemote() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SCloseGui.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.ModNetwork 5 | import io.netty.buffer.ByteBuf 6 | import net.minecraft.network.codec.StreamCodec 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 8 | import net.minecraft.resources.ResourceLocation 9 | import net.neoforged.neoforge.network.handling.IPayloadContext 10 | 11 | class C2SCloseGui : IC2SMessage { 12 | 13 | override fun type(): CustomPacketPayload.Type = TYPE 14 | 15 | companion object { 16 | val TYPE = 17 | CustomPacketPayload.Type( 18 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "close_gui") 19 | ) 20 | private val EMPTY = C2SCloseGui() 21 | val STREAM_CODEC: StreamCodec = StreamCodec.unit(EMPTY) 22 | } 23 | 24 | override fun equals(other: Any?): Boolean = other is C2SCloseGui 25 | override fun hashCode(): Int = 0 26 | } 27 | 28 | val ServerCloseGuiHandler: ((C2SCloseGui, IPayloadContext) -> Unit) = 29 | { message: C2SCloseGui, ctx: IPayloadContext -> 30 | ModNetwork.playerState.remove(ctx.player().uuid) 31 | Unit 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/util/p2p/TunnelInfo.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.util.p2p 2 | 3 | import appeng.parts.p2p.P2PTunnelPart 4 | import net.minecraft.ChatFormatting 5 | import net.minecraft.network.chat.Component 6 | import net.minecraft.resources.ResourceLocation 7 | import net.minecraft.world.item.ItemStack 8 | 9 | /** Common tunnel info to be used on server */ 10 | open class TunnelInfo( 11 | val index: Int, 12 | val stack: ItemStack, 13 | val clazz: Class> 14 | ) { 15 | val dispName: Component = 16 | stack.displayName ?: Component.literal("").withStyle(ChatFormatting.RED) 17 | override fun toString(): String { 18 | return "TunnelInfo(index=$index, stack=$stack, clazz=$clazz, dispName='${dispName.string}')" 19 | } 20 | } 21 | 22 | /** 23 | * Client tunnel info contains icon info too. Because textures are not loaded until after postInit, 24 | * we need to use a supplier, unfortunately. 25 | */ 26 | class ClientTunnelInfo( 27 | index: Int, 28 | stack: ItemStack, 29 | clazz: Class>, 30 | val icon: () -> ResourceLocation 31 | ) : TunnelInfo(index, stack, clazz) { 32 | override fun toString(): String { 33 | return "ClientTunnelInfo(icon=${icon()}) ${super.toString()}" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | types: [ opened, synchronize, reopened ] 8 | jobs: 9 | validate-gradle: 10 | name: "Validate Gradle wrapper" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | submodules: true # Clone with vs-core submodule 17 | - uses: gradle/wrapper-validation-action@v1 18 | build: 19 | name: Build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 25 | submodules: true # Clone with vs-core submodule 26 | - name: Set up JDK 21 27 | uses: actions/setup-java@v1 28 | with: 29 | java-version: 21 30 | - name: Grant execute permission for gradlew 31 | run: chmod +x gradlew 32 | - name: Cache Gradle packages 33 | uses: actions/cache@v1 34 | with: 35 | path: ~/.gradle/caches 36 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 37 | restore-keys: ${{ runner.os }}-gradle 38 | - name: Build and analyze 39 | run: ./gradlew build --info 40 | - name: Attach compilation artifacts 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: neoforge-build-libs 44 | path: build/libs 45 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SRefreshP2PList.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.ModNetwork 5 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 6 | import io.netty.buffer.ByteBuf 7 | import net.minecraft.network.codec.ByteBufCodecs 8 | import net.minecraft.network.codec.StreamCodec 9 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 10 | import net.minecraft.resources.ResourceLocation 11 | import net.neoforged.neoforge.network.handling.IPayloadContext 12 | 13 | /** Send a request to the server to refresh the p2p list with the given type. */ 14 | class C2SRefreshP2PList(val type: Int = TUNNEL_ANY) : IC2SMessage { 15 | override fun type(): CustomPacketPayload.Type = TYPE 16 | 17 | companion object { 18 | val TYPE = 19 | CustomPacketPayload.Type( 20 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "refresh_p2p_list") 21 | ) 22 | val STREAM_CODEC: StreamCodec = 23 | StreamCodec.composite(ByteBufCodecs.INT, C2SRefreshP2PList::type, ::C2SRefreshP2PList) 24 | } 25 | } 26 | 27 | /** Client -> C2SRefreshP2P -> Server Handler on server side */ 28 | val ServerRefreshP2PListHandler: ((C2SRefreshP2PList, IPayloadContext) -> Unit) = 29 | { message: C2SRefreshP2PList, ctx: IPayloadContext -> 30 | ModNetwork.requestP2PList(ctx.player(), message.type) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/util/CableBusUtil.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.util 2 | 3 | import appeng.api.parts.IPart 4 | import appeng.api.parts.IPartHost 5 | import appeng.api.parts.SelectedPart 6 | import appeng.blockentity.networking.CableBusBlockEntity 7 | import appeng.parts.AEBasePart 8 | import appeng.parts.ICableBusContainer 9 | import net.minecraft.core.BlockPos 10 | import net.minecraft.core.Direction 11 | import net.minecraft.world.level.BlockGetter 12 | import net.minecraft.world.level.block.entity.BlockEntity 13 | import net.minecraft.world.phys.HitResult 14 | 15 | /** @see appeng.block.networking.BlockCableBus.cb */ 16 | fun getCableBus(w: BlockGetter, pos: BlockPos): ICableBusContainer? { 17 | val te = w.getBlockEntity(pos) 18 | var out: ICableBusContainer? = null 19 | if (te is CableBusBlockEntity) { 20 | out = te.cableBus 21 | } 22 | return out 23 | } 24 | 25 | fun getPart(w: BlockGetter, pos: BlockPos, hitResult: HitResult): IPart? { 26 | val te = w.getBlockEntity(pos) 27 | if (te !is IPartHost) return null 28 | val p: SelectedPart? = (te as IPartHost).selectPartWorld(hitResult.location) 29 | return p?.part 30 | } 31 | 32 | val AEBasePart.facingPos: BlockPos? 33 | get() = host?.location?.pos?.offset(side?.normal ?: Direction.UP.normal) 34 | 35 | val AEBasePart.facingTile: BlockEntity? 36 | get() { 37 | if (host.isInWorld) { 38 | val pos = facingPos 39 | if (pos != null) return host?.location?.level?.getBlockEntity(pos) 40 | } 41 | return null 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/data/P2PInfo.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.data 2 | 3 | import appeng.me.GridNode 4 | import appeng.parts.p2p.P2PTunnelPart 5 | import dev.lasm.betterp2p.util.p2p.getTypeIndex 6 | import dev.lasm.betterp2p.util.p2p.hasChannel 7 | import net.minecraft.core.BlockPos 8 | import net.minecraft.core.Direction 9 | import net.minecraft.resources.ResourceKey 10 | import net.minecraft.world.level.Level 11 | 12 | class P2PInfo( 13 | val frequency: Short, 14 | val pos: BlockPos, 15 | val dim: ResourceKey, 16 | val facing: Direction, 17 | val name: String, 18 | val output: Boolean, 19 | val hasChannel: Boolean, 20 | val channels: Int, 21 | val type: Int 22 | ) { 23 | override fun hashCode(): Int { 24 | return hashP2P(pos, facing.ordinal, dim).hashCode() 25 | } 26 | 27 | override fun equals(other: Any?): Boolean { 28 | if (this === other) return true 29 | if (javaClass != other?.javaClass) return false 30 | 31 | other as P2PInfo 32 | if (this.pos != other.pos) return false 33 | if (facing != other.facing) return false 34 | return dim == other.dim 35 | } 36 | } 37 | 38 | fun P2PTunnelPart<*>.toInfo() = 39 | P2PInfo( 40 | frequency, 41 | blockEntity.blockPos, 42 | blockEntity.level!!.dimension(), 43 | side, 44 | customName?.string ?: "", 45 | isOutput, 46 | hasChannel, 47 | (externalFacingNode as? GridNode)?.getUsedChannels() ?: -1, 48 | getTypeIndex() 49 | ) 50 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SUpdateMemoryInfo.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.data.MemoryInfo 5 | import io.netty.buffer.ByteBuf 6 | import net.minecraft.network.codec.StreamCodec 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 8 | import net.minecraft.resources.ResourceLocation 9 | import net.neoforged.neoforge.network.handling.IPayloadContext 10 | 11 | class C2SUpdateMemoryInfo(val info: MemoryInfo = MemoryInfo()) : IC2SMessage { 12 | 13 | override fun type(): CustomPacketPayload.Type = TYPE 14 | 15 | companion object { 16 | val TYPE = 17 | CustomPacketPayload.Type( 18 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "update_memory_info") 19 | ) 20 | val STREAM_CODEC: StreamCodec = 21 | StreamCodec.composite( 22 | MemoryInfo.STREAM_CODEC, 23 | C2SUpdateMemoryInfo::info, 24 | ::C2SUpdateMemoryInfo 25 | ) 26 | } 27 | } 28 | 29 | val ServerUpdateMemoryInfoHandler: ((C2SUpdateMemoryInfo, IPayloadContext) -> Unit) = 30 | { message: C2SUpdateMemoryInfo, ctx: IPayloadContext -> 31 | val player = ctx.player() 32 | val stack = player.mainHandItem 33 | if (stack.has(BetterP2P.MEMORY_INFO)) { 34 | stack.update(BetterP2P.MEMORY_INFO.get(), MemoryInfo()) { _ -> message.info } 35 | } 36 | Unit 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BetterP2P 2 | 3 | ![](https://cf.way2muchnoise.eu/versions/538092.svg) ![](https://cf.way2muchnoise.eu/full_538092_downloads.svg) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 4 | 5 | ![logo](logo.png) 6 | 7 | An advanced tool for AE2, to manage P2P networks. 8 | 9 | Supports both **1.20.1 Fabric and Forge!** 10 | 11 | **Fabric**: Requires [Architectury API (fabric)](https://modrinth.com/mod/architectury-api/versions?g=1.20.1&l=fabric), [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin/versions?g=1.20.1) 12 | 13 | **Forge**: Requires [Architectury API (forge)](https://modrinth.com/mod/architectury-api/versions?g=1.20.1&l=forge), [Kotlin for Forge](https://modrinth.com/mod/kotlin-for-forge/versions?l=forge&g=1.20.1) 14 | 15 | **Please choose the corrent corresponding dependencies for your mod loader!** 16 | 17 | Documentation is now available to read: 18 | 19 | ## Older Versions 20 | 21 | 1.7.10 Currently Maintained by GTNH Team: 22 | 23 | 1.12.2 Currently Maintained by AE2UEL: 24 | 25 | **All new features and ideas in 1.20 are based on Betterer P2P, thanks AE2UEL Team!** 26 | 27 | ## TODOs 28 | 29 | - [ ] Config 30 | - [ ] Stability Checks 31 | 32 | ## Credits 33 | 34 | - PnC for its block outline code 35 | - LasmGratel for the first BetterP2P 36 | - GlodBlock for the first 1.7.10 port 37 | - firenoo for the big revamps <3 38 | - AE2UEL Team for Betterer P2P 39 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SLinkP2P.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.ModNetwork 5 | import dev.lasm.betterp2p.network.data.P2PLocation 6 | import io.netty.buffer.ByteBuf 7 | import net.minecraft.network.codec.StreamCodec 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 9 | import net.minecraft.resources.ResourceLocation 10 | import net.neoforged.neoforge.network.handling.IPayloadContext 11 | 12 | class C2SLinkP2P(val input: P2PLocation, val output: P2PLocation) : IC2SMessage { 13 | 14 | override fun type(): CustomPacketPayload.Type = TYPE 15 | 16 | companion object { 17 | val TYPE = 18 | CustomPacketPayload.Type( 19 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "link_p2p") 20 | ) 21 | val STREAM_CODEC: StreamCodec = 22 | StreamCodec.composite( 23 | P2PLocation.STREAM_CODEC, 24 | C2SLinkP2P::input, 25 | P2PLocation.STREAM_CODEC, 26 | C2SLinkP2P::output, 27 | ::C2SLinkP2P 28 | ) 29 | } 30 | } 31 | 32 | val ServerLinkP2PHandler: ((C2SLinkP2P, IPayloadContext) -> Unit) = 33 | { message: C2SLinkP2P, ctx: IPayloadContext -> 34 | ModNetwork.playerState[ctx.player().uuid]?.also { state -> 35 | val result = state.gridCache.linkP2P(message.input, message.output) 36 | 37 | if (result != null) { 38 | ModNetwork.requestP2PUpdate(ctx.player()) 39 | } 40 | } 41 | Unit 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SUnlinkP2P.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.ModNetwork 5 | import dev.lasm.betterp2p.network.data.P2PLocation 6 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 7 | import io.netty.buffer.ByteBuf 8 | import net.minecraft.network.codec.ByteBufCodecs 9 | import net.minecraft.network.codec.StreamCodec 10 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 11 | import net.minecraft.resources.ResourceLocation 12 | import net.neoforged.neoforge.network.handling.IPayloadContext 13 | 14 | /** Unlink input from outputs message (set freq to 0) */ 15 | class C2SUnlinkP2P(val p2p: P2PLocation, val type: Int = TUNNEL_ANY) : IC2SMessage { 16 | override fun type(): CustomPacketPayload.Type = TYPE 17 | 18 | companion object { 19 | val TYPE = 20 | CustomPacketPayload.Type( 21 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "unlink_p2p") 22 | ) 23 | val STREAM_CODEC: StreamCodec = 24 | StreamCodec.composite( 25 | P2PLocation.STREAM_CODEC, 26 | C2SUnlinkP2P::p2p, 27 | ByteBufCodecs.INT, 28 | C2SUnlinkP2P::type, 29 | ::C2SUnlinkP2P 30 | ) 31 | } 32 | } 33 | 34 | /** Client -> C2SUnlinkP2P -> Server Handler on server side */ 35 | val ServerUnlinkP2PHandler: ((C2SUnlinkP2P, IPayloadContext) -> Unit) = 36 | a@{ message: C2SUnlinkP2P, ctx: IPayloadContext -> 37 | val cache = ModNetwork.playerState[ctx.player().uuid] ?: return@a 38 | cache.gridCache.unlinkP2P(message.p2p) 39 | ModNetwork.requestP2PUpdate(ctx.player()) 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/item/BetterMemoryCardModes.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.item 2 | 3 | const val MAX_TOOLTIP_LENGTH = 40 4 | 5 | enum class BetterMemoryCardModes(val unlocalizedName: String, vararg val unlocalizedDesc: String) { 6 | /** Select an input P2P and bind its output */ 7 | OUTPUT( 8 | "gui.advanced_memory_card.mode.output", 9 | "gui.advanced_memory_card.mode.output.desc.1", 10 | "gui.advanced_memory_card.mode.output.desc.2", 11 | "gui.advanced_memory_card.mode.output.desc.3" 12 | ), 13 | 14 | /** Select an output P2P and bind its input */ 15 | INPUT( 16 | "gui.advanced_memory_card.mode.input", 17 | "gui.advanced_memory_card.mode.input.desc.1", 18 | "gui.advanced_memory_card.mode.input.desc.2", 19 | "gui.advanced_memory_card.mode.input.desc.3" 20 | ), 21 | 22 | /** Copy same output frequency */ 23 | COPY( 24 | "gui.advanced_memory_card.mode.copy", 25 | "gui.advanced_memory_card.mode.copy.desc.1", 26 | "gui.advanced_memory_card.mode.copy.desc.2", 27 | "gui.advanced_memory_card.mode.copy.desc.3", 28 | "gui.advanced_memory_card.mode.copy.desc.4" 29 | ), 30 | 31 | /** Unbind/reset frequencies */ 32 | UNBIND( 33 | "gui.advanced_memory_card.mode.unbind", 34 | "gui.advanced_memory_card.mode.unbind.desc.1", 35 | "gui.advanced_memory_card.mode.unbind.desc.2" 36 | ); 37 | 38 | fun next(reverse: Boolean = false): BetterMemoryCardModes { 39 | if (reverse) { 40 | return if (ordinal - 1 < 0) { 41 | values()[values().size - 1] 42 | } else { 43 | values()[(ordinal - 1).rem(values().size)] 44 | } 45 | } 46 | return values()[(ordinal + 1).rem(values().size)] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SChangeP2PType.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.ModNetwork 5 | import dev.lasm.betterp2p.network.data.P2PLocation 6 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 7 | import io.netty.buffer.ByteBuf 8 | import net.minecraft.network.codec.ByteBufCodecs 9 | import net.minecraft.network.codec.StreamCodec 10 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 11 | import net.minecraft.resources.ResourceLocation 12 | import net.neoforged.neoforge.network.handling.IPayloadContext 13 | 14 | class C2SChangeP2PType(val newType: Int = TUNNEL_ANY, val p2p: P2PLocation) : IC2SMessage { 15 | override fun type(): CustomPacketPayload.Type = TYPE 16 | 17 | companion object { 18 | val TYPE = 19 | CustomPacketPayload.Type( 20 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "change_p2p_type") 21 | ) 22 | val STREAM_CODEC: StreamCodec = 23 | StreamCodec.composite( 24 | ByteBufCodecs.INT, 25 | C2SChangeP2PType::newType, 26 | P2PLocation.STREAM_CODEC, 27 | C2SChangeP2PType::p2p, 28 | ::C2SChangeP2PType 29 | ) 30 | } 31 | } 32 | 33 | val ServerTypeChangeHandler: ((C2SChangeP2PType, IPayloadContext) -> Unit) = 34 | a@{ message: C2SChangeP2PType, ctx: IPayloadContext -> 35 | val state = ModNetwork.playerState[ctx.player().uuid] ?: return@a 36 | val type = BetterP2P.proxy.getP2PFromIndex(message.newType) ?: return@a 37 | 38 | if (state.gridCache.changeAllP2Ps(message.p2p, type)) { 39 | ModNetwork.requestP2PList(ctx.player(), type.index) 40 | } 41 | Unit 42 | } 43 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048M 2 | org.gradle.daemon=true 3 | org.gradle.parallel=true 4 | org.gradle.caching=true 5 | org.gradle.configuration-cache=true 6 | 7 | #read more on this at https://github.com/neoforged/NeoGradle/blob/NG_7.0/README.md#apply-parchment-mappings 8 | # you can also find the latest versions at: https://parchmentmc.org/docs/getting-started 9 | neogradle.subsystems.parchment.minecraftVersion=1.21.1 10 | neogradle.subsystems.parchment.mappingsVersion=2024.11.17 11 | 12 | # Environment Properties 13 | # You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge 14 | # The Minecraft version must agree with the Neo version to get a valid artifact 15 | minecraft_version=1.21.1 16 | # The Minecraft version range can use any release version of Minecraft as bounds. 17 | # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly 18 | # as they do not follow standard versioning conventions. 19 | minecraft_version_range=[1.21.1] 20 | # The Neo version must agree with the Minecraft version to get a valid artifact 21 | neo_version=21.1.209 22 | # The Neo version range can use any version of Neo as bounds or match the loader version range 23 | neo_version_range=[21.1.0,) 24 | # The loader version range can only use the major version of FML as bounds 25 | loader_version_range=[1,) 26 | 27 | ae2_version=19.2.7 28 | 29 | # The human-readable display name for the mod. 30 | mod_name=Better P2P 31 | # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. 32 | mod_license=GPLv3 33 | # The authors of the mod. This is a simple text string that is used for display purposes in the mod list. 34 | mod_authors=Lasm Gratel, dyedquartz, AE2UEL Team, GlodBlock, heisluft 35 | # The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. 36 | mod_description=An advanced tool for AE2, to manage P2P networks. 37 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/data/HashHelper.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.data 2 | 3 | import appeng.parts.p2p.P2PTunnelPart 4 | import net.minecraft.core.BlockPos 5 | import net.minecraft.resources.ResourceKey 6 | import net.minecraft.world.level.Level 7 | 8 | /** 9 | * Generates a 64-bit hash code from X, Y, Z, Facing, and Dimension info. bits 0-47: FastHash of (x, 10 | * y, z) -> bits 48-59: dim bits 60-62: facing bit 63: Reserved 11 | */ 12 | fun hashP2P(pos: BlockPos, facing: Int, dim: ResourceKey): Long { 13 | val ret = facing.toULong() shl 60 14 | val lo: ULong = pos.x.toULong() or (pos.y.toULong() shl 32) 15 | val hi: ULong = pos.z.toULong() or (dim.location().hashCode().toULong() shl 32) 16 | var hash = hashLen16(lo, hi) 17 | hash = hash xor (hash shr 59) 18 | return (ret or hash).toLong() 19 | } 20 | 21 | fun hashP2P(p: P2PTunnelPart<*>): Long = 22 | hashP2P(p.blockEntity.blockPos, p.side.ordinal, p.level.dimension()) 23 | 24 | const val k2 = 0x9ae16a3b2f90404fUL 25 | 26 | /** 27 | * Fetches the contiguous byte-aligned 64 bits from the 128 bit "register". Only works for indexes 28 | * 1-7. lo - low 64 bits hi - high 64 bits idx - start byte index, should be 1-7. don't even bother 29 | * using this method for other amounts 30 | */ 31 | private fun fetch64(lo: ULong, hi: ULong, idx: Int): ULong { 32 | return (lo shr (idx * Byte.SIZE_BITS)) or (hi shl ULong.SIZE_BITS - (idx * Byte.SIZE_BITS)) 33 | } 34 | 35 | /** 36 | * City64 hash. It's basically black magic, but here's a link 37 | * https://opensource.googleblog.com/2011/04/introducing-cityhash.html 38 | */ 39 | private fun hashLen16(lo: ULong, hi: ULong): ULong { 40 | val mul: ULong = k2 + 32U 41 | val a: ULong = lo + k2 42 | val b: ULong = hi 43 | val c: ULong = b.rotateRight(37) * mul + a 44 | val d: ULong = (a.rotateRight(25) + b) * mul 45 | 46 | var e: ULong = (c xor d) * mul 47 | e = e xor (e shr 47) 48 | var f: ULong = (d xor e) * mul 49 | f = f xor (f shr 47) 50 | f *= mul 51 | 52 | return f 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/S2CUpdateP2P.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.client.gui.GuiAdvancedMemoryCard 5 | import dev.lasm.betterp2p.network.data.BetterP2PCodecs 6 | import dev.lasm.betterp2p.network.data.P2PInfo 7 | import io.netty.buffer.ByteBuf 8 | import net.minecraft.client.Minecraft 9 | import net.minecraft.network.codec.ByteBufCodecs 10 | import net.minecraft.network.codec.StreamCodec 11 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 12 | import net.minecraft.resources.ResourceLocation 13 | import net.neoforged.neoforge.network.handling.IPayloadContext 14 | 15 | class S2CUpdateP2P(val infos: List = emptyList(), val clear: Boolean = false) : 16 | IS2CMessage { 17 | 18 | override fun type(): CustomPacketPayload.Type = TYPE 19 | 20 | companion object { 21 | val TYPE = 22 | CustomPacketPayload.Type( 23 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "update_p2p") 24 | ) 25 | val STREAM_CODEC: StreamCodec = 26 | StreamCodec.composite( 27 | ByteBufCodecs.collection( 28 | ::ArrayList, 29 | BetterP2PCodecs.P2P_INFO_STREAM, 30 | Int.MAX_VALUE 31 | ), 32 | S2CUpdateP2P::infos, 33 | ByteBufCodecs.BOOL, 34 | S2CUpdateP2P::clear, 35 | ::S2CUpdateP2P 36 | ) 37 | } 38 | } 39 | 40 | val ClientUpdateP2PHandler: ((S2CUpdateP2P, IPayloadContext) -> Unit) = 41 | { message: S2CUpdateP2P, _: IPayloadContext -> 42 | Minecraft.getInstance().submit { 43 | val gui = Minecraft.getInstance().screen 44 | 45 | if (gui is GuiAdvancedMemoryCard) { 46 | if (message.clear) { 47 | gui.refreshInfo(message.infos) 48 | } else { 49 | gui.updateInfo(message.infos) 50 | } 51 | } 52 | } 53 | Unit 54 | } 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.5.2] - 2025-10-23 11 | 12 | ### Fixed 13 | 14 | - Better Memory Card now selects P2P tunnel and type correctly (#30) 15 | 16 | ## [1.5.1] - 2025-10-18 17 | 18 | ### Added 19 | 20 | - NeoForge Support by heisluft 21 | - Update to 1.21.1 22 | 23 | ### Changed 24 | - Migrate from Artifactory loom to NeoGradle 25 | - Update AE2 dependency 26 | - Update Gradle 27 | - Update KotlinForForge 28 | - Migrate Network stack to Stream Codecs 29 | - Migrate ItemNBT to Data Attachments 30 | - Migrate Event Handling to plain NeoForge 31 | - Replace Mixin with Event-based initialization 32 | - Replace redundant network worker with NeoForge Network 33 | 34 | ### Removed 35 | - Fabric Support 36 | - Forge Support 37 | - Architectury dependency 38 | 39 | ## [1.5.0] - 2024-12-23 40 | 41 | ### Added 42 | 43 | - MAE2 Support 44 | - Applied Mekanistics Support 45 | 46 | ### Changed 47 | 48 | - Minimum AE2 version requires 15.3.0-beta 49 | - Default Mode is set to "Bind Input" 50 | - While in "Bind Input" mode, unbound P2P tunnels are sorted on the top of the list 51 | 52 | ### Fixed 53 | 54 | - Massive P2P tunnels cause lag. Now only render outlines of <= 200 tunnels and in 50m range of player 55 | 56 | ## [1.4.3] - 2024-09-19 57 | 58 | ### Fixed 59 | 60 | - Rare issue that crashes client when drawing outline (#19) 61 | 62 | ## [1.4.2] - 2024-07-23 63 | 64 | ### Added 65 | 66 | - Crowdin integration 67 | - Chinese (Simplified) localization 68 | - Search bar tooltip 69 | 70 | ### Fixed 71 | 72 | - Recipe for Advanced Memory Card 73 | 74 | ### Removed 75 | 76 | - Cleanup unused code 77 | 78 | 79 | ## [1.4.1] - 2024-05-21 80 | 81 | ### Fixed 82 | 83 | - Fixed a issue on Forge side that took over vanilla Block Outline render (#11). 84 | 85 | ## [1.4.0] - 2024-05-15 86 | 87 | ### Added 88 | 89 | - 1.20.1 Support. 90 | 91 | [1.4.2]: https://github.com/LasmGratel/BetterP2P/releases/tag/v1.4.2 92 | [1.4.1]: https://github.com/LasmGratel/BetterP2P/releases/tag/v1.4.1 93 | [1.4.0]: https://github.com/LasmGratel/BetterP2P/releases/tag/v1.4.0 94 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/data/P2PLocation.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.data 2 | 3 | import appeng.parts.p2p.P2PTunnelPart 4 | import com.mojang.serialization.Codec 5 | import com.mojang.serialization.codecs.RecordCodecBuilder 6 | import io.netty.buffer.ByteBuf 7 | import net.minecraft.core.BlockPos 8 | import net.minecraft.core.Direction 9 | import net.minecraft.core.registries.Registries 10 | import net.minecraft.network.codec.StreamCodec 11 | import net.minecraft.resources.ResourceKey 12 | import net.minecraft.world.level.Level 13 | 14 | data class P2PLocation(val pos: BlockPos, val facing: Direction, val dim: ResourceKey) { 15 | 16 | companion object { 17 | val CODEC: Codec = 18 | RecordCodecBuilder.create { instance -> 19 | instance 20 | .group( 21 | BlockPos.CODEC.fieldOf("pos").forGetter(P2PLocation::pos), 22 | Direction.CODEC.fieldOf("facing").forGetter(P2PLocation::facing), 23 | ResourceKey.codec(Registries.DIMENSION) 24 | .fieldOf("dim") 25 | .forGetter(P2PLocation::dim) 26 | ) 27 | .apply(instance, ::P2PLocation) 28 | } 29 | val STREAM_CODEC: StreamCodec = 30 | StreamCodec.composite( 31 | BlockPos.STREAM_CODEC, 32 | P2PLocation::pos, 33 | Direction.STREAM_CODEC, 34 | P2PLocation::facing, 35 | ResourceKey.streamCodec(Registries.DIMENSION), 36 | P2PLocation::dim, 37 | ::P2PLocation 38 | ) 39 | } 40 | 41 | override fun hashCode(): Int { 42 | return hashP2P(pos, facing.ordinal, dim).hashCode() 43 | } 44 | 45 | /** Autogenerated equals by IntelliJ */ 46 | override fun equals(other: Any?): Boolean { 47 | if (this === other) return true 48 | if (javaClass != other?.javaClass) return false 49 | 50 | other as P2PLocation 51 | 52 | if (pos != other.pos) return false 53 | if (dim != other.dim) return false 54 | if (facing != other.facing) return false 55 | 56 | return true 57 | } 58 | } 59 | 60 | fun P2PTunnelPart<*>.toLoc() = P2PLocation(blockEntity.blockPos, side, level.dimension()) 61 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/S2COpenGui.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.client.AdvancedMemoryCardMenu 5 | import dev.lasm.betterp2p.client.gui.GuiAdvancedMemoryCard 6 | import dev.lasm.betterp2p.network.data.* 7 | import io.netty.buffer.ByteBuf 8 | import net.minecraft.client.Minecraft 9 | import net.minecraft.network.codec.ByteBufCodecs 10 | import net.minecraft.network.codec.StreamCodec 11 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 12 | import net.minecraft.resources.ResourceLocation 13 | import net.neoforged.neoforge.network.handling.IPayloadContext 14 | 15 | class S2COpenGui( 16 | val infos: List = emptyList(), 17 | val memoryInfo: MemoryInfo = MemoryInfo() 18 | ) : IS2CMessage { 19 | 20 | override fun type(): CustomPacketPayload.Type = TYPE 21 | 22 | companion object { 23 | val TYPE = 24 | CustomPacketPayload.Type( 25 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "open_gui") 26 | ) 27 | val STREAM_CODEC: StreamCodec = 28 | StreamCodec.composite( 29 | ByteBufCodecs.collection( 30 | ::ArrayList, 31 | BetterP2PCodecs.P2P_INFO_STREAM, 32 | Int.MAX_VALUE 33 | ), 34 | S2COpenGui::infos, 35 | MemoryInfo.STREAM_CODEC, 36 | S2COpenGui::memoryInfo, 37 | ::S2COpenGui 38 | ) 39 | } 40 | } 41 | 42 | val ClientOpenGuiHandler: ((S2COpenGui, IPayloadContext) -> Unit) = 43 | { message: S2COpenGui, ctx: IPayloadContext -> 44 | val gui = Minecraft.getInstance().screen 45 | if (gui is GuiAdvancedMemoryCard) { 46 | gui.refreshInfo(message.infos) 47 | gui.memoryInfo = message.memoryInfo 48 | } else { 49 | Minecraft.getInstance().submit { 50 | Minecraft.getInstance() 51 | .setScreen( 52 | GuiAdvancedMemoryCard( 53 | AdvancedMemoryCardMenu(0, null).also { 54 | it.memoryInfo = message.memoryInfo 55 | it.infos = message.infos 56 | } 57 | ) 58 | ) 59 | } 60 | } 61 | Unit 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/data/MemoryInfo.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.data 2 | 3 | import com.mojang.serialization.Codec 4 | import com.mojang.serialization.codecs.RecordCodecBuilder 5 | import dev.lasm.betterp2p.client.gui.widget.GuiScale 6 | import dev.lasm.betterp2p.item.BetterMemoryCardModes 7 | import io.netty.buffer.ByteBuf 8 | import java.util.Optional 9 | import net.minecraft.network.codec.ByteBufCodecs 10 | import net.minecraft.network.codec.StreamCodec 11 | 12 | const val TUNNEL_ANY: Int = -1 13 | 14 | data class MemoryInfo( 15 | val selectedEntry: Optional = Optional.empty(), 16 | val frequency: Short = 0, 17 | val mode: BetterMemoryCardModes = BetterMemoryCardModes.OUTPUT, 18 | val guiScale: GuiScale = GuiScale.DYNAMIC, 19 | val type: Int = TUNNEL_ANY 20 | ) { 21 | companion object { 22 | val STREAM_CODEC: StreamCodec = 23 | StreamCodec.composite( 24 | ByteBufCodecs.optional(P2PLocation.STREAM_CODEC), 25 | MemoryInfo::selectedEntry, 26 | ByteBufCodecs.SHORT, 27 | MemoryInfo::frequency, 28 | ByteBufCodecs.INT.map( 29 | BetterMemoryCardModes.values()::get, 30 | BetterMemoryCardModes::ordinal 31 | ), 32 | MemoryInfo::mode, 33 | ByteBufCodecs.INT.map(GuiScale.values()::get, GuiScale::ordinal), 34 | MemoryInfo::guiScale, 35 | ByteBufCodecs.INT, 36 | MemoryInfo::type, 37 | ::MemoryInfo 38 | ) 39 | 40 | val CODEC: Codec = 41 | RecordCodecBuilder.create { instance -> 42 | instance 43 | .group( 44 | Codec.optionalField("selectedEntry", P2PLocation.CODEC, false) 45 | .forGetter(MemoryInfo::selectedEntry), 46 | Codec.SHORT.fieldOf("frequency").forGetter(MemoryInfo::frequency), 47 | Codec.INT.xmap( 48 | BetterMemoryCardModes.values()::get, 49 | BetterMemoryCardModes::ordinal 50 | ) 51 | .fieldOf("mode") 52 | .forGetter(MemoryInfo::mode), 53 | Codec.INT.xmap(GuiScale.values()::get, GuiScale::ordinal) 54 | .fieldOf("guiScale") 55 | .forGetter(MemoryInfo::guiScale) 56 | ) 57 | .apply(instance, ::MemoryInfo) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/WidgetScrollBar.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import net.minecraft.client.gui.GuiGraphics 4 | import net.minecraft.client.gui.components.AbstractWidget 5 | import net.minecraft.client.gui.narration.NarrationElementOutput 6 | import net.minecraft.network.chat.Component 7 | import net.minecraft.resources.ResourceLocation 8 | 9 | class WidgetScrollBar(x: Int, y: Int) : AbstractWidget(x, y, 12, 15, Component.empty()) { 10 | var pageSize = 1 11 | 12 | var maxScroll = 0 13 | var minScroll = 0 14 | 15 | var onScroll: () -> Unit = {} 16 | 17 | var currentScroll = 0 18 | 19 | val SCROLLER = ResourceLocation.withDefaultNamespace("container/creative_inventory/scroller") 20 | val SCROLLER_DISABLED = 21 | ResourceLocation.withDefaultNamespace("container/creative_inventory/scroller_disabled") 22 | 23 | override fun renderWidget(graphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { 24 | if (getRange() == 0) { 25 | graphics.blitSprite(SCROLLER_DISABLED, x, y, 12, 15) 26 | } else { 27 | val offset = (currentScroll - minScroll) * (height - 15) / getRange() 28 | graphics.blitSprite(SCROLLER, x, offset + y, 12, 15) 29 | } 30 | } 31 | 32 | private fun getRange(): Int { 33 | return maxScroll - minScroll 34 | } 35 | 36 | fun setRange(min: Int, max: Int, pageSize: Int) { 37 | minScroll = min 38 | maxScroll = max 39 | this.pageSize = pageSize 40 | if (minScroll > maxScroll) { 41 | maxScroll = minScroll 42 | } 43 | applyRange() 44 | } 45 | 46 | private fun applyRange() { 47 | currentScroll = currentScroll.coerceIn(minScroll, maxScroll) 48 | onScroll() 49 | } 50 | 51 | override fun onDrag(mouseX: Double, mouseY: Double, dragX: Double, dragY: Double) { 52 | if (getRange() == 0) { 53 | return 54 | } 55 | currentScroll = (mouseY - y).toInt() 56 | currentScroll = minScroll + currentScroll * 2 * getRange() / height 57 | currentScroll = currentScroll + 1 shr 1 58 | applyRange() 59 | super.onDrag(mouseX, mouseY, dragX, dragY) 60 | } 61 | 62 | override fun mouseScrolled( 63 | mouseX: Double, 64 | mouseY: Double, 65 | scrollX: Double, 66 | scrollY: Double 67 | ): Boolean { 68 | var delta = scrollY.toInt() 69 | delta = (-delta).coerceIn(-1, 1) 70 | currentScroll += delta 71 | applyRange() 72 | return true 73 | } 74 | 75 | override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {} 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/IconButton.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import appeng.client.gui.widgets.ITooltip 4 | import com.mojang.blaze3d.systems.RenderSystem 5 | import dev.lasm.betterp2p.client.gui.GUI_TEX_HEIGHT 6 | import dev.lasm.betterp2p.client.gui.GUI_WIDTH 7 | import dev.lasm.betterp2p.client.gui.TEXTURE 8 | import dev.lasm.betterp2p.client.gui.drawTexturedQuad 9 | import net.minecraft.client.gui.GuiGraphics 10 | import net.minecraft.client.gui.components.Button 11 | import net.minecraft.client.renderer.Rect2i 12 | import net.minecraft.network.chat.Component 13 | 14 | open class IconButton(var texX: Int, var texY: Int, onPress: OnPress) : 15 | Button(0, 0, 32, 32, Component.empty(), onPress, DEFAULT_NARRATION), ITooltip { 16 | 17 | var messages = mutableListOf(message) 18 | 19 | public override fun renderWidget( 20 | guiGraphics: GuiGraphics, 21 | mouseX: Int, 22 | mouseY: Int, 23 | partial: Float 24 | ) { 25 | RenderSystem.enableBlend() 26 | RenderSystem.enableDepthTest() 27 | renderBackground(guiGraphics, mouseY, mouseY, partial) 28 | 29 | guiGraphics.drawTexturedQuad( 30 | TEXTURE, 31 | x0 = x + 1.0f, 32 | y0 = y + 1.0f, 33 | x1 = x + width - 1.0f, 34 | y1 = y + height - 1.0f, 35 | u0 = texX / GUI_WIDTH.toFloat(), 36 | v0 = texY / GUI_TEX_HEIGHT.toFloat(), 37 | u1 = (texX + width) / GUI_WIDTH.toFloat(), 38 | v1 = (texY + height) / GUI_TEX_HEIGHT.toFloat() 39 | ) 40 | } 41 | 42 | fun getHoverState(): Int { 43 | var i = 1 44 | if (!this.active) { 45 | i = 0 46 | } else if (this.isHovered) { 47 | i = 2 48 | } 49 | return i 50 | } 51 | 52 | fun renderBackground(graphics: GuiGraphics, mouseX: Int, mouseY: Int, partial: Float) { 53 | val k = getHoverState() 54 | graphics.drawTexturedQuad( 55 | TEXTURE, 56 | x.toFloat(), 57 | y.toFloat(), 58 | (x + width).toFloat(), 59 | (y + height).toFloat(), 60 | u0 = (32.0f * k) / GUI_WIDTH, 61 | v0 = (232.0f) / GUI_TEX_HEIGHT, 62 | u1 = (32.0f * (k + 1)) / GUI_WIDTH, 63 | v1 = (232.0f + height) / GUI_TEX_HEIGHT 64 | ) 65 | } 66 | 67 | override fun getTooltipMessage(): List { 68 | return messages 69 | } 70 | 71 | override fun getTooltipArea(): Rect2i { 72 | return Rect2i(x, y, width, height) 73 | } 74 | 75 | override fun isTooltipAreaVisible(): Boolean { 76 | return this.visible 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/packet/C2SRenameP2P.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.packet 2 | 3 | import appeng.api.networking.IInWorldGridNodeHost 4 | import appeng.api.parts.IPartHost 5 | import appeng.parts.p2p.P2PTunnelPart 6 | import dev.lasm.betterp2p.BetterP2P 7 | import dev.lasm.betterp2p.network.ModNetwork 8 | import dev.lasm.betterp2p.network.data.P2PLocation 9 | import dev.lasm.betterp2p.network.data.toLoc 10 | import dev.lasm.betterp2p.util.p2p.setCustomName 11 | import io.netty.buffer.ByteBuf 12 | import net.minecraft.network.chat.Component 13 | import net.minecraft.network.codec.ByteBufCodecs 14 | import net.minecraft.network.codec.StreamCodec 15 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 16 | import net.minecraft.resources.ResourceLocation 17 | import net.neoforged.neoforge.network.handling.IPayloadContext 18 | 19 | class C2SRenameP2P(val p2p: P2PLocation, val name: String) : IC2SMessage { 20 | override fun type(): CustomPacketPayload.Type = TYPE 21 | 22 | companion object { 23 | val TYPE = 24 | CustomPacketPayload.Type( 25 | ResourceLocation.fromNamespaceAndPath(BetterP2P.MOD_ID, "rename_p2p") 26 | ) 27 | val STREAM_CODEC: StreamCodec = 28 | StreamCodec.composite( 29 | P2PLocation.STREAM_CODEC, 30 | C2SRenameP2P::p2p, 31 | ByteBufCodecs.STRING_UTF8, 32 | C2SRenameP2P::name, 33 | ::C2SRenameP2P 34 | ) 35 | } 36 | } 37 | 38 | val ServerRenameP2PTunnelHandler: (C2SRenameP2P, IPayloadContext) -> Unit = 39 | a@{ message: C2SRenameP2P, ctx: IPayloadContext -> 40 | val player = ctx.player() 41 | 42 | val world = player.server?.getLevel(message.p2p.dim) ?: return@a 43 | val te = world.getChunkAt(message.p2p.pos).getBlockEntity(message.p2p.pos) ?: return@a 44 | val state = ModNetwork.playerState[player.uuid] ?: return@a 45 | val facing = message.p2p.facing 46 | 47 | if (te is IInWorldGridNodeHost && te is IPartHost && te.getGridNode(facing) != null) { 48 | val partTunnel = te.getPart(facing) 49 | 50 | if (partTunnel is P2PTunnelPart<*>) { 51 | partTunnel.setCustomName(Component.literal(message.name)) 52 | val input: P2PTunnelPart<*> = 53 | if (partTunnel.isOutput) { 54 | partTunnel.getInput()!! 55 | } else { 56 | partTunnel 57 | } 58 | // Mark all dirty 59 | input.outputs.forEach { state.gridCache.markDirty(it.toLoc(), it) } 60 | state.gridCache.markDirty(input.toLoc(), input) 61 | 62 | ModNetwork.requestP2PUpdate(player) 63 | } 64 | } 65 | Unit 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/GuiHelper.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem 4 | import com.mojang.blaze3d.vertex.BufferUploader 5 | import com.mojang.blaze3d.vertex.DefaultVertexFormat 6 | import com.mojang.blaze3d.vertex.Tesselator 7 | import com.mojang.blaze3d.vertex.VertexFormat 8 | import net.minecraft.client.gui.GuiGraphics 9 | import net.minecraft.client.gui.components.AbstractWidget 10 | import net.minecraft.client.renderer.GameRenderer 11 | import net.minecraft.resources.ResourceLocation 12 | import org.joml.Matrix4f 13 | 14 | fun GuiGraphics.drawTexturedQuad( 15 | texture: ResourceLocation, 16 | x0: Float, 17 | y0: Float, 18 | x1: Float, 19 | y1: Float, 20 | u0: Float, 21 | v0: Float, 22 | u1: Float, 23 | v1: Float 24 | ) { 25 | RenderSystem.setShader { GameRenderer.getPositionTexShader() } 26 | RenderSystem.setShaderTexture(0, texture) 27 | val matrix4f: Matrix4f = this.pose().last().pose() 28 | val bufferBuilder = 29 | Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX) 30 | bufferBuilder.addVertex(matrix4f, x0, y1, 0.0f).setUv(u0, v1) 31 | bufferBuilder.addVertex(matrix4f, x1, y1, 0.0f).setUv(u1, v1) 32 | bufferBuilder.addVertex(matrix4f, x1, y0, 0.0f).setUv(u1, v0) 33 | bufferBuilder.addVertex(matrix4f, x0, y0, 0.0f).setUv(u0, v0) 34 | BufferUploader.drawWithShader(bufferBuilder.build()!!) 35 | } 36 | 37 | fun GuiGraphics.drawIcon(srcX: Int, srcY: Int, x: Int, y: Int, width: Int = 16, height: Int = 16) { 38 | blit(TEXTURE, x, y, 0, srcX.toFloat(), srcY.toFloat(), width, height, 288, 264) 39 | } 40 | 41 | fun AbstractWidget.isClicked(mouseX: Double, mouseY: Double) = 42 | this.isActive && 43 | this.visible && 44 | (mouseX >= x.toDouble()) && 45 | (mouseY >= y.toDouble()) && 46 | (mouseX < (this.x + this.width).toDouble()) && 47 | (mouseY < (this.y + this.height).toDouble()) 48 | 49 | fun drawBlockIcon( 50 | graphics: GuiGraphics, 51 | icon: ResourceLocation, 52 | overlay: ResourceLocation = 53 | ResourceLocation.fromNamespaceAndPath("ae2", "textures/part/p2p_tunnel_front.png"), 54 | x: Int, 55 | y: Int, 56 | width: Int = 16, 57 | height: Int = 16 58 | ) { 59 | graphics.drawTexturedQuad( 60 | icon, 61 | x0 = x.toFloat() + 2, 62 | y0 = y.toFloat() + 2, 63 | x1 = x.toFloat() + width - 2, 64 | y1 = y.toFloat() + height - 2, 65 | u0 = 0.0f, 66 | v0 = 0.0f, 67 | u1 = 1.0f, 68 | v1 = 1.0f 69 | ) 70 | 71 | graphics.drawTexturedQuad( 72 | overlay, 73 | x0 = x.toFloat(), 74 | y0 = y.toFloat(), 75 | x1 = x.toFloat() + width, 76 | y1 = y.toFloat() + height, 77 | u0 = 0.0f, 78 | v0 = 0.0f, 79 | u1 = 1.0f, 80 | v1 = 1.0f 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.betterp2p.advanced_memory_card": "高级内存卡", 3 | "item.betterp2p.advanced_memory_card.selected": "频率:", 4 | "gui.advanced_memory_card.select": "选择", 5 | "gui.advanced_memory_card.bind": "绑定", 6 | "gui.advanced_memory_card.unbind": "解绑", 7 | "gui.advanced_memory_card.pos": "位置:%d, %d, %d", 8 | "gui.advanced_memory_card.side": "朝向:%s", 9 | "gui.advanced_memory_card.dim": "世界: %d", 10 | "gui.advanced_memory_card.name": "名称: %s", 11 | "gui.advanced_memory_card.sortinfo1": "过滤器标签:", 12 | "gui.advanced_memory_card.sortinfo2": "仅显示输入类型的P2P通道", 13 | "gui.advanced_memory_card.sortinfo3": "仅显示输出类型的P2P通道", 14 | "gui.advanced_memory_card.sortinfo4": "仅显示已绑定的P2P通道", 15 | "gui.advanced_memory_card.sortinfo5": "仅显示未绑定的P2P通道", 16 | "gui.advanced_memory_card.sortinfo6": "过滤P2P类型", 17 | "gui.advanced_memory_card.sortinfo7": "过滤标签用空格隔开,用AND来合并。", 18 | "gui.advanced_memory_card.extra.channel": "使用的频道数: %s", 19 | "gui.advanced_memory_card.mode.input": "模式:绑定输入", 20 | "gui.advanced_memory_card.mode.input.desc.1": "• 在绑定连接前,§a选中的P2P通道(若为输出模式)§7和§b被绑定目标§7的所有连接都将被移除。", 21 | "gui.advanced_memory_card.mode.input.desc.2": "• §a选中的P2P通道§7变为§6输出模式§7,而§b被绑定目标§7将变为§9输入模式§7。", 22 | "gui.advanced_memory_card.mode.input.desc.3": "• §a选中的P2P通道§7变成与其新输入相同的P2P类型,§b被绑定目标§7将同时加入其输出列表。", 23 | "gui.advanced_memory_card.mode.output": "模式:绑定输出", 24 | "gui.advanced_memory_card.mode.output.desc.1": "• 在绑定前,§a选中的P2P通道§7(如果是输出模式) 及§b被绑定目标§7的所有连接都将被移除。", 25 | "gui.advanced_memory_card.mode.output.desc.2": "• §a选中的P2P通道§7将变为§9输入模式§7,而§b被绑定目标§7将变为§6输出模式§7。", 26 | "gui.advanced_memory_card.mode.output.desc.3": "• §b被绑定目标§7将变成与其新输入相同的P2P类型,并添加到输出列表中。", 27 | "gui.advanced_memory_card.mode.copy": "模式:复制输出", 28 | "gui.advanced_memory_card.mode.copy.desc.1": "• §a选定的P2P通道§7必须是§9输入模式。", 29 | "gui.advanced_memory_card.mode.copy.desc.2": "• 在绑定前,§b被绑定的P2P§7将断开所有连接。", 30 | "gui.advanced_memory_card.mode.copy.desc.3": "• §a选中的§aP2P§7变为§9输入模式§7,而§b被绑定的目标P2P通道§7被添加到§a选中§aP2P频率的§6输出端口§7。", 31 | "gui.advanced_memory_card.mode.copy.desc.4": "• §b被绑定的P2P§7变成与其新输入相同的类型。", 32 | "gui.advanced_memory_card.mode.unbind": "模式:解绑", 33 | "gui.advanced_memory_card.mode.unbind.desc.1": "• 禁用所有功能,您不能再绑定P2P。", 34 | "gui.advanced_memory_card.mode.unbind.desc.2": "• 所有绑定的 P2P 通道都将解绑,频率将被重置并恢复§9输入模式§7。", 35 | "gui.advanced_memory_card.desc.not_set": "未配置", 36 | "gui.advanced_memory_card.desc.effect": "效果:", 37 | "gui.advanced_memory_card.gui_scale.small": "小", 38 | "gui.advanced_memory_card.gui_scale.normal": "中", 39 | "gui.advanced_memory_card.gui_scale.large": "大", 40 | "gui.advanced_memory_card.gui_scale.dynamic": "自动", 41 | "gui.advanced_memory_card.p2p_status.bound": "已绑定", 42 | "gui.advanced_memory_card.p2p_status.unbound": "未绑定", 43 | "gui.advanced_memory_card.p2p_status.input": "输入", 44 | "gui.advanced_memory_card.p2p_status.output": "输出", 45 | "gui.advanced_memory_card.p2p_status.offline": "设备关闭", 46 | "gui.advanced_memory_card.types.filtered": "正显示: %s", 47 | "gui.advanced_memory_card.types.any": "§a全部", 48 | "gui.advanced_memory_card.error.same_type": "P2P已经是此种类型了。" 49 | } 50 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/lang/ja_jp.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.betterp2p.advanced_memory_card": "発展型メモリーカード", 3 | "item.betterp2p.advanced_memory_card.selected": "周波数:", 4 | "gui.advanced_memory_card.select": "選択", 5 | "gui.advanced_memory_card.bind": "バインド", 6 | "gui.advanced_memory_card.unbind": "バインド解除", 7 | "gui.advanced_memory_card.pos": "位置: %d, %d, %d", 8 | "gui.advanced_memory_card.side": "大きさ: %s", 9 | "gui.advanced_memory_card.dim": "ディメンション %d", 10 | "gui.advanced_memory_card.name": "名前: %s", 11 | "gui.advanced_memory_card.sortinfo1": "タグで並び替え", 12 | "gui.advanced_memory_card.sortinfo2": "入力P2Pのみ表示", 13 | "gui.advanced_memory_card.sortinfo3": "出力P2Pのみ表示", 14 | "gui.advanced_memory_card.sortinfo4": "バインド済みP2Pのみ表示", 15 | "gui.advanced_memory_card.sortinfo5": "未バインドP2Pのみ表示", 16 | "gui.advanced_memory_card.sortinfo6": "タイプ名でフィルタリング", 17 | "gui.advanced_memory_card.sortinfo7": "タグはスペースで区切られ、AND演算子で結合されます。", 18 | "gui.advanced_memory_card.extra.channel": "使用チャンネル数: %s", 19 | "gui.advanced_memory_card.mode.input": "現在のモード:入力にバインド", 20 | "gui.advanced_memory_card.mode.input.desc.1": "• バインドする前に、§a選択された§aP2P§7とその§bバインド§b対象§7(出力の場合)の接続が削除されます。", 21 | "gui.advanced_memory_card.mode.input.desc.2": "• §a選択されたP2P§7は§6出力§7となり、§bバインドされた§b対象§7は§9入力§7となります。", 22 | "gui.advanced_memory_card.mode.input.desc.3": "• §a選択されたP2P§7は新しい入力と同じ種類になり、§bバインド§7対象§7が出力一覧に追加されます。", 23 | "gui.advanced_memory_card.mode.output": "現在のモード:出力にバインド", 24 | "gui.advanced_memory_card.mode.output.desc.1": "• バインドする前に、§a選択された§aP2P§7(出力の場合)とその§bバインド§b対象§7のバインドが削除されます。", 25 | "gui.advanced_memory_card.mode.output.desc.2": "• §a選択されたP2P§7は§9入力§7となり、§bバインドされた§b対象§7は§6出力§7となります。", 26 | "gui.advanced_memory_card.mode.output.desc.3": "• §bバインド§7対象§7は新しい入力と同じ種類になり、§a選択されたP2P§7が出力一覧に追加されます。", 27 | "gui.advanced_memory_card.mode.copy": "現在のモード:入力を出力にコピー", 28 | "gui.advanced_memory_card.mode.copy.desc.1": "• §a選択されたP2P§7は§9入力である必要があります。", 29 | "gui.advanced_memory_card.mode.copy.desc.2": "• バインドする前に、§バインド§対象§7の接続が削除されます。", 30 | "gui.advanced_memory_card.mode.copy.desc.3": "• §選択された§aP2P§7は§9入力§7となり、§bバインド§b対象§7は§選択された§aP2Pの§6出力§7一覧に追加されます。", 31 | "gui.advanced_memory_card.mode.copy.desc.4": "• §bバインド§7対象§7は新しい入力と同じ種類になります。", 32 | "gui.advanced_memory_card.mode.unbind": "現在のモード:バインド解除", 33 | "gui.advanced_memory_card.mode.unbind.desc.1": "• 通常の機能を無効にします。P2Pをバインドできなくなります。", 34 | "gui.advanced_memory_card.mode.unbind.desc.2": "• バインドされたすべてのP2Pはバインド解除され、周波数がリセットされ、§9入力§7に変換されます。", 35 | "gui.advanced_memory_card.desc.not_set": "未設定", 36 | "gui.advanced_memory_card.desc.effect": "エフェクト:", 37 | "gui.advanced_memory_card.gui_scale.small": "現在の大きさ:小", 38 | "gui.advanced_memory_card.gui_scale.normal": "現在の大きさ:普通", 39 | "gui.advanced_memory_card.gui_scale.large": "現在の大きさ:大", 40 | "gui.advanced_memory_card.gui_scale.dynamic": "現在の大きさ:自動", 41 | "gui.advanced_memory_card.p2p_status.bound": "バインド", 42 | "gui.advanced_memory_card.p2p_status.unbound": "未バインド", 43 | "gui.advanced_memory_card.p2p_status.input": "入力", 44 | "gui.advanced_memory_card.p2p_status.output": "出力", 45 | "gui.advanced_memory_card.p2p_status.offline": "オフライン", 46 | "gui.advanced_memory_card.types.filtered": "表示: %s", 47 | "gui.advanced_memory_card.types.any": "§a全て", 48 | "gui.advanced_memory_card.error.same_type": "P2Pはすでにこの種類です。" 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/item/ItemAdvancedMemoryCard.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.item 2 | 3 | import appeng.api.networking.IInWorldGridNodeHost 4 | import appeng.api.parts.IPartHost 5 | import appeng.parts.p2p.P2PTunnelPart 6 | import dev.lasm.betterp2p.BetterP2P 7 | import dev.lasm.betterp2p.client.ClientCache 8 | import dev.lasm.betterp2p.network.ModNetwork 9 | import dev.lasm.betterp2p.network.data.* 10 | import dev.lasm.betterp2p.util.p2p.getTypeIndex 11 | import java.util.Optional 12 | import net.minecraft.network.chat.Component 13 | import net.minecraft.world.InteractionHand 14 | import net.minecraft.world.InteractionResult 15 | import net.minecraft.world.InteractionResultHolder 16 | import net.minecraft.world.entity.player.Player 17 | import net.minecraft.world.item.Item 18 | import net.minecraft.world.item.ItemStack 19 | import net.minecraft.world.item.TooltipFlag 20 | import net.minecraft.world.item.context.UseOnContext 21 | import net.minecraft.world.level.Level 22 | 23 | object ItemAdvancedMemoryCard : 24 | Item(Properties().stacksTo(1).component(BetterP2P.MEMORY_INFO, MemoryInfo())) { 25 | 26 | override fun appendHoverText( 27 | stack: ItemStack, 28 | context: TooltipContext, 29 | list: MutableList, 30 | tooltipFlag: TooltipFlag 31 | ) { 32 | val info = stack.components.get(BetterP2P.MEMORY_INFO.get())!! 33 | list.add( 34 | Component.translatable("gui.advanced_memory_card.mode.${info.mode.name.lowercase()}") 35 | ) 36 | } 37 | 38 | override fun use( 39 | level: Level, 40 | player: Player, 41 | interactionHand: InteractionHand 42 | ): InteractionResultHolder { 43 | if (player.isCrouching && !level.isClientSide) { 44 | ClientCache.clear() 45 | return InteractionResultHolder.success(player.getItemInHand(interactionHand)) 46 | } 47 | return super.use(level, player, interactionHand) 48 | } 49 | 50 | override fun useOn(useOnContext: UseOnContext): InteractionResult { 51 | val w = useOnContext.level 52 | val player = useOnContext.player!! 53 | val stack = useOnContext.itemInHand 54 | val pos = useOnContext.clickedPos 55 | 56 | if (w.isClientSide) return InteractionResult.PASS 57 | 58 | val te = w.getBlockEntity(pos) 59 | if (te is IInWorldGridNodeHost && te is IPartHost) { 60 | val part = te.selectPartWorld(useOnContext.clickLocation).part ?: te.getPart(null) 61 | val grid = part?.gridNode?.grid ?: return InteractionResult.FAIL 62 | 63 | val info = stack.components.get(BetterP2P.MEMORY_INFO.get())!! 64 | val selectedEntry: Optional 65 | val type: Int 66 | if (part is P2PTunnelPart<*>) { 67 | type = part.getTypeIndex() 68 | selectedEntry = Optional.of(part.toLoc()) 69 | } else { 70 | type = TUNNEL_ANY 71 | selectedEntry = Optional.empty() 72 | } 73 | val info1 = MemoryInfo(selectedEntry, info.frequency, info.mode, info.guiScale, type) 74 | stack.update(BetterP2P.MEMORY_INFO, MemoryInfo()) { info -> info1 } 75 | ModNetwork.initConnection(player, grid, info1) 76 | return InteractionResult.SUCCESS 77 | } 78 | 79 | return InteractionResult.PASS 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/data/BetterP2PCodecs.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.data 2 | 3 | import com.mojang.datafixers.util.Function9 4 | import io.netty.buffer.ByteBuf 5 | import java.util.function.Function 6 | import net.minecraft.core.BlockPos 7 | import net.minecraft.core.Direction 8 | import net.minecraft.core.registries.Registries 9 | import net.minecraft.network.codec.ByteBufCodecs 10 | import net.minecraft.network.codec.StreamCodec 11 | import net.minecraft.resources.ResourceKey 12 | 13 | object BetterP2PCodecs { 14 | 15 | val P2P_INFO_STREAM: StreamCodec = 16 | composite( 17 | ByteBufCodecs.SHORT, 18 | P2PInfo::frequency, 19 | BlockPos.STREAM_CODEC, 20 | P2PInfo::pos, 21 | ResourceKey.streamCodec(Registries.DIMENSION), 22 | P2PInfo::dim, 23 | Direction.STREAM_CODEC, 24 | P2PInfo::facing, 25 | ByteBufCodecs.STRING_UTF8, 26 | P2PInfo::name, 27 | ByteBufCodecs.BOOL, 28 | P2PInfo::output, 29 | ByteBufCodecs.BOOL, 30 | P2PInfo::hasChannel, 31 | ByteBufCodecs.INT, 32 | P2PInfo::channels, 33 | ByteBufCodecs.INT, 34 | P2PInfo::type, 35 | ::P2PInfo 36 | ) 37 | 38 | fun composite( 39 | codec1: StreamCodec, 40 | getter1: Function, 41 | codec2: StreamCodec, 42 | getter2: Function, 43 | codec3: StreamCodec, 44 | getter3: Function, 45 | codec4: StreamCodec, 46 | getter4: Function, 47 | codec5: StreamCodec, 48 | getter5: Function, 49 | codec6: StreamCodec, 50 | getter6: Function, 51 | codec7: StreamCodec, 52 | getter7: Function, 53 | codec8: StreamCodec, 54 | getter8: Function, 55 | codec9: StreamCodec, 56 | getter9: Function, 57 | factory: Function9 58 | ): StreamCodec { 59 | return object : StreamCodec { 60 | override fun decode(buffer: B): C { 61 | val t1 = codec1.decode(buffer) 62 | val t2 = codec2.decode(buffer) 63 | val t3 = codec3.decode(buffer) 64 | val t4 = codec4.decode(buffer) 65 | val t5 = codec5.decode(buffer) 66 | val t6 = codec6.decode(buffer) 67 | val t7 = codec7.decode(buffer) 68 | val t8 = codec8.decode(buffer) 69 | val t9 = codec9.decode(buffer) 70 | return factory.apply(t1, t2, t3, t4, t5, t6, t7, t8, t9) 71 | } 72 | 73 | override fun encode(buffer: B, value: C) { 74 | codec1.encode(buffer, getter1.apply(value)) 75 | codec2.encode(buffer, getter2.apply(value)) 76 | codec3.encode(buffer, getter3.apply(value)) 77 | codec4.encode(buffer, getter4.apply(value)) 78 | codec5.encode(buffer, getter5.apply(value)) 79 | codec6.encode(buffer, getter6.apply(value)) 80 | codec7.encode(buffer, getter7.apply(value)) 81 | codec8.encode(buffer, getter8.apply(value)) 82 | codec9.encode(buffer, getter9.apply(value)) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.betterp2p.advanced_memory_card": "Advanced Memory Card", 3 | "item.betterp2p.advanced_memory_card.selected": "Frequency:", 4 | "gui.advanced_memory_card.select": "Select", 5 | "gui.advanced_memory_card.bind": "Bind", 6 | "gui.advanced_memory_card.unbind": "Unbind", 7 | "gui.advanced_memory_card.pos": "Pos: %d, %d, %d", 8 | "gui.advanced_memory_card.side": "Side: %s", 9 | "gui.advanced_memory_card.dim": "Dim: %d", 10 | "gui.advanced_memory_card.name": "Name: %s", 11 | "gui.advanced_memory_card.sortinfo1": "Sorting tags", 12 | "gui.advanced_memory_card.sortinfo2": "Show Input P2Ps only", 13 | "gui.advanced_memory_card.sortinfo3": "Show Output P2Ps only", 14 | "gui.advanced_memory_card.sortinfo4": "Show Bound P2Ps only", 15 | "gui.advanced_memory_card.sortinfo5": "Show Unbound P2Ps only", 16 | "gui.advanced_memory_card.sortinfo6": "Filter by type name(s)", 17 | "gui.advanced_memory_card.sortinfo7": "Tags are space separated and are combined w/ the AND operator.", 18 | "gui.advanced_memory_card.extra.channel": "Channels used: %s", 19 | "gui.advanced_memory_card.mode.input": "Current Mode: Bind to Input", 20 | "gui.advanced_memory_card.mode.input.desc.1": "• Before binding, the §aselected §aP2P §7and §bbind §btarget §7(if output) have their connections removed.", 21 | "gui.advanced_memory_card.mode.input.desc.2": "• The §aselected P2P §7becomes the the §6output§7, while the §bbind §btarget §7becomes the §9input§7.", 22 | "gui.advanced_memory_card.mode.input.desc.3": "• The §aselected P2P §7becomes the same type as its new input, and the §bbind §btarget §7is added to the output list.", 23 | "gui.advanced_memory_card.mode.output": "Current Mode: Bind to Output", 24 | "gui.advanced_memory_card.mode.output.desc.1": "• Before binding, the §aselected §aP2P §7(if output) and §bbind §btarget §7have their connections removed.", 25 | "gui.advanced_memory_card.mode.output.desc.2": "• The §aselected P2P §7becomes the the §9input§7, while the §bbind §btarget §7becomes the §6output§7.", 26 | "gui.advanced_memory_card.mode.output.desc.3": "• The §bbind target §7becomes the same type as its new input, and is added to the output list.", 27 | "gui.advanced_memory_card.mode.copy": "Current Mode: Copy Input to Output", 28 | "gui.advanced_memory_card.mode.copy.desc.1": "• §aSelected P2P §7must be an §9input.", 29 | "gui.advanced_memory_card.mode.copy.desc.2": "• Before binding, the §bbind §btarget §7has its connections removed.", 30 | "gui.advanced_memory_card.mode.copy.desc.3": "• The §aselected §aP2P §7becomes the §9input§7, while the §bbind §btarget §7is added to the list of the §aselected §aP2P's §6outputs§7.", 31 | "gui.advanced_memory_card.mode.copy.desc.4": "• The §bbind target §7becomes the same type as its new input.", 32 | "gui.advanced_memory_card.mode.unbind": "Current Mode: Unbind", 33 | "gui.advanced_memory_card.mode.unbind.desc.1": "• Disables normal functions; you can no longer bind P2Ps.", 34 | "gui.advanced_memory_card.mode.unbind.desc.2": "• All bound P2Ps are now unbindable, which resets its frequency and converts it back to an §9input§7.", 35 | "gui.advanced_memory_card.desc.not_set": "Not set", 36 | "gui.advanced_memory_card.desc.effect": "Effect:", 37 | "gui.advanced_memory_card.gui_scale.small": "Current Size: Small", 38 | "gui.advanced_memory_card.gui_scale.normal": "Current Size: Standard", 39 | "gui.advanced_memory_card.gui_scale.large": "Current Size: Large", 40 | "gui.advanced_memory_card.gui_scale.dynamic": "Current Size: Automatic", 41 | "gui.advanced_memory_card.p2p_status.bound": "Bound", 42 | "gui.advanced_memory_card.p2p_status.unbound": "Unbound", 43 | "gui.advanced_memory_card.p2p_status.input": "Input", 44 | "gui.advanced_memory_card.p2p_status.output": "Output", 45 | "gui.advanced_memory_card.p2p_status.offline": "Offline", 46 | "gui.advanced_memory_card.types.filtered": "Showing: %s", 47 | "gui.advanced_memory_card.types.any": "§aAll", 48 | "gui.advanced_memory_card.error.same_type": "P2P already this type." 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/lang/ru_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.betterp2p.advanced_memory_card": "Расширенная карта памяти", 3 | "item.betterp2p.advanced_memory_card.selected": "Frequency:", 4 | "gui.advanced_memory_card.select": "Выбрать", 5 | "gui.advanced_memory_card.bind": "Привязать", 6 | "gui.advanced_memory_card.unbind": "Unbind", 7 | "gui.advanced_memory_card.pos": "Позиция: %d, %d, %d", 8 | "gui.advanced_memory_card.side": "Side: %s", 9 | "gui.advanced_memory_card.dim": "Dim: %d", 10 | "gui.advanced_memory_card.name": "Name: %s", 11 | "gui.advanced_memory_card.sortinfo1": "Sorting tags", 12 | "gui.advanced_memory_card.sortinfo2": "Show Input P2Ps only", 13 | "gui.advanced_memory_card.sortinfo3": "Show Output P2Ps only", 14 | "gui.advanced_memory_card.sortinfo4": "Show Bound P2Ps only", 15 | "gui.advanced_memory_card.sortinfo5": "Show Unbound P2Ps only", 16 | "gui.advanced_memory_card.sortinfo6": "Filter by type name(s)", 17 | "gui.advanced_memory_card.sortinfo7": "Tags are space separated and are combined w/ the AND operator.", 18 | "gui.advanced_memory_card.extra.channel": "Channels used: %s", 19 | "gui.advanced_memory_card.mode.input": "Режим: Привязать ввод", 20 | "gui.advanced_memory_card.mode.input.desc.1": "• Before binding, the §aselected §aP2P §7and §bbind §btarget §7(if output) have their connections removed.", 21 | "gui.advanced_memory_card.mode.input.desc.2": "• The §aselected P2P §7becomes the the §6output§7, while the §bbind §btarget §7becomes the §9input§7.", 22 | "gui.advanced_memory_card.mode.input.desc.3": "• The §aselected P2P §7becomes the same type as its new input, and the §bbind §btarget §7is added to the output list.", 23 | "gui.advanced_memory_card.mode.output": "Режим: Привязать вывод", 24 | "gui.advanced_memory_card.mode.output.desc.1": "• Before binding, the §aselected §aP2P §7(if output) and §bbind §btarget §7have their connections removed.", 25 | "gui.advanced_memory_card.mode.output.desc.2": "• The §aselected P2P §7becomes the the §9input§7, while the §bbind §btarget §7becomes the §6output§7.", 26 | "gui.advanced_memory_card.mode.output.desc.3": "• The §bbind target §7becomes the same type as its new input, and is added to the output list.", 27 | "gui.advanced_memory_card.mode.copy": "Режим: копирование вывода", 28 | "gui.advanced_memory_card.mode.copy.desc.1": "• §aSelected P2P §7must be an §9input.", 29 | "gui.advanced_memory_card.mode.copy.desc.2": "• Before binding, the §bbind §btarget §7has its connections removed.", 30 | "gui.advanced_memory_card.mode.copy.desc.3": "• The §aselected §aP2P §7becomes the §9input§7, while the §bbind §btarget §7is added to the list of the §aselected §aP2P's §6outputs§7.", 31 | "gui.advanced_memory_card.mode.copy.desc.4": "• The §bbind target §7becomes the same type as its new input.", 32 | "gui.advanced_memory_card.mode.unbind": "Current Mode: Unbind", 33 | "gui.advanced_memory_card.mode.unbind.desc.1": "• Disables normal functions; you can no longer bind P2Ps.", 34 | "gui.advanced_memory_card.mode.unbind.desc.2": "• All bound P2Ps are now unbindable, which resets its frequency and converts it back to an §9input§7.", 35 | "gui.advanced_memory_card.desc.not_set": "Не установлено", 36 | "gui.advanced_memory_card.desc.effect": "Эффект:", 37 | "gui.advanced_memory_card.gui_scale.small": "Current Size: Small", 38 | "gui.advanced_memory_card.gui_scale.normal": "Current Size: Standard", 39 | "gui.advanced_memory_card.gui_scale.large": "Current Size: Large", 40 | "gui.advanced_memory_card.gui_scale.dynamic": "Current Size: Automatic", 41 | "gui.advanced_memory_card.p2p_status.bound": "Bound", 42 | "gui.advanced_memory_card.p2p_status.unbound": "Unbound", 43 | "gui.advanced_memory_card.p2p_status.input": "Input", 44 | "gui.advanced_memory_card.p2p_status.output": "Output", 45 | "gui.advanced_memory_card.p2p_status.offline": "Offline", 46 | "gui.advanced_memory_card.types.filtered": "Showing: %s", 47 | "gui.advanced_memory_card.types.any": "§aAll", 48 | "gui.advanced_memory_card.error.same_type": "P2P already this type." 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/assets/betterp2p/lang/zh_tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.betterp2p.advanced_memory_card": "Advanced Memory Card", 3 | "item.betterp2p.advanced_memory_card.selected": "Frequency:", 4 | "gui.advanced_memory_card.select": "Select", 5 | "gui.advanced_memory_card.bind": "Bind", 6 | "gui.advanced_memory_card.unbind": "Unbind", 7 | "gui.advanced_memory_card.pos": "Pos: %d, %d, %d", 8 | "gui.advanced_memory_card.side": "Side: %s", 9 | "gui.advanced_memory_card.dim": "Dim: %d", 10 | "gui.advanced_memory_card.name": "Name: %s", 11 | "gui.advanced_memory_card.sortinfo1": "Sorting tags", 12 | "gui.advanced_memory_card.sortinfo2": "Show Input P2Ps only", 13 | "gui.advanced_memory_card.sortinfo3": "Show Output P2Ps only", 14 | "gui.advanced_memory_card.sortinfo4": "Show Bound P2Ps only", 15 | "gui.advanced_memory_card.sortinfo5": "Show Unbound P2Ps only", 16 | "gui.advanced_memory_card.sortinfo6": "Filter by type name(s)", 17 | "gui.advanced_memory_card.sortinfo7": "Tags are space separated and are combined w/ the AND operator.", 18 | "gui.advanced_memory_card.extra.channel": "Channels used: %s", 19 | "gui.advanced_memory_card.mode.input": "Current Mode: Bind to Input", 20 | "gui.advanced_memory_card.mode.input.desc.1": "• Before binding, the §aselected §aP2P §7and §bbind §btarget §7(if output) have their connections removed.", 21 | "gui.advanced_memory_card.mode.input.desc.2": "• The §aselected P2P §7becomes the the §6output§7, while the §bbind §btarget §7becomes the §9input§7.", 22 | "gui.advanced_memory_card.mode.input.desc.3": "• The §aselected P2P §7becomes the same type as its new input, and the §bbind §btarget §7is added to the output list.", 23 | "gui.advanced_memory_card.mode.output": "Current Mode: Bind to Output", 24 | "gui.advanced_memory_card.mode.output.desc.1": "• Before binding, the §aselected §aP2P §7(if output) and §bbind §btarget §7have their connections removed.", 25 | "gui.advanced_memory_card.mode.output.desc.2": "• The §aselected P2P §7becomes the the §9input§7, while the §bbind §btarget §7becomes the §6output§7.", 26 | "gui.advanced_memory_card.mode.output.desc.3": "• The §bbind target §7becomes the same type as its new input, and is added to the output list.", 27 | "gui.advanced_memory_card.mode.copy": "Current Mode: Copy Input to Output", 28 | "gui.advanced_memory_card.mode.copy.desc.1": "• §aSelected P2P §7must be an §9input.", 29 | "gui.advanced_memory_card.mode.copy.desc.2": "• Before binding, the §bbind §btarget §7has its connections removed.", 30 | "gui.advanced_memory_card.mode.copy.desc.3": "• The §aselected §aP2P §7becomes the §9input§7, while the §bbind §btarget §7is added to the list of the §aselected §aP2P's §6outputs§7.", 31 | "gui.advanced_memory_card.mode.copy.desc.4": "• The §bbind target §7becomes the same type as its new input.", 32 | "gui.advanced_memory_card.mode.unbind": "Current Mode: Unbind", 33 | "gui.advanced_memory_card.mode.unbind.desc.1": "• Disables normal functions; you can no longer bind P2Ps.", 34 | "gui.advanced_memory_card.mode.unbind.desc.2": "• All bound P2Ps are now unbindable, which resets its frequency and converts it back to an §9input§7.", 35 | "gui.advanced_memory_card.desc.not_set": "Not set", 36 | "gui.advanced_memory_card.desc.effect": "Effect:", 37 | "gui.advanced_memory_card.gui_scale.small": "Current Size: Small", 38 | "gui.advanced_memory_card.gui_scale.normal": "Current Size: Standard", 39 | "gui.advanced_memory_card.gui_scale.large": "Current Size: Large", 40 | "gui.advanced_memory_card.gui_scale.dynamic": "Current Size: Automatic", 41 | "gui.advanced_memory_card.p2p_status.bound": "Bound", 42 | "gui.advanced_memory_card.p2p_status.unbound": "Unbound", 43 | "gui.advanced_memory_card.p2p_status.input": "Input", 44 | "gui.advanced_memory_card.p2p_status.output": "Output", 45 | "gui.advanced_memory_card.p2p_status.offline": "Offline", 46 | "gui.advanced_memory_card.types.filtered": "Showing: %s", 47 | "gui.advanced_memory_card.types.any": "§aAll", 48 | "gui.advanced_memory_card.error.same_type": "P2P already this type." 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/publish_project.yml: -------------------------------------------------------------------------------- 1 | # Publishes the project to GitHub Releases, CurseForge, and Modrinth 2 | name: Publish Project 3 | 4 | on: 5 | push: 6 | tags: 7 | - "v[0-9]+.[0-9]+.[0-9]+" # any SemVer tag, e.g. v1.2.3 8 | workflow_dispatch: 9 | 10 | env: 11 | # link to the changelog with a format code for the version 12 | CHANGELOG_LOCATION: "Changelog is available [here](https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }})" 13 | # type of release 14 | RELEASE_TYPE: "release" 15 | 16 | concurrency: 17 | group: publish-${{ github.head_ref || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | publish: 22 | name: Publish 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | contents: write # needed to create GitHub releases 27 | 28 | steps: 29 | - name: Checkout Repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Get Tags 33 | id: tag 34 | uses: ildug/get-tag-action@v1 35 | 36 | - name: Set up JDK 21 37 | uses: actions/setup-java@v1 38 | with: 39 | java-version: 21 40 | 41 | - name: Grant execute permission for gradlew 42 | run: chmod +x gradlew 43 | 44 | - name: Restore Cache 45 | id: cache-restore 46 | uses: actions/cache/restore@v4 47 | with: 48 | path: ~/.gradle/caches 49 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 50 | restore-keys: ${{ runner.os }}-gradle 51 | 52 | - name: Cache Gradle packages 53 | uses: actions/cache@v4 54 | with: 55 | path: ~/.gradle/caches 56 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 57 | restore-keys: ${{ runner.os }}-gradle 58 | 59 | - name: Build Project 60 | run: ./gradlew build --warning-mode all --build-cache 61 | 62 | - name: Publish to GitHub 63 | uses: softprops/action-gh-release@v2 64 | with: 65 | files: | 66 | build/libs/betterp2p-${{ steps.tag.outputs.version }}.jar 67 | generate_release_notes: true 68 | fail_on_unmatched_files: true 69 | 70 | - name: "Upload Forge to CurseForge" 71 | uses: itsmeow/curseforge-upload@v3 72 | with: 73 | display_name: "[NeoForge 1.21.1] v${{ steps.tag.outputs.version }}" 74 | file_path: "build/libs/betterp2p-${{ steps.tag.outputs.version }}.jar" 75 | game_endpoint: "minecraft" 76 | relations: "applied-energistics-2:requiredDependency,kotlin-for-forge:requiredDependency" 77 | game_versions: "Minecraft 1.21.1,Java 21,NeoForge" 78 | project_id: "538092" 79 | token: "${{ secrets.CF_API_TOKEN }}" 80 | 81 | - name: "Upload Forge to Modrinth" 82 | uses: dsx137/modrinth-release-action@main 83 | env: 84 | MODRINTH_TOKEN: "${{ secrets.MODRINTH_TOKEN }}" 85 | with: 86 | name: "[NeoForge 1.21.1] v${{ steps.tag.outputs.version }}" 87 | project_id: 9DDxOvTJ 88 | loaders: neoforge 89 | game_versions: 1.21.1 90 | version_number: ${{ steps.tag.outputs.version }} 91 | files: | 92 | "build/libs/betterp2p-${{ steps.tag.outputs.version }}.jar" 93 | dependencies: "ordsPcFz:required, XxWD5pD3:required" 94 | version_type: release 95 | 96 | - name: Always Save Cache 97 | id: cache-save 98 | if: always() && steps.cache-restore.outputs.cache-hit != 'true' 99 | uses: actions/cache/save@v4 100 | with: 101 | path: ~/.gradle/caches 102 | key: ${{ steps.cache-restore.outputs.cache-primary-key }} 103 | restore-keys: ${{ runner.os }}-gradle 104 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/WidgetTypeSelector.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import dev.lasm.betterp2p.client.gui.GuiAdvancedMemoryCard 4 | import dev.lasm.betterp2p.client.gui.drawBlockIcon 5 | import dev.lasm.betterp2p.util.p2p.ClientTunnelInfo 6 | import kotlin.math.min 7 | import net.minecraft.client.Minecraft 8 | import net.minecraft.client.gui.GuiGraphics 9 | import net.minecraft.client.gui.components.AbstractWidget 10 | import net.minecraft.client.gui.layouts.LayoutElement 11 | import net.minecraft.client.gui.narration.NarrationElementOutput 12 | import net.minecraft.network.chat.Component 13 | import net.minecraft.resources.ResourceLocation 14 | 15 | const val ICONS_PER_ROW = 5 16 | 17 | /** Select a type. */ 18 | class WidgetTypeSelector( 19 | x: Int, 20 | y: Int, 21 | val gui: GuiAdvancedMemoryCard, 22 | val p2pTypes: List 23 | ) : 24 | AbstractWidget( 25 | x, 26 | y, 27 | min(ICONS_PER_ROW, p2pTypes.size) * 18 + 8, 28 | (p2pTypes.size + ICONS_PER_ROW - 1) * 18 / ICONS_PER_ROW + 8, 29 | Component.empty() 30 | ) { 31 | var hoveredIdx: Int = 0 32 | var useAny = false 33 | /** Feeds the input into this parent. */ 34 | var parent: ITypeReceiver? = null 35 | private val translated: List> 36 | 37 | override fun setFocused(focused: Boolean) { 38 | super.setFocused(focused) 39 | if (!focused) visible = false 40 | } 41 | 42 | init { 43 | val list = p2pTypes.map { listOf(it.dispName) }.toMutableList() 44 | list.add(listOf(Component.translatable("gui.advanced_memory_card.types.any"))) 45 | translated = list 46 | } 47 | 48 | override fun onClick(mouseX: Double, mouseY: Double, button: Int) { 49 | if (hoveredIdx != -1) parent?.accept(p2pTypes.getOrNull(hoveredIdx)) 50 | super.onClick(mouseX, mouseY, button) 51 | } 52 | 53 | override fun renderWidget( 54 | graphics: GuiGraphics, 55 | mouseX: Int, 56 | mouseY: Int, 57 | partialFloat: Float 58 | ) { 59 | // Background 60 | graphics.fill(x, y, x + width, y + height, 0xAA000000.toInt()) 61 | 62 | hoveredIdx = -1 63 | for ((i, type) in p2pTypes.withIndex()) { 64 | val iconPosX = x + 4 + (i % ICONS_PER_ROW) * 18 65 | val iconPosY = y + 4 + (i / ICONS_PER_ROW) * 18 66 | val iconHover = 67 | mouseX > iconPosX && 68 | mouseX < iconPosX + 18 && 69 | mouseY > iconPosY && 70 | mouseY < iconPosY + 18 71 | if (iconHover) { 72 | hoveredIdx = i 73 | graphics.fill(iconPosX, iconPosY, iconPosX + 18, iconPosY + 18, 0xFF00FF00.toInt()) 74 | } 75 | drawBlockIcon(graphics, type.icon(), x = iconPosX + 1, y = iconPosY + 1) 76 | } 77 | if (useAny) { 78 | val iconPosX = x + 4 + (p2pTypes.size % ICONS_PER_ROW) * 18 79 | val iconPosY = y + 4 + (p2pTypes.size / ICONS_PER_ROW) * 18 80 | val iconHover = 81 | mouseX > iconPosX && 82 | mouseX < iconPosX + 18 && 83 | mouseY > iconPosY && 84 | mouseY < iconPosY + 18 85 | if (iconHover) { 86 | hoveredIdx = p2pTypes.size 87 | graphics.fill(iconPosX, iconPosY, iconPosX + 18, iconPosY + 18, 0xFF00FF00.toInt()) 88 | } 89 | drawBlockIcon( 90 | graphics, 91 | ResourceLocation.withDefaultNamespace("textures/block/coal_block.png"), 92 | x = iconPosX + 1, 93 | y = iconPosY + 1 94 | ) 95 | graphics.drawString( 96 | Minecraft.getInstance().font, 97 | "?", 98 | iconPosX + 6, 99 | iconPosY + 6, 100 | 0xFFFF0000.toInt(), 101 | false 102 | ) 103 | } 104 | if (hoveredIdx != -1) { 105 | gui.drawTooltip(graphics, mouseX, mouseY, translated[hoveredIdx]) 106 | } 107 | } 108 | 109 | override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {} 110 | } 111 | 112 | interface ITypeReceiver : LayoutElement { 113 | 114 | fun accept(type: ClientTunnelInfo?) 115 | } 116 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/InfoFilter.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui 2 | 3 | /** 4 | * Extend the default filtering. Holds information about the search filter. 5 | * 6 | * There are 4 modes of filtering: 7 | * - By input/output: Using `@in/@out` filters by input/output respectively. 8 | * - By bound/unbound: Using `@b` or `@u` filters by input/output respectively. 9 | * - By type: Using `@types=;;...` filters by type. 10 | * - By name: Use the name If someone comes and says "sort by freq pls" then we can add it at that 11 | * time 12 | */ 13 | class InfoFilter { 14 | 15 | /** Active filters to use when filtering entries. */ 16 | val activeFilters: MutableMap?> = mutableMapOf() 17 | 18 | /** Parse the query string for filters and update the active filter list. */ 19 | fun updateFilter(query: String) { 20 | val tokens = SEARCH_REGEX.findAll(query) 21 | activeFilters.clear() 22 | var bind: String 23 | tokens.forEach { 24 | val token = it.value 25 | // If we don't start with a tag, skip all these regexes 26 | if (token.startsWith("@")) { 27 | when { 28 | it.value.matches(Filter.INPUT.pattern) -> { 29 | activeFilters.putIfAbsent(Filter.INPUT, null) 30 | } 31 | it.value.matches(Filter.OUTPUT.pattern) -> { 32 | activeFilters.putIfAbsent(Filter.OUTPUT, null) 33 | } 34 | it.value.matches(Filter.BOUND.pattern) -> { 35 | activeFilters.putIfAbsent(Filter.BOUND, null) 36 | } 37 | it.value.matches(Filter.UNBOUND.pattern) -> { 38 | activeFilters.putIfAbsent(Filter.UNBOUND, null) 39 | } 40 | it.value.matches(Filter.TYPE.pattern) -> { 41 | val result = Filter.TYPE.pattern.find(it.value)!! 42 | val l = mutableListOf() 43 | activeFilters.putIfAbsent(Filter.TYPE, l) 44 | val types = result.groupValues[1].split(";") 45 | types.forEach { filter -> l.add(filter) } 46 | } 47 | it.value.isBlank() -> {} 48 | else -> {} 49 | } 50 | } else { 51 | val l = mutableListOf() 52 | activeFilters.putIfAbsent(Filter.NAME, l) 53 | when { 54 | it.groups[1] != null -> l.add(it.groups[1]!!.value) 55 | it.groups[2] != null -> l.add(it.groups[2]!!.value) 56 | else -> activeFilters[Filter.NAME]!!.add(it.value) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | /** The different filter types. Probably will let these be adjustable in the config. */ 64 | enum class Filter(val pattern: Regex, val filter: (InfoWrapper, List?) -> Boolean) { 65 | INPUT("\\A@in\\z".toRegex(), { it, _ -> !it.output }), 66 | OUTPUT("\\A@out\\z".toRegex(), { it, _ -> it.output }), 67 | BOUND("\\A@b\\z".toRegex(), { it, _ -> it.frequency != 0.toShort() }), 68 | UNBOUND("\\A@u\\z".toRegex(), { it, _ -> it.frequency == 0.toShort() || it.error }), 69 | TYPE( 70 | "\\A@types*=(.+)\\z".toRegex(), 71 | filter@{ it, strs -> 72 | val tags = 73 | dev.lasm.betterp2p.BetterP2P.proxy 74 | .getP2PFromIndex(it.type)!! 75 | .dispName 76 | .string 77 | .lowercase() 78 | for (f in strs!!) { 79 | if (tags.contains(f.lowercase())) { 80 | return@filter true 81 | } 82 | } 83 | false 84 | } 85 | ), 86 | NAME( 87 | "\"?.+\"?".toRegex(), 88 | filter@{ it, strs -> 89 | val name = it.name.lowercase() 90 | for (f in strs!!) { 91 | // Ppl better not troll and use double quotes in their P2P tunnel names 92 | val query = f.removeSurrounding("\"") 93 | if (name.contains(query)) { 94 | return@filter true 95 | } 96 | } 97 | false 98 | } 99 | ) 100 | } 101 | 102 | // I spent 10 minutes on this until I gave up... regex wtf 103 | // https://stackoverflow.com/questions/366202/regex-for-splitting-a-string-using-space-when-not-surrounded-by-single-or-double 104 | val SEARCH_REGEX = "[^\\s\"']+|\"([^\"]*)\"|'([^']*)'".toRegex() 105 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/BetterP2P.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack 4 | import dev.lasm.betterp2p.client.AdvancedMemoryCardMenu 5 | import dev.lasm.betterp2p.client.RenderBlockOutline 6 | import dev.lasm.betterp2p.client.gui.GuiAdvancedMemoryCard 7 | import dev.lasm.betterp2p.item.ItemAdvancedMemoryCard 8 | import dev.lasm.betterp2p.network.ModNetwork 9 | import dev.lasm.betterp2p.network.data.MemoryInfo 10 | import java.util.function.Supplier 11 | import net.minecraft.client.Camera 12 | import net.minecraft.client.Minecraft 13 | import net.minecraft.client.multiplayer.ClientLevel 14 | import net.minecraft.client.renderer.MultiBufferSource 15 | import net.minecraft.core.component.DataComponentType 16 | import net.minecraft.core.registries.Registries 17 | import net.minecraft.resources.ResourceKey 18 | import net.minecraft.resources.ResourceLocation 19 | import net.minecraft.world.flag.FeatureFlagSet 20 | import net.minecraft.world.inventory.MenuType 21 | import net.minecraft.world.item.Item 22 | import net.neoforged.fml.common.Mod 23 | import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent 24 | import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent 25 | import net.neoforged.neoforge.client.event.RenderLevelStageEvent 26 | import net.neoforged.neoforge.common.NeoForge 27 | import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent 28 | import net.neoforged.neoforge.event.entity.player.PlayerEvent 29 | import net.neoforged.neoforge.registries.DeferredHolder 30 | import net.neoforged.neoforge.registries.DeferredRegister 31 | import org.apache.logging.log4j.LogManager 32 | import org.apache.logging.log4j.Logger 33 | import thedarkcolour.kotlinforforge.neoforge.forge.MOD_BUS 34 | import thedarkcolour.kotlinforforge.neoforge.forge.runForDist 35 | 36 | @Mod(BetterP2P.MOD_ID) 37 | object BetterP2P { 38 | val proxy: CommonProxy = 39 | runForDist({ Supplier { ClientProxy() } }, { Supplier { CommonProxy() } }).get() 40 | 41 | const val MOD_ID = "betterp2p" 42 | 43 | val logger: Logger = LogManager.getLogger(MOD_ID) 44 | 45 | val ITEMS: DeferredRegister = DeferredRegister.createItems(MOD_ID) 46 | val ADVANCED_MEMORY_CARD_ITEM: DeferredHolder = 47 | ITEMS.register("advanced_memory_card", Supplier { ItemAdvancedMemoryCard }) 48 | 49 | val MENUS: DeferredRegister> = DeferredRegister.create(Registries.MENU, MOD_ID) 50 | val ADVANCED_MEMORY_CARD_MENU: DeferredHolder, MenuType> = 51 | MENUS.register( 52 | "advanced_memory_card", 53 | Supplier { MenuType(::AdvancedMemoryCardMenu, FeatureFlagSet.of()) } 54 | ) 55 | 56 | val DATA_COMPONENTS: DeferredRegister.DataComponents = 57 | DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MOD_ID) 58 | val MEMORY_INFO: DeferredHolder, DataComponentType> = 59 | DATA_COMPONENTS.registerComponentType("memory_info") { builder -> 60 | builder.persistent(MemoryInfo.CODEC).networkSynchronized(MemoryInfo.STREAM_CODEC) 61 | } 62 | 63 | init { 64 | ITEMS.register(MOD_BUS) 65 | MENUS.register(MOD_BUS) 66 | DATA_COMPONENTS.register(MOD_BUS) 67 | NeoForge.EVENT_BUS.addListener(::onPlayerQuit) 68 | NeoForge.EVENT_BUS.addListener(::onRenderLevelStage) 69 | MOD_BUS.addListener(ModNetwork::registerNetwork) 70 | MOD_BUS.addListener(::onRegisterMenuScreens) 71 | MOD_BUS.addListener(::onBuildCreativeModeTabContents) 72 | MOD_BUS.addListener(::onCommonSetup) 73 | } 74 | 75 | fun onCommonSetup(event: FMLCommonSetupEvent) { 76 | logger.info("Tunnels init") 77 | proxy.initTunnels() 78 | } 79 | 80 | fun onPlayerQuit(event: PlayerEvent.PlayerLoggedOutEvent) { 81 | ModNetwork.removeConnection(event.entity) 82 | } 83 | 84 | fun onBuildCreativeModeTabContents(event: BuildCreativeModeTabContentsEvent) { 85 | if ( 86 | event.tabKey == 87 | ResourceKey.create( 88 | Registries.CREATIVE_MODE_TAB, 89 | ResourceLocation.fromNamespaceAndPath("ae2", "main") 90 | ) 91 | ) 92 | event.accept(ADVANCED_MEMORY_CARD_ITEM.get()) 93 | } 94 | 95 | fun onRenderLevelStage(context: RenderLevelStageEvent) { 96 | if (context.stage != RenderLevelStageEvent.Stage.AFTER_BLOCK_ENTITIES) return 97 | val level: ClientLevel? = Minecraft.getInstance().level 98 | val poseStack: PoseStack = context.poseStack 99 | val buffers: MultiBufferSource? = Minecraft.getInstance().renderBuffers().bufferSource() 100 | val camera: Camera = context.camera 101 | if (level == null || buffers == null) { 102 | return 103 | } else { 104 | RenderBlockOutline.showPartPlacementPreview( 105 | Minecraft.getInstance().player, 106 | poseStack, 107 | buffers, 108 | camera 109 | ) 110 | } 111 | } 112 | 113 | fun onRegisterMenuScreens(event: RegisterMenuScreensEvent) { 114 | event.register(ADVANCED_MEMORY_CARD_MENU.get()) { menu, inv, name -> 115 | GuiAdvancedMemoryCard(menu) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/P2PTypeButton.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import appeng.parts.p2p.FluidP2PTunnelPart 4 | import appeng.parts.p2p.MEP2PTunnelPart 5 | import appeng.parts.p2p.RedstoneP2PTunnelPart 6 | import com.mojang.blaze3d.systems.RenderSystem 7 | import dev.lasm.betterp2p.BetterP2P 8 | import dev.lasm.betterp2p.client.gui.GuiAdvancedMemoryCard 9 | import dev.lasm.betterp2p.client.gui.drawBlockIcon 10 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 11 | import dev.lasm.betterp2p.network.packet.C2SRefreshP2PList 12 | import dev.lasm.betterp2p.util.p2p.ClientTunnelInfo 13 | import kotlin.reflect.KProperty0 14 | import net.minecraft.ChatFormatting 15 | import net.minecraft.client.Minecraft 16 | import net.minecraft.client.gui.GuiGraphics 17 | import net.minecraft.network.chat.Component 18 | import net.minecraft.network.chat.MutableComponent 19 | import net.neoforged.neoforge.network.PacketDistributor 20 | 21 | private const val s = "gui.advanced_memory_card.types.filtered" 22 | 23 | class P2PTypeButton( 24 | val type: KProperty0, 25 | onPress: OnPress, 26 | private val onSecondaryPress: OnPress 27 | ) : IconButton(0, 0, onPress), ITypeReceiver { 28 | init { 29 | updateHoverText() 30 | } 31 | 32 | val types = BetterP2P.proxy.getP2PTypeList() 33 | var index = 34 | if (type.get() != null) { 35 | types.first { it.index == type.get()?.index }.index 36 | } else { 37 | types.size 38 | } 39 | private val me = 40 | BetterP2P.proxy.getP2PFromClass(MEP2PTunnelPart::class.java) as ClientTunnelInfo 41 | private val fluid = 42 | BetterP2P.proxy.getP2PFromClass(FluidP2PTunnelPart::class.java) as ClientTunnelInfo 43 | private val redstone = 44 | BetterP2P.proxy.getP2PFromClass(RedstoneP2PTunnelPart::class.java) as ClientTunnelInfo 45 | 46 | fun nextType(reverse: Boolean): ClientTunnelInfo? { 47 | return if (reverse) { 48 | index = (index - 1).rem(types.size + 1) 49 | types.getOrNull(index) as? ClientTunnelInfo 50 | } else { 51 | index = (index + 1).rem(types.size + 1) 52 | types.getOrNull(index) as? ClientTunnelInfo 53 | } 54 | } 55 | 56 | override fun mouseClicked(d: Double, e: Double, i: Int): Boolean { 57 | if (!this.active || !this.visible) { 58 | return false 59 | } 60 | if ((clicked(d, e))) { 61 | if (i == 0) { 62 | this.playDownSound(Minecraft.getInstance().soundManager) 63 | this.onClick(d, e) 64 | return true 65 | } else if (i == 1) { 66 | this.playDownSound(Minecraft.getInstance().soundManager) 67 | this.onSecondaryPress.onPress(this) 68 | return true 69 | } 70 | } 71 | return false 72 | } 73 | 74 | override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partial: Float) { 75 | RenderSystem.enableBlend() 76 | RenderSystem.enableDepthTest() 77 | renderBackground(guiGraphics, mouseY, mouseY, partial) 78 | 79 | if (type.get() != null) { 80 | drawBlockIcon( 81 | guiGraphics, 82 | type.get()!!.icon(), 83 | x = this.x + 2, 84 | y = this.y + 2, 85 | width = 28, 86 | height = 28 87 | ) 88 | } else { 89 | drawBlockIcon( 90 | guiGraphics, 91 | redstone.icon(), 92 | x = this.x + 12, 93 | y = this.y + 12, 94 | width = 18, 95 | height = 18 96 | ) 97 | drawBlockIcon( 98 | guiGraphics, 99 | fluid.icon(), 100 | x = this.x + 7, 101 | y = this.y + 7, 102 | width = 18, 103 | height = 18 104 | ) 105 | drawBlockIcon( 106 | guiGraphics, 107 | me.icon(), 108 | x = this.x + 2, 109 | y = this.y + 2, 110 | width = 18, 111 | height = 18 112 | ) 113 | } 114 | } 115 | 116 | private fun updateHoverText() { 117 | messages[0] = 118 | if (type.get() == null) { 119 | Component.translatable( 120 | "gui.advanced_memory_card.types.filtered", 121 | Component.translatable("gui.advanced_memory_card.types.any") 122 | ) 123 | } else { 124 | Component.translatable( 125 | "gui.advanced_memory_card.types.filtered", 126 | (type.get()!!.stack.displayName as MutableComponent).withStyle( 127 | ChatFormatting.GREEN 128 | ), 129 | ) 130 | } 131 | } 132 | 133 | fun commitType() { 134 | updateHoverText() 135 | PacketDistributor.sendToServer(C2SRefreshP2PList(type.get()?.index ?: TUNNEL_ANY)) 136 | playDownSound(Minecraft.getInstance().soundManager) 137 | } 138 | 139 | override fun accept(type: ClientTunnelInfo?) { 140 | val screen = Minecraft.getInstance().screen 141 | if (screen is GuiAdvancedMemoryCard) { 142 | screen.closeTypeSelector(type) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/InfoWrapper.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.network.data.P2PInfo 5 | import dev.lasm.betterp2p.network.data.P2PLocation 6 | import dev.lasm.betterp2p.util.p2p.ClientTunnelInfo 7 | import net.minecraft.ChatFormatting 8 | import net.minecraft.network.chat.Component 9 | import net.minecraft.resources.ResourceLocation 10 | 11 | class InfoWrapper(info: P2PInfo) { 12 | var frequency: Short = info.frequency 13 | set(value) { 14 | if (error || value == 0.toShort()) { 15 | hoverInfo[4] = 16 | Component.translatable("gui.advanced_memory_card.p2p_status.unbound") 17 | .withStyle(ChatFormatting.RED) 18 | } else { 19 | hoverInfo[4] = 20 | Component.translatable("gui.advanced_memory_card.p2p_status.bound") 21 | .withStyle(ChatFormatting.GREEN) 22 | } 23 | field = value 24 | } 25 | 26 | val hasChannel = info.hasChannel 27 | val loc: P2PLocation = P2PLocation(info.pos, info.facing, info.dim) 28 | val output: Boolean = info.output 29 | val type: Int = info.type 30 | var name: String = info.name 31 | var error: Boolean = false 32 | 33 | /** The backing p2p icon/feature */ 34 | var icon: ResourceLocation 35 | 36 | /** p2p frame */ 37 | var overlay: ResourceLocation = 38 | ResourceLocation.fromNamespaceAndPath("ae2", "textures/part/p2p_tunnel_front.png") 39 | 40 | val description: Component 41 | 42 | val freqDisplay: Component = 43 | Component.translatable("item.betterp2p.advanced_memory_card.selected") 44 | .append(" ") 45 | .append( 46 | if (frequency != 0.toShort()) { 47 | val hex: String = 48 | buildString { 49 | append((frequency.toUInt() shr 32).toString(16).uppercase()) 50 | append(frequency.toUInt().toString(16).uppercase()) 51 | } 52 | .format4() 53 | Component.literal(hex) 54 | } else { 55 | Component.translatable("gui.advanced_memory_card.desc.not_set") 56 | } 57 | ) 58 | 59 | val hoverInfo: MutableList 60 | 61 | val channels: Component? by lazy { 62 | if (info.channels >= 0) { 63 | Component.translatable("gui.advanced_memory_card.extra.channel", info.channels) 64 | } else { 65 | null 66 | } 67 | } 68 | 69 | init { 70 | val p2pType: ClientTunnelInfo = 71 | BetterP2P.proxy.getP2PFromIndex(info.type) as ClientTunnelInfo 72 | icon = p2pType.icon() 73 | description = 74 | Component.literal("Type: ") 75 | .append(p2pType.dispName) 76 | .append(" - ") 77 | .append( 78 | if (output) { 79 | Component.translatable("gui.advanced_memory_card.p2p_status.output") 80 | } else { 81 | Component.translatable("gui.advanced_memory_card.p2p_status.input") 82 | } 83 | ) 84 | 85 | val online = info.hasChannel 86 | hoverInfo = 87 | mutableListOf( 88 | Component.literal("P2P - ").withStyle(ChatFormatting.AQUA).append(p2pType.dispName), 89 | Component.translatable( 90 | "gui.advanced_memory_card.pos", 91 | info.pos.x, 92 | info.pos.y, 93 | info.pos.z 94 | ) 95 | .withStyle(ChatFormatting.YELLOW), 96 | Component.translatable("gui.advanced_memory_card.side", info.facing.name) 97 | .withStyle(ChatFormatting.YELLOW), 98 | Component.translatable( 99 | "gui.advanced_memory_card.dim", 100 | info.dim.location().toString() 101 | ) 102 | .withStyle(ChatFormatting.YELLOW) 103 | ) 104 | if (error || frequency == 0.toShort()) { 105 | hoverInfo.add( 106 | Component.translatable("gui.advanced_memory_card.p2p_status.unbound") 107 | .withStyle(ChatFormatting.RED) 108 | ) 109 | } else { 110 | hoverInfo.add( 111 | Component.translatable("gui.advanced_memory_card.p2p_status.bound") 112 | .withStyle(ChatFormatting.GREEN) 113 | ) 114 | } 115 | 116 | if (!online) { 117 | hoverInfo.add( 118 | Component.translatable("gui.advanced_memory_card.p2p_status.offline") 119 | .withStyle(ChatFormatting.RED) 120 | ) 121 | } 122 | } 123 | 124 | override fun hashCode(): Int { 125 | return loc.hashCode() 126 | } 127 | 128 | override fun equals(other: Any?): Boolean { 129 | if (this === other) return true 130 | if (this.javaClass != other?.javaClass) return false 131 | other as InfoWrapper 132 | 133 | return this.loc == other.loc 134 | } 135 | } 136 | 137 | fun String.format4(): String { 138 | val format = StringBuilder() 139 | for (index in this.indices) { 140 | if (index % 4 == 0 && index != 0) { 141 | format.append(" ") 142 | } 143 | format.append(this[index]) 144 | } 145 | return format.toString() 146 | } 147 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/ModNetwork.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network 2 | 3 | import appeng.api.networking.IGrid 4 | import dev.lasm.betterp2p.client.AdvancedMemoryCardMenu 5 | import dev.lasm.betterp2p.network.data.GridServerCache 6 | import dev.lasm.betterp2p.network.data.MemoryInfo 7 | import dev.lasm.betterp2p.network.packet.* 8 | import java.util.* 9 | import net.minecraft.network.chat.Component 10 | import net.minecraft.server.level.ServerPlayer 11 | import net.minecraft.world.MenuProvider 12 | import net.minecraft.world.entity.player.Inventory 13 | import net.minecraft.world.entity.player.Player 14 | import net.minecraft.world.inventory.AbstractContainerMenu 15 | import net.neoforged.neoforge.network.PacketDistributor 16 | import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent 17 | import net.neoforged.neoforge.network.registration.HandlerThread 18 | 19 | /** Network cooldown time in milliseconds */ 20 | const val NETWORK_CD = 200L 21 | 22 | /** Mod network manager. Handles server <-> client communication. */ 23 | object ModNetwork { 24 | 25 | /** for client requests (changing viewed p2p) */ 26 | val playerState: MutableMap = Collections.synchronizedMap(WeakHashMap()) 27 | 28 | fun registerNetwork(event: RegisterPayloadHandlersEvent) { 29 | val reg = event.registrar("1").executesOn(HandlerThread.NETWORK) 30 | reg.playToClient(S2COpenGui.TYPE, S2COpenGui.STREAM_CODEC, ClientOpenGuiHandler) 31 | reg.playToClient(S2CUpdateP2P.TYPE, S2CUpdateP2P.STREAM_CODEC, ClientUpdateP2PHandler) 32 | reg.playToServer(C2SLinkP2P.TYPE, C2SLinkP2P.STREAM_CODEC, ServerLinkP2PHandler) 33 | reg.playToServer(C2SCloseGui.TYPE, C2SCloseGui.STREAM_CODEC, ServerCloseGuiHandler) 34 | reg.playToServer( 35 | C2SUpdateMemoryInfo.TYPE, 36 | C2SUpdateMemoryInfo.STREAM_CODEC, 37 | ServerUpdateMemoryInfoHandler 38 | ) 39 | reg.playToServer(C2SRenameP2P.TYPE, C2SRenameP2P.STREAM_CODEC, ServerRenameP2PTunnelHandler) 40 | reg.playToServer( 41 | C2SRefreshP2PList.TYPE, 42 | C2SRefreshP2PList.STREAM_CODEC, 43 | ServerRefreshP2PListHandler 44 | ) 45 | reg.playToServer(C2SUnlinkP2P.TYPE, C2SUnlinkP2P.STREAM_CODEC, ServerUnlinkP2PHandler) 46 | reg.playToServer( 47 | C2SChangeP2PType.TYPE, 48 | C2SChangeP2PType.STREAM_CODEC, 49 | ServerTypeChangeHandler 50 | ) 51 | } 52 | 53 | /** Utility function that asks for a full refresh of a specific p2p type. */ 54 | fun requestP2PList(player: Player, type: Int) { 55 | val playerState = playerState[player.uuid] ?: return 56 | val cache = playerState.gridCache 57 | 58 | cache.type = type 59 | if (playerState.updateReady + NETWORK_CD < System.currentTimeMillis()) { 60 | PacketDistributor.sendToPlayer( 61 | player as ServerPlayer, 62 | S2CUpdateP2P(cache.retrieveP2PList(), true) 63 | ) 64 | playerState.updateReady = System.currentTimeMillis() + NETWORK_CD 65 | } else if (!playerState.updatePending) { 66 | playerState.updatePending = true 67 | PacketDistributor.sendToPlayer( 68 | player as ServerPlayer, 69 | S2CUpdateP2P(cache.retrieveP2PList(), true) 70 | ) 71 | playerState.updatePending = false 72 | } 73 | } 74 | 75 | /** 76 | * Utility function that asks for a p2p update. Multiple requests are bundled into 1. If dirty, 77 | * only an incremental update is sent. 78 | */ 79 | fun requestP2PUpdate(player: Player) { 80 | val playerState = playerState[player.uuid] ?: return 81 | val cache = playerState.gridCache 82 | 83 | if (playerState.updateReady + NETWORK_CD < System.currentTimeMillis()) { 84 | PacketDistributor.sendToPlayer( 85 | player as ServerPlayer, 86 | S2CUpdateP2P(cache.getP2PUpdates()) 87 | ) 88 | playerState.updateReady = System.currentTimeMillis() + NETWORK_CD 89 | } else if (!playerState.updatePending) { 90 | playerState.updatePending = true 91 | PacketDistributor.sendToPlayer( 92 | player as ServerPlayer, 93 | S2CUpdateP2P(cache.getP2PUpdates()) 94 | ) 95 | playerState.updatePending = false 96 | } 97 | } 98 | 99 | /** Sets up a connection. */ 100 | fun initConnection(player: Player, grid: IGrid, info: MemoryInfo) { 101 | val cache = GridServerCache(grid, player, info.type) 102 | 103 | playerState[player.uuid] = PlayerRequest(gridCache = cache) 104 | if (player !is ServerPlayer) return 105 | 106 | player.openMenu( 107 | object : MenuProvider { 108 | override fun createMenu( 109 | id: Int, 110 | inventory: Inventory, 111 | player: Player 112 | ): AbstractContainerMenu { 113 | return AdvancedMemoryCardMenu(id, inventory).also { 114 | it.memoryInfo = info 115 | it.infos = cache.retrieveP2PList() 116 | } 117 | } 118 | 119 | override fun getDisplayName(): Component { 120 | return Component.translatable("container.examplemod.example_menu") 121 | } 122 | } 123 | ) 124 | PacketDistributor.sendToPlayer(player, S2COpenGui(cache.retrieveP2PList(), info)) 125 | } 126 | 127 | fun removeConnection(player: Player) { 128 | playerState.remove(player.uuid) 129 | } 130 | } 131 | 132 | /** Keeps track of when to send network updates. */ 133 | data class PlayerRequest( 134 | internal var updatePending: Boolean = false, 135 | internal var updateReady: Long = System.currentTimeMillis(), 136 | val gridCache: GridServerCache 137 | ) 138 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/Infolist.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui 2 | 3 | import dev.lasm.betterp2p.client.gui.widget.WidgetScrollBar 4 | import dev.lasm.betterp2p.item.BetterMemoryCardModes 5 | import dev.lasm.betterp2p.network.data.P2PLocation 6 | import java.util.* 7 | import kotlin.reflect.KProperty0 8 | 9 | /** 10 | * InfoList Dedicated list to hold "InfoWrappers". Internally, stores them in a HashMap. This is an 11 | * opaque type, and access to it is restricted. External access is instead directed to a sorted view 12 | * and a filtered view of the internal map. 13 | */ 14 | class InfoList( 15 | initList: Collection, 16 | private val search: KProperty0, 17 | private val mode: KProperty0 18 | ) { 19 | 20 | /** The master map, acts as the source of truth for all items in this list. */ 21 | private val masterMap: HashMap = hashMapOf() 22 | 23 | /** Sorted view of the master map. This is resorted whenever the map is updated. */ 24 | val sorted: MutableList = mutableListOf() 25 | 26 | /** Filtered view of the sorted view (not the master map) */ 27 | var filtered: List = listOf() 28 | 29 | private val filter: InfoFilter = InfoFilter() 30 | 31 | /** Binding to the search string in the text box */ 32 | private val searchStr: String 33 | get() = search.get() 34 | 35 | val selectedInfo: InfoWrapper? 36 | get() { 37 | return if (selectedEntry == null) { 38 | null 39 | } else { 40 | masterMap[selectedEntry!!] 41 | } 42 | } 43 | 44 | var selectedEntry: P2PLocation? = null 45 | 46 | val size: Int 47 | get() = masterMap.size 48 | 49 | init { 50 | initList.forEach { masterMap[it.loc] = it } 51 | } 52 | 53 | fun resort() { 54 | sorted.sortBy { 55 | when { 56 | it.loc == selectedEntry -> { 57 | -2 // Put the selected p2p in the front 58 | // Non-Zero frequencies 59 | } 60 | it.frequency != 0.toShort() && 61 | it.frequency == selectedInfo?.frequency && 62 | !it.output -> { 63 | -3 // Put input in the beginning 64 | } 65 | it.frequency != 0.toShort() && it.frequency == selectedInfo?.frequency -> { 66 | -1 // Put same frequency in the front 67 | } 68 | it.frequency == 0.toShort() && mode.get() == BetterMemoryCardModes.INPUT -> { 69 | 0 // Put zero frequency to the front on Input mode 70 | } 71 | else -> { 72 | // Frequencies from lowest to highest 73 | 1 + it.frequency + Short.MAX_VALUE 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** Updates the filtered list. */ 80 | fun refilter() { 81 | filter.updateFilter(searchStr.lowercase(Locale.getDefault())) 82 | filtered = 83 | sorted 84 | .filter { 85 | if (it.loc == selectedEntry) { 86 | return@filter true 87 | } 88 | for ((f, strs) in filter.activeFilters) { 89 | if (!f.filter(it, strs?.toList())) { 90 | return@filter false 91 | } 92 | } 93 | true 94 | } 95 | .sortedBy { 96 | when { 97 | it.loc == selectedEntry -> Long.MIN_VALUE + 1 98 | it.frequency != 0.toShort() && 99 | it.frequency == selectedInfo?.frequency && 100 | !it.output -> Long.MIN_VALUE 101 | it.frequency != 0.toShort() && it.frequency == selectedInfo?.frequency -> 102 | Long.MIN_VALUE + 2 103 | filter.activeFilters.containsKey(Filter.NAME) -> { 104 | var hits = 0L 105 | var name = it.name 106 | for (f in filter.activeFilters[Filter.NAME]!!) { 107 | if (name.contains(f, true)) { 108 | hits += 1 109 | name = name.replaceFirst(f, "", true) 110 | } 111 | } 112 | -(hits * hits) + name.length 113 | } 114 | else -> it.frequency + Long.MAX_VALUE - (if (it.output) 0 else 1) 115 | } 116 | } 117 | } 118 | 119 | /** Updates the sorted list and applies the filter again. */ 120 | fun refresh() { 121 | sorted.clear() 122 | sorted.addAll(masterMap.values) 123 | resort() 124 | refilter() 125 | } 126 | 127 | /** Completely refresh the master list. */ 128 | fun rebuild(updateList: Collection, scrollbar: WidgetScrollBar, numEntries: Int) { 129 | masterMap.clear() 130 | updateList.forEach { masterMap[it.loc] = it } 131 | sorted.clear() 132 | sorted.addAll(masterMap.values) 133 | resort() 134 | // TODO: Extend the filtering mechanism. 135 | refilter() 136 | scrollbar.setRange( 137 | 0, 138 | masterMap.size.coerceIn(0, (masterMap.size - numEntries).coerceAtLeast(0)), 139 | 23 140 | ) 141 | } 142 | 143 | /** Update the master list, and send the changes downstream to sorted/filtered */ 144 | fun update(updateList: Collection, scrollbar: WidgetScrollBar, numEntries: Int) { 145 | updateList.forEach { masterMap[it.loc] = it } 146 | sorted.clear() 147 | sorted.addAll(masterMap.values) 148 | resort() 149 | // TODO: Extend the filtering mechanism. 150 | refilter() 151 | scrollbar.setRange( 152 | 0, 153 | masterMap.size.coerceIn(0, (masterMap.size - numEntries).coerceAtLeast(0)), 154 | 23 155 | ) 156 | } 157 | 158 | fun select(which: P2PLocation?) { 159 | selectedEntry = masterMap.getOrDefault(which, null)?.loc 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/RenderBlockOutline.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client 2 | 3 | import appeng.parts.BusCollisionHelper 4 | import com.mojang.blaze3d.vertex.DefaultVertexFormat 5 | import com.mojang.blaze3d.vertex.PoseStack 6 | import com.mojang.blaze3d.vertex.VertexFormat 7 | import dev.lasm.betterp2p.client.ClientCache.positions 8 | import dev.lasm.betterp2p.client.ClientCache.selectedFacing 9 | import dev.lasm.betterp2p.client.ClientCache.selectedPosition 10 | import dev.lasm.betterp2p.item.ItemAdvancedMemoryCard 11 | import java.util.* 12 | import net.minecraft.client.Camera 13 | import net.minecraft.client.renderer.MultiBufferSource 14 | import net.minecraft.client.renderer.RenderStateShard 15 | import net.minecraft.client.renderer.RenderType 16 | import net.minecraft.core.BlockPos 17 | import net.minecraft.util.Mth 18 | import net.minecraft.world.entity.player.Player 19 | import net.minecraft.world.phys.AABB 20 | import net.minecraft.world.phys.shapes.Shapes 21 | import org.lwjgl.opengl.GL11 22 | 23 | object RenderBlockOutline { 24 | @JvmStatic 25 | val LINES_BEHIND_BLOCK: RenderType = 26 | RenderType.create( 27 | "lines_behind_block", 28 | DefaultVertexFormat.POSITION_COLOR_NORMAL, 29 | VertexFormat.Mode.LINES, 30 | 256, 31 | false, 32 | false, 33 | RenderType.CompositeState.builder() 34 | .setShaderState(RenderStateShard.RENDERTYPE_LINES_SHADER) 35 | .setLineState(RenderStateShard.LineStateShard(OptionalDouble.empty())) 36 | .setLayeringState(RenderStateShard.VIEW_OFFSET_Z_LAYERING) 37 | .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) 38 | .setDepthTestState(RenderStateShard.DepthTestStateShard(">", GL11.GL_GREATER)) 39 | .setOutputState(RenderStateShard.ITEM_ENTITY_TARGET) 40 | .setWriteMaskState(RenderStateShard.COLOR_WRITE) 41 | .setCullState(RenderStateShard.NO_CULL) 42 | .createCompositeState(false) 43 | ) 44 | 45 | fun showPartPlacementPreview( 46 | player: Player?, 47 | poseStack: PoseStack, 48 | buffers: MultiBufferSource, 49 | camera: Camera 50 | ) { 51 | val item = player?.mainHandItem 52 | if (item?.item is ItemAdvancedMemoryCard) { 53 | if (positions.isNotEmpty() || selectedPosition != null) { 54 | if (selectedPosition != null) { 55 | val side = selectedFacing 56 | val boxes = ArrayList() 57 | val bch = BusCollisionHelper(boxes, side, true) 58 | bch.addBox(5.0, 5.0, 12.0, 11.0, 11.0, 13.0) 59 | bch.addBox(3.0, 3.0, 13.0, 13.0, 13.0, 14.0) 60 | bch.addBox(2.0, 2.0, 14.0, 14.0, 14.0, 16.0) 61 | renderBoxes( 62 | poseStack, 63 | buffers, 64 | camera, 65 | selectedPosition, 66 | boxes, 67 | 0x45, 68 | 0xDA, 69 | 0x75, 70 | true // TODO We need a setting here 71 | ) 72 | // 0x45DA75 73 | } 74 | // 0x66CCFF 75 | for (entry in positions.toList()) { 76 | val side = entry.component2() 77 | val boxes = ArrayList() 78 | val bch = BusCollisionHelper(boxes, side, true) 79 | bch.addBox(5.0, 5.0, 12.0, 11.0, 11.0, 13.0) 80 | bch.addBox(3.0, 3.0, 13.0, 13.0, 13.0, 14.0) 81 | bch.addBox(2.0, 2.0, 14.0, 14.0, 14.0, 16.0) 82 | renderBoxes( 83 | poseStack, 84 | buffers, 85 | camera, 86 | entry.component1(), 87 | boxes, 88 | 0x66, 89 | 0xCC, 90 | 0xFF, 91 | true // TODO We need a setting here 92 | ) 93 | } 94 | } 95 | } 96 | } 97 | 98 | fun renderBoxes( 99 | poseStack: PoseStack, 100 | buffers: MultiBufferSource, 101 | camera: Camera, 102 | pos: BlockPos?, 103 | boxes: List, 104 | red: Int, 105 | green: Int, 106 | blue: Int, 107 | insideBlock: Boolean 108 | ) { 109 | val renderType = if (insideBlock) LINES_BEHIND_BLOCK else RenderType.lines() 110 | val buffer = buffers.getBuffer(renderType) 111 | val alpha = ((if (insideBlock) 0.6f else 0.8f) * 255.0f).toInt() 112 | 113 | for (box in boxes) { 114 | val shape = Shapes.create(box) 115 | 116 | val x = pos!!.x - camera.position.x 117 | val y = pos.y - camera.position.y 118 | val z = pos.z - camera.position.z 119 | 120 | val pose = poseStack.last() 121 | shape.forAllEdges { k: Double, l: Double, m: Double, n: Double, o: Double, p: Double -> 122 | var q = (n - k).toFloat() 123 | var r = (o - l).toFloat() 124 | var s = (p - m).toFloat() 125 | val t = Mth.sqrt(q * q + r * r + s * s) 126 | buffer 127 | .addVertex(pose.pose(), (k + x).toFloat(), (l + y).toFloat(), (m + z).toFloat()) 128 | .setColor(red, green, blue, alpha) 129 | .setNormal( 130 | pose, 131 | t.let { 132 | q /= it 133 | q 134 | }, 135 | t.let { 136 | r /= it 137 | r 138 | }, 139 | t.let { 140 | s /= it 141 | s 142 | } 143 | ) 144 | buffer 145 | .addVertex(pose.pose(), (n + x).toFloat(), (o + y).toFloat(), (p + z).toFloat()) 146 | .setColor(red, green, blue, alpha) 147 | .setNormal(pose, q, r, s) 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/WidgetP2PColumn.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import dev.lasm.betterp2p.BetterP2P 4 | import dev.lasm.betterp2p.client.gui.GuiAdvancedMemoryCard 5 | import dev.lasm.betterp2p.client.gui.InfoList 6 | import dev.lasm.betterp2p.client.gui.InfoWrapper 7 | import dev.lasm.betterp2p.item.BetterMemoryCardModes 8 | import dev.lasm.betterp2p.network.packet.C2SLinkP2P 9 | import dev.lasm.betterp2p.network.packet.C2SRenameP2P 10 | import dev.lasm.betterp2p.network.packet.C2SUnlinkP2P 11 | import java.util.function.Consumer 12 | import kotlin.reflect.KProperty0 13 | import net.minecraft.client.gui.Font 14 | import net.minecraft.client.gui.GuiGraphics 15 | import net.minecraft.client.gui.components.AbstractWidget 16 | import net.minecraft.client.gui.components.Button 17 | import net.minecraft.client.gui.components.EditBox 18 | import net.minecraft.client.gui.narration.NarrationElementOutput 19 | import net.minecraft.network.chat.Component 20 | import net.neoforged.neoforge.network.PacketDistributor 21 | 22 | /** 23 | * WidgetP2PColumn 24 | * 25 | * A widget that contains a list of P2P entries. 26 | */ 27 | class WidgetP2PColumn( 28 | val gui: GuiAdvancedMemoryCard, 29 | private val infos: InfoList, 30 | x: Int, 31 | y: Int, 32 | private val selectedInfo: KProperty0, 33 | val mode: () -> BetterMemoryCardModes, 34 | private var scrollBar: WidgetScrollBar 35 | ) : AbstractWidget(x, y, 160, 0, Component.empty()) { 36 | 37 | val entries: MutableList = mutableListOf() 38 | 39 | private val renameBar by lazy { 40 | object : EditBox(font, 0, 0, 160, 12, Component.empty()) { 41 | var info: InfoWrapper? = null 42 | } 43 | } 44 | 45 | fun getRenameBar() = renameBar 46 | 47 | val font: Font 48 | get() = gui.getFont() 49 | 50 | init { 51 | renameBar.setMaxLength(50) 52 | renameBar.visible = false 53 | renameBar.setCanLoseFocus(true) 54 | } 55 | 56 | /** Resize the column */ 57 | fun resize(scale: GuiScale, availableHeight: Int) { 58 | entries.clear() 59 | for (i in 0 until scale.size(availableHeight)) { 60 | val widget = 61 | WidgetP2PDevice( 62 | i, 63 | selectedInfo, 64 | mode, 65 | { infos.filtered.getOrNull(i + scrollBar.currentScroll) }, 66 | this, 67 | x, 68 | y + i * (P2PEntryConstants.HEIGHT + 1) 69 | ) 70 | entries.add(widget) 71 | } 72 | } 73 | 74 | override fun setPosition(x: Int, y: Int) { 75 | super.setPosition(x, y) 76 | for ((i, entry) in entries.withIndex()) { 77 | entry.x = x 78 | entry.y = y + i * (P2PEntryConstants.HEIGHT + 1) 79 | } 80 | } 81 | 82 | override fun renderWidget(guiGraphics: GuiGraphics, i: Int, j: Int, f: Float) {} 83 | 84 | override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {} 85 | 86 | fun finishRename() { 87 | if (!renameBar.visible) return 88 | for (widget in entries) { 89 | widget.renderNameTextfield = true 90 | } 91 | if ( 92 | renameBar.info != null && 93 | renameBar.value.isNotEmpty() && 94 | renameBar.info!!.name != renameBar.value 95 | ) { 96 | val info: InfoWrapper = renameBar.info!! 97 | 98 | renameBar.value = renameBar.value.trim() 99 | PacketDistributor.sendToServer(C2SRenameP2P(info.loc, renameBar.value)) 100 | } 101 | renameBar.visible = false 102 | renameBar.value = "" 103 | renameBar.isFocused = false 104 | renameBar.info = null 105 | gui.focused = this 106 | } 107 | 108 | /** 109 | * Called when rename button "area" is clicked. Rename text bar must be visible after this is 110 | * called 111 | */ 112 | fun onRenameButtonClicked(info: InfoWrapper, index: Int) { 113 | renameBar.visible = true 114 | renameBar.y = (this.y) + index * (P2PEntryConstants.HEIGHT + 1) + 1 115 | renameBar.x = this.x + 50 116 | renameBar.value = info.name 117 | renameBar.isFocused = true 118 | renameBar.cursorPosition = 0 119 | renameBar.info = info 120 | gui.focused = renameBar 121 | } 122 | 123 | override fun onClick(mouseX: Double, mouseY: Double, button: Int) { 124 | val clickRenameButton = false 125 | if (!clickRenameButton && renameBar.visible) { 126 | finishRename() 127 | } 128 | super.onClick(mouseX, mouseY, button) 129 | } 130 | 131 | fun onBindButtonClicked(button: Button, info: InfoWrapper) { 132 | button.active = false 133 | if (infos.selectedEntry == null) return 134 | when (mode()) { 135 | BetterMemoryCardModes.INPUT -> { 136 | BetterP2P.logger.debug("Bind {} as input", info.loc) 137 | PacketDistributor.sendToServer(C2SLinkP2P(info.loc, infos.selectedEntry!!)) 138 | } 139 | BetterMemoryCardModes.OUTPUT -> { 140 | BetterP2P.logger.debug("Bind {} as output", info.loc) 141 | PacketDistributor.sendToServer(C2SLinkP2P(infos.selectedEntry!!, info.loc)) 142 | } 143 | BetterMemoryCardModes.COPY -> { 144 | val input = findInput(infos.selectedInfo?.frequency) 145 | if (input != null) PacketDistributor.sendToServer(C2SLinkP2P(input.loc, info.loc)) 146 | } 147 | else -> { 148 | BetterP2P.logger.debug("Somehow bind button was pressed while in UNBIND mode.") 149 | } 150 | } 151 | gui.onRefresh(null) 152 | } 153 | 154 | fun onUnbindButtonClicked(button: Button, info: InfoWrapper) { 155 | button.active = false 156 | if (info.frequency != 0.toShort()) { 157 | PacketDistributor.sendToServer(C2SUnlinkP2P(info.loc, gui.getTypeID())) 158 | info.frequency = 0.toShort() 159 | } 160 | gui.onRefresh(null) 161 | } 162 | 163 | fun findInput(frequency: Short?) = 164 | infos.filtered.find { it.frequency == frequency && !it.output } 165 | 166 | fun findOutput(frequency: Short?) = 167 | infos.filtered.find { it.frequency == frequency && it.output } 168 | 169 | override fun visitWidgets(consumer: Consumer) { 170 | super.visitWidgets(consumer) 171 | entries.forEach { it.visitWidgets(consumer) } 172 | consumer.accept(renameBar) 173 | } 174 | 175 | fun onGuiClosed() { 176 | finishRename() 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/Proxy.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p 2 | 3 | import appeng.api.parts.IPartItem 4 | import appeng.core.definitions.AEParts 5 | import appeng.core.definitions.ItemDefinition 6 | import appeng.parts.p2p.* 7 | import dev.lasm.betterp2p.util.p2p.ClientTunnelInfo 8 | import dev.lasm.betterp2p.util.p2p.TunnelInfo 9 | import java.util.function.Supplier 10 | import net.minecraft.core.registries.BuiltInRegistries 11 | import net.minecraft.resources.ResourceLocation 12 | import net.minecraft.world.item.Item 13 | import net.minecraft.world.item.ItemStack 14 | import net.neoforged.fml.ModList 15 | 16 | /** A proxy for the server */ 17 | open class CommonProxy { 18 | /** 19 | * Tunnels available in this instance. These are used to communicate p2p information between 20 | * server/client. 21 | */ 22 | protected val tunnelTypes = mutableMapOf, TunnelInfo>() 23 | 24 | /** Same as above, but maps ints -> tunnel info. */ 25 | protected val tunnelIndices = mutableMapOf() 26 | 27 | /** Discover what tunnels are available. */ 28 | open fun initTunnels() { 29 | var typeId = 0 30 | registerTunnel( 31 | def = AEParts.ME_P2P_TUNNEL, 32 | type = typeId++, 33 | classType = MEP2PTunnelPart::class.java 34 | ) 35 | registerTunnel( 36 | def = AEParts.FE_P2P_TUNNEL, 37 | type = typeId++, 38 | classType = FEP2PTunnelPart::class.java 39 | ) 40 | registerTunnel( 41 | def = AEParts.REDSTONE_P2P_TUNNEL, 42 | type = typeId++, 43 | classType = RedstoneP2PTunnelPart::class.java 44 | ) 45 | registerTunnel( 46 | def = AEParts.FLUID_P2P_TUNNEL, 47 | type = typeId++, 48 | classType = FluidP2PTunnelPart::class.java 49 | ) 50 | registerTunnel( 51 | def = AEParts.ITEM_P2P_TUNNEL, 52 | type = typeId++, 53 | classType = ItemP2PTunnelPart::class.java 54 | ) 55 | registerTunnel( 56 | def = AEParts.LIGHT_P2P_TUNNEL, 57 | type = typeId++, 58 | classType = LightP2PTunnelPart::class.java 59 | ) 60 | 61 | registerModTunnel( 62 | def = { 63 | BuiltInRegistries.ITEM[ 64 | ResourceLocation.fromNamespaceAndPath("appmek", "chemical_p2p_tunnel")] 65 | }, 66 | type = typeId++, 67 | classType = "me.ramidzkh.mekae2.ae2.ChemicalP2PTunnelPart" 68 | ) 69 | 70 | registerModTunnel( 71 | def = { 72 | BuiltInRegistries.ITEM[ 73 | ResourceLocation.fromNamespaceAndPath("mae2", "pattern_p2p_tunnel")] 74 | }, 75 | type = typeId++, 76 | classType = "stone.mae2.parts.p2p.PatternP2PTunnelPart" 77 | ) 78 | 79 | if (ModList.get().isLoaded("gtceu")) 80 | registerModTunnel( 81 | def = { 82 | BuiltInRegistries.ITEM[ 83 | ResourceLocation.fromNamespaceAndPath("mae2", "eu_p2p_tunnel")] 84 | }, 85 | type = typeId++, 86 | classType = "stone.mae2.parts.p2p.EUP2PTunnelPart" 87 | ) 88 | } 89 | 90 | private fun registerTunnel( 91 | def: ItemDefinition<*>, 92 | type: Int, 93 | classType: Class> 94 | ) { 95 | val stack = def.stack(1) 96 | val info = TunnelInfo(type, stack, classType) 97 | tunnelTypes[classType] = info 98 | tunnelIndices[type] = info 99 | } 100 | 101 | private fun registerModTunnel(def: Supplier, type: Int, classType: String) { 102 | try { 103 | val clazz = Class.forName(classType) 104 | val stack = ItemStack(def.get()) 105 | if (!P2PTunnelPart::class.java.isAssignableFrom(clazz) || def.get() !is IPartItem<*>) { 106 | BetterP2P.logger.error( 107 | "Found mod support {} but it's not a P2P tunnel, this indicates a mod update, please report it to BetterP2P repository", 108 | classType 109 | ) 110 | } 111 | 112 | val info = TunnelInfo(type, stack, clazz as Class>) 113 | tunnelTypes[clazz] = info 114 | tunnelIndices[type] = info 115 | } catch (e: ClassNotFoundException) { 116 | BetterP2P.logger.error("Mod support for {} not found", classType) 117 | } catch (e: NullPointerException) { 118 | BetterP2P.logger.error("Mod support for {} not found", classType) 119 | } 120 | } 121 | 122 | fun getP2PFromIndex(index: Int): TunnelInfo? { 123 | if (tunnelTypes.isEmpty()) initTunnels() 124 | return tunnelIndices[index] 125 | } 126 | 127 | fun getP2PFromClass(clazz: Class<*>): TunnelInfo? { 128 | if (tunnelTypes.isEmpty()) initTunnels() 129 | return tunnelTypes[clazz] 130 | } 131 | 132 | fun getP2PTypeList(): List { 133 | return tunnelIndices.values.toList() 134 | } 135 | } 136 | 137 | /** A proxy for the client */ 138 | class ClientProxy : CommonProxy() { 139 | 140 | /** Keeps a cache of icons to use in GUI. */ 141 | override fun initTunnels() { 142 | var typeId = 0 143 | registerTunnel( 144 | def = AEParts.ME_P2P_TUNNEL, 145 | type = typeId++, 146 | classType = MEP2PTunnelPart::class.java, 147 | icon = ResourceLocation.fromNamespaceAndPath("ae2", "textures/block/quartz_block.png") 148 | ) 149 | registerTunnel( 150 | def = AEParts.FE_P2P_TUNNEL, 151 | type = typeId++, 152 | classType = FEP2PTunnelPart::class.java, 153 | icon = ResourceLocation.withDefaultNamespace("textures/block/gold_block.png") 154 | ) 155 | registerTunnel( 156 | def = AEParts.REDSTONE_P2P_TUNNEL, 157 | type = typeId++, 158 | classType = RedstoneP2PTunnelPart::class.java, 159 | icon = ResourceLocation.withDefaultNamespace("textures/block/redstone_block.png") 160 | ) 161 | registerTunnel( 162 | def = AEParts.FLUID_P2P_TUNNEL, 163 | type = typeId++, 164 | classType = FluidP2PTunnelPart::class.java, 165 | icon = ResourceLocation.withDefaultNamespace("textures/block/lapis_block.png") 166 | ) 167 | registerTunnel( 168 | def = AEParts.ITEM_P2P_TUNNEL, 169 | type = typeId++, 170 | classType = ItemP2PTunnelPart::class.java, 171 | icon = ResourceLocation.withDefaultNamespace("textures/block/hopper_outside.png") 172 | ) 173 | registerTunnel( 174 | def = AEParts.LIGHT_P2P_TUNNEL, 175 | type = typeId++, 176 | classType = LightP2PTunnelPart::class.java, 177 | icon = ResourceLocation.withDefaultNamespace("textures/block/quartz_block_top.png") 178 | ) 179 | 180 | registerModTunnel( 181 | def = { 182 | BuiltInRegistries.ITEM[ 183 | ResourceLocation.fromNamespaceAndPath("appmek", "chemical_p2p_tunnel")] 184 | }, 185 | type = typeId++, 186 | classType = "me.ramidzkh.mekae2.ae2.ChemicalP2PTunnelPart", 187 | icon = 188 | ResourceLocation.fromNamespaceAndPath("mekanism", "textures/block/block_osmium.png") 189 | ) 190 | 191 | registerModTunnel( 192 | def = { 193 | BuiltInRegistries.ITEM[ 194 | ResourceLocation.fromNamespaceAndPath("mae2", "pattern_p2p_tunnel")] 195 | }, 196 | type = typeId++, 197 | classType = "stone.mae2.parts.p2p.PatternP2PTunnelPart", 198 | icon = 199 | ResourceLocation.fromNamespaceAndPath("ae2", "textures/block/pattern_provider.png") 200 | ) 201 | 202 | if (ModList.get().isLoaded("gtceu")) 203 | registerModTunnel( 204 | def = { 205 | BuiltInRegistries.ITEM[ 206 | ResourceLocation.fromNamespaceAndPath("mae2", "eu_p2p_tunnel")] 207 | }, 208 | type = typeId++, 209 | classType = "stone.mae2.parts.p2p.EUP2PTunnelPart", 210 | icon = ResourceLocation.withDefaultNamespace("textures/block/copper_block.png") 211 | ) 212 | 213 | BetterP2P.logger.info("Registered tunnel types: {}", tunnelTypes.values.joinToString(",")) 214 | } 215 | 216 | private inline fun registerTunnel( 217 | def: ItemDefinition<*>, 218 | type: Int, 219 | classType: Class>, 220 | icon: ResourceLocation 221 | ) { 222 | val stack = def.stack(1) 223 | val info = ClientTunnelInfo(type, stack, classType) { icon } 224 | tunnelTypes[classType] = info 225 | tunnelIndices[type] = info 226 | } 227 | 228 | private inline fun registerModTunnel( 229 | def: Supplier, 230 | type: Int, 231 | classType: String, 232 | icon: ResourceLocation 233 | ) { 234 | try { 235 | val clazz = Class.forName(classType) 236 | if (!P2PTunnelPart::class.java.isAssignableFrom(clazz)) { 237 | BetterP2P.logger.error( 238 | "Found mod support {} but it's not a P2P tunnel, this indicates a mod update, please report it to BetterP2P repository", 239 | classType 240 | ) 241 | } 242 | val stack = ItemStack(def.get()) 243 | val info = ClientTunnelInfo(type, stack, clazz as Class>) { icon } 244 | tunnelTypes[clazz] = info 245 | tunnelIndices[type] = info 246 | } catch (e: ClassNotFoundException) { 247 | BetterP2P.logger.error("Mod support for {} not found", classType) 248 | } catch (e: NullPointerException) { 249 | BetterP2P.logger.error("Mod support for {} not found", classType) 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/network/data/GridServerCache.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.network.data 2 | 3 | import appeng.api.networking.IGrid 4 | import appeng.api.parts.IPartItem 5 | import appeng.me.service.P2PService 6 | import appeng.parts.p2p.P2PTunnelPart 7 | import appeng.util.Platform 8 | import dev.lasm.betterp2p.BetterP2P 9 | import dev.lasm.betterp2p.util.p2p.TunnelInfo 10 | import dev.lasm.betterp2p.util.p2p.getTypeIndex 11 | import dev.lasm.betterp2p.util.p2p.pleaseSetTheFuckingOutputState 12 | import dev.lasm.betterp2p.util.p2p.setCustomName 13 | import java.util.* 14 | import net.minecraft.network.chat.Component 15 | import net.minecraft.server.level.ServerLevel 16 | import net.minecraft.world.InteractionHand 17 | import net.minecraft.world.entity.player.Player 18 | 19 | /** 20 | * When the player uses the adv memory card, this is cached on the server side to provide access to 21 | * the Grid when the player performs actions in the GUI Each player has a list of p2ps that will be 22 | * sent. These are tracked in [listP2P]. 23 | */ 24 | class GridServerCache(private val grid: IGrid, val player: Player, var type: Int) { 25 | /** The P2P list. On init, this is the full list. */ 26 | private val listP2P: MutableMap> = mutableMapOf() 27 | 28 | /** The dirty P2P list. Updates are accumulated here and sent altogether. */ 29 | private val dirtyP2P: MutableSet = mutableSetOf() 30 | 31 | init { 32 | rebuildList(type) 33 | } 34 | 35 | /** Refreshes the global p2p list */ 36 | private fun rebuildList(type: Int) { 37 | synchronized(listP2P) { 38 | listP2P.clear() 39 | dirtyP2P.clear() 40 | grid.machineClasses.forEach { 41 | // Find all P2P tunnels... 42 | if ( 43 | P2PTunnelPart::class.java.isAssignableFrom(it) && 44 | (type == TUNNEL_ANY || BetterP2P.proxy.getP2PFromIndex(type)?.clazz == it) 45 | ) { 46 | grid.getMachines(it).forEach { gridNode -> 47 | val p2p = gridNode as P2PTunnelPart<*> 48 | listP2P[p2p.toLoc()] = p2p 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Refreshes and gets the p2p list of the targeted type If [type] is [TUNNEL_ANY] or invalid, 57 | * returns the full list; else filters the list to the targeted type 58 | */ 59 | fun retrieveP2PList(): List { 60 | rebuildList(type) 61 | 62 | return listP2P.values.mapNotNull { 63 | val index = BetterP2P.proxy.getP2PFromClass(it.javaClass)?.index ?: TUNNEL_ANY 64 | if (type == TUNNEL_ANY || index == type) { 65 | it.toInfo() 66 | } else null 67 | } 68 | } 69 | 70 | /** 71 | * Sets the entry to the p2p tunnel and marks it as dirty to be sent in the next network update. 72 | * Only p2ps of the currently targeted type can be shown 73 | */ 74 | fun markDirty(key: P2PLocation, p2p: P2PTunnelPart<*>) { 75 | synchronized(listP2P) { 76 | if (type == TUNNEL_ANY || p2p.getTypeIndex() == type) { 77 | listP2P[key] = p2p 78 | dirtyP2P.add(key) 79 | } 80 | } 81 | } 82 | 83 | /** Returns the list of P2Ps that are currently marked dirty, and clears the dirty list. */ 84 | fun getP2PUpdates(): List { 85 | val result = dirtyP2P.mapNotNull { listP2P[it]?.toInfo() } 86 | 87 | dirtyP2P.clear() 88 | 89 | return result 90 | } 91 | 92 | /** 93 | * Link the two P2P tunnels together. Returns the pair of P2P tunnels on success, or null 94 | * otherwise. 95 | */ 96 | fun linkP2P( 97 | inputIndex: P2PLocation, 98 | outputIndex: P2PLocation 99 | ): Pair, P2PTunnelPart<*>>? { 100 | // If these calls mess up we have bigger problems... 101 | val input = listP2P[inputIndex] ?: return null 102 | var output = listP2P[outputIndex] ?: return null 103 | 104 | // change type if necessary 105 | if (input.javaClass != output.javaClass) { 106 | output = 107 | changeP2PType(output, BetterP2P.proxy.getP2PFromClass(input.javaClass)!!) 108 | ?: return null 109 | } 110 | 111 | // Network loop 112 | if (input == output) { 113 | return null 114 | } 115 | 116 | var frequency = input.frequency 117 | val cache = P2PService.get(input.gridNode!!.grid) 118 | 119 | // Generate a new frequency if needed 120 | if (input.frequency == 0.toShort() || input.isOutput) { 121 | frequency = cache.newFrequency() 122 | } 123 | 124 | // If tunnel was already bound, unbind that one 125 | if (cache.getInput(frequency) != null) { 126 | val originalInput = cache.getInput(frequency) 127 | if (originalInput != input) { 128 | updateP2P(originalInput.toLoc(), originalInput, frequency, true, input.customName) 129 | } 130 | } 131 | 132 | // Perform the link 133 | val inputResult: P2PTunnelPart<*> = 134 | updateP2P(inputIndex, input, frequency, false, input.customName) 135 | val outputResult: P2PTunnelPart<*> = 136 | updateP2P(outputIndex, output, frequency, true, input.customName) 137 | 138 | return inputResult to outputResult 139 | } 140 | 141 | fun unlinkP2P(p2pIndex: P2PLocation): P2PTunnelPart<*>? { 142 | val tunnel = listP2P[p2pIndex] ?: return null 143 | val oldFreq = tunnel.frequency 144 | if (oldFreq == 0.toShort()) { 145 | return tunnel 146 | } 147 | 148 | return updateP2P(p2pIndex, tunnel, 0.toShort(), false, tunnel.customName) 149 | } 150 | 151 | /** 152 | * Sets the p2p tunnel to the frequency, output, and custom name. Removes the old one and 153 | * replaces it, which lets AE2 trigger the Grid refresh for us (though we need to update the 154 | * tunnels ourselves) 155 | */ 156 | private fun updateP2P( 157 | key: P2PLocation, 158 | tunnel: P2PTunnelPart<*>, 159 | frequency: Short, 160 | output: Boolean, 161 | name: Component? 162 | ): P2PTunnelPart<*> { 163 | val service = P2PService.get(tunnel.mainNode.grid) 164 | 165 | tunnel.pleaseSetTheFuckingOutputState(output) 166 | tunnel.setCustomName(name) 167 | 168 | service.updateFreq(tunnel, frequency) 169 | 170 | Platform.notifyBlocksOfNeighbors(tunnel.blockEntity.level, tunnel.blockEntity.blockPos) 171 | return tunnel 172 | } 173 | 174 | /** 175 | * Converts one P2P into the type 176 | * @see P2PTunnelPart.onPartActivate 177 | */ 178 | private fun changeP2PType(tunnel: P2PTunnelPart<*>, newType: TunnelInfo): P2PTunnelPart<*>? { 179 | if (BetterP2P.proxy.getP2PFromClass(tunnel.javaClass) == newType) { 180 | player.displayClientMessage( 181 | Component.translatable("gui.advanced_memory_card.error.same_type"), 182 | false 183 | ) 184 | return null 185 | } 186 | 187 | val level = tunnel.blockEntity?.level 188 | if (level is ServerLevel) { 189 | var newBus: P2PTunnelPart<*>? = null 190 | level.server.executeBlocking { 191 | val oldOutput: Boolean = tunnel.isOutput 192 | val freq = tunnel.frequency 193 | // Regular checks 194 | Objects.requireNonNull(tunnel.blockEntity) 195 | if (newType.stack.item !is IPartItem<*>) { 196 | BetterP2P.logger.error( 197 | "Attempt to assign a invalid type {} to tunnel {}, this shouldn't happen!", 198 | newType, 199 | tunnel.blockEntity 200 | ) 201 | return@executeBlocking 202 | } 203 | val partItem = newType.stack.item as IPartItem<*> 204 | if (!P2PTunnelPart::class.java.isAssignableFrom(partItem.partClass)) { 205 | BetterP2P.logger.error( 206 | "Attempt to assign a invalid type {} to tunnel {}, this shouldn't happen!", 207 | partItem.partClass, 208 | tunnel.blockEntity 209 | ) 210 | return@executeBlocking 211 | } 212 | 213 | newBus = tunnel 214 | if (newBus!!.partItem !== partItem) { 215 | val replaced = 216 | tunnel.host.replacePart( 217 | partItem, 218 | tunnel.side, 219 | player, 220 | InteractionHand.MAIN_HAND 221 | )!! 222 | as P2PTunnelPart<*> 223 | 224 | replaced.pleaseSetTheFuckingOutputState(oldOutput) 225 | replaced.onTunnelNetworkChange() 226 | 227 | replaced.setFrequency(freq) 228 | newBus = replaced 229 | } 230 | 231 | Platform.notifyBlocksOfNeighbors(tunnel.level, tunnel.blockEntity.blockPos) 232 | } 233 | return newBus 234 | } 235 | 236 | return tunnel 237 | } 238 | 239 | /** Converts all connected P2Ps to a new type */ 240 | fun changeAllP2Ps(p2p: P2PLocation, newType: TunnelInfo): Boolean { 241 | 242 | var tunnel = listP2P[p2p] ?: return false 243 | 244 | try { 245 | if (tunnel.isOutput && tunnel.getInput() != null) { 246 | tunnel = tunnel.getInput()!! 247 | } 248 | 249 | val outputs = tunnel.outputs.toMutableList() 250 | /* TODO: static p2p 251 | if (newType.clazz.superclass == P2PTunnelPartStatic::class.java) { 252 | val amt = outputs.size + 1 253 | var hasItems = 0 254 | for (stack in player.inventory.mainInventory) { 255 | if (stack?.isItemEqual(newType.stack) == true) { 256 | hasItems += stack.stackSize 257 | if (hasItems >= amt) { 258 | break 259 | } 260 | } 261 | } 262 | if (hasItems < amt) { 263 | player.addChatMessage(ChatComponentTranslation("gui.advanced_memory_card.error.missing_items", amt, newType.stack.displayName)) 264 | return false 265 | } 266 | } 267 | */ 268 | changeP2PType(tunnel, newType) 269 | for (o in outputs) { 270 | changeP2PType(o, newType) 271 | } 272 | return true 273 | } catch (e: Exception) { 274 | // :P 275 | } 276 | return false 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/widget/WidgetP2PDevice.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui.widget 2 | 3 | import appeng.client.gui.widgets.ITooltip 4 | import dev.lasm.betterp2p.client.gui.InfoWrapper 5 | import dev.lasm.betterp2p.client.gui.drawBlockIcon 6 | import dev.lasm.betterp2p.client.gui.drawIcon 7 | import dev.lasm.betterp2p.client.gui.isClicked 8 | import dev.lasm.betterp2p.item.BetterMemoryCardModes 9 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 10 | import dev.lasm.betterp2p.network.packet.C2SChangeP2PType 11 | import dev.lasm.betterp2p.util.p2p.ClientTunnelInfo 12 | import java.util.function.Consumer 13 | import kotlin.reflect.KProperty0 14 | import net.minecraft.client.Minecraft 15 | import net.minecraft.client.gui.GuiGraphics 16 | import net.minecraft.client.gui.components.AbstractWidget 17 | import net.minecraft.client.gui.components.Button 18 | import net.minecraft.client.gui.narration.NarrationElementOutput 19 | import net.minecraft.client.renderer.Rect2i 20 | import net.minecraft.network.chat.Component 21 | import net.neoforged.neoforge.network.PacketDistributor 22 | import org.lwjgl.glfw.GLFW 23 | 24 | object P2PEntryConstants { 25 | const val HEIGHT = 41 26 | const val WIDTH = 254 27 | const val OUTPUT_COLOR = 0x4566ccff 28 | const val SELECTED_COLOR = 0x4545DA75 29 | const val ERROR_COLOR = 0x45DA4527 30 | const val INACTIVE_COLOR = 0x45FFEA05 31 | const val LEFT_ALIGN = 24 32 | } 33 | 34 | class WidgetP2PDevice( 35 | private val index: Int, 36 | private val selectedInfoProperty: KProperty0, 37 | val modeSupplier: () -> BetterMemoryCardModes, 38 | val infoSupplier: () -> InfoWrapper?, 39 | val col: WidgetP2PColumn, 40 | x: Int, 41 | y: Int 42 | ) : 43 | AbstractWidget(x, y, P2PEntryConstants.WIDTH, P2PEntryConstants.HEIGHT, Component.empty()), 44 | ITypeReceiver, 45 | ITooltip { 46 | 47 | val font 48 | get() = Minecraft.getInstance().font 49 | 50 | var renderNameTextfield = true 51 | 52 | private val selectedInfo: InfoWrapper? 53 | get() = selectedInfoProperty.get() 54 | 55 | val bindButton: Button = 56 | Button.builder(Component.translatable("gui.advanced_memory_card.bind")) { 57 | col.onBindButtonClicked(it, infoSupplier()!!) 58 | } 59 | .size(56, 20) 60 | .build() 61 | val unbindButton: Button = 62 | Button.builder(Component.translatable("gui.advanced_memory_card.unbind")) { 63 | col.onUnbindButtonClicked(it, infoSupplier()!!) 64 | } 65 | .size(56, 20) 66 | .build() 67 | 68 | /** Update the button visibility */ 69 | fun updateButtonVisibility() { 70 | val info = infoSupplier() 71 | val mode = modeSupplier() 72 | 73 | if (info == null) { 74 | bindButton.visible = false 75 | unbindButton.visible = false 76 | return 77 | } 78 | 79 | when { 80 | selectedInfo == null || 81 | mode == BetterMemoryCardModes.COPY && 82 | ((!info.output && info.frequency != 0.toShort()) || selectedInfo!!.output) -> { 83 | // Copy mode 84 | // If this info is (input && set freq) || selected info is an output 85 | // Disable all buttons 86 | bindButton.visible = false 87 | unbindButton.visible = false 88 | } 89 | mode == BetterMemoryCardModes.UNBIND -> { 90 | // Only unbinds allowed in unbind mode 91 | bindButton.visible = false 92 | unbindButton.visible = info.frequency != 0.toShort() 93 | unbindButton.active = unbindButton.visible 94 | } 95 | else -> { 96 | // Other modes: 97 | // Bind allowed only if currently not selected && selected is unbound; OR not bound 98 | // to 99 | // selected 100 | bindButton.visible = 101 | info.loc != selectedInfo!!.loc && 102 | (selectedInfo!!.frequency == 0.toShort() || 103 | info.frequency != selectedInfo!!.frequency) 104 | bindButton.active = bindButton.visible 105 | unbindButton.visible = false 106 | } 107 | } 108 | } 109 | 110 | override fun updateWidgetNarration(narrationElementOutput: NarrationElementOutput) {} 111 | 112 | override fun renderWidget( 113 | graphics: GuiGraphics, 114 | mouseX: Int, 115 | mouseY: Int, 116 | partialTicks: Float 117 | ) { 118 | val info = infoSupplier() ?: return 119 | // draw the background first 120 | when { 121 | selectedInfo?.loc == info.loc -> { 122 | graphics.fill( 123 | x, 124 | y, 125 | x + P2PEntryConstants.WIDTH, 126 | y + P2PEntryConstants.HEIGHT, 127 | P2PEntryConstants.SELECTED_COLOR 128 | ) 129 | } 130 | info.error -> { 131 | // P2P output without an input, or unbound 132 | graphics.fill( 133 | x, 134 | y, 135 | x + P2PEntryConstants.WIDTH, 136 | y + P2PEntryConstants.HEIGHT, 137 | P2PEntryConstants.ERROR_COLOR 138 | ) 139 | } 140 | !info.hasChannel && info.frequency != 0.toShort() -> { 141 | // No channel 142 | graphics.fill( 143 | x, 144 | y, 145 | x + P2PEntryConstants.WIDTH, 146 | y + P2PEntryConstants.HEIGHT, 147 | P2PEntryConstants.INACTIVE_COLOR 148 | ) 149 | } 150 | selectedInfo?.frequency == info.frequency && info.frequency != 0.toShort() -> { 151 | // Show same frequency 152 | graphics.fill( 153 | x, 154 | y, 155 | x + P2PEntryConstants.WIDTH, 156 | y + P2PEntryConstants.HEIGHT, 157 | P2PEntryConstants.OUTPUT_COLOR 158 | ) 159 | } 160 | } 161 | 162 | if ( 163 | isHovered && 164 | mouseX > x.toDouble() + 50 && 165 | mouseX < x.toDouble() + 50 + 160 && 166 | mouseY > y.toDouble() + 1 && 167 | mouseY < y.toDouble() + 1 + 13 168 | ) { 169 | graphics.fill( 170 | x + 50, 171 | y + 1, 172 | x + 50 + 160, 173 | y + 1 + 12, 174 | 0x6E000000 // ARGB xd 175 | ) 176 | } 177 | 178 | graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f) 179 | // Draw our icons... 180 | drawBlockIcon(graphics, info.icon, info.overlay, x + 3, y + 3) 181 | 182 | if (info.output) { 183 | graphics.drawIcon(144, 200, x, y + 4) 184 | } else { 185 | graphics.drawIcon(128, 200, x, y + 4) 186 | } 187 | if (info.error || info.frequency == 0.toShort() || !info.hasChannel) { 188 | graphics.drawIcon(144, 216, x + 3, y + 20) 189 | } else { 190 | graphics.drawIcon(128, 216, x + 3, y + 20) 191 | } 192 | // Now draw the stuff that messes up our GL state (aka text) 193 | val leftAlign = x + P2PEntryConstants.LEFT_ALIGN 194 | if (renderNameTextfield) { 195 | graphics.drawString( 196 | font, 197 | Component.translatable("gui.advanced_memory_card.name", info.name), 198 | leftAlign, 199 | y + 2, 200 | 0x404040, 201 | false 202 | ) 203 | } else { 204 | graphics.drawString( 205 | font, 206 | Component.translatable("gui.advanced_memory_card.name", ""), 207 | leftAlign, 208 | y + 2, 209 | 0x404040, 210 | false 211 | ) 212 | } 213 | graphics.drawString(font, info.description, leftAlign, y + 12, 0x404040, false) 214 | graphics.drawString(font, info.freqDisplay, leftAlign, y + 22, 0x404040, false) 215 | if (info.channels != null) { 216 | graphics.drawString(font, info.channels!!, leftAlign, y + 32, 0x404040, false) 217 | } 218 | 219 | updateButtonVisibility() 220 | } 221 | 222 | override fun clicked(mouseX: Double, mouseY: Double): Boolean { 223 | return bindButton.isClicked(mouseX, mouseY) || 224 | unbindButton.isClicked(mouseX, mouseY) || 225 | super.clicked(mouseX, mouseY) 226 | } 227 | 228 | override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { 229 | if (clicked(mouseX, mouseY) && button == GLFW.GLFW_MOUSE_BUTTON_RIGHT) { 230 | this.playDownSound(Minecraft.getInstance().soundManager) 231 | col.gui.openTypeSelector(this, false) 232 | return true 233 | } 234 | return bindButton.mouseClicked(mouseX, mouseY, button) || 235 | unbindButton.mouseClicked(mouseX, mouseY, button) || 236 | super.mouseClicked(mouseX, mouseY, button) 237 | } 238 | 239 | override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { 240 | if (col.getRenameBar().visible) { 241 | col.getRenameBar().isFocused = true 242 | if (keyCode == GLFW.GLFW_KEY_ENTER) { 243 | col.finishRename() 244 | } else { 245 | return col.getRenameBar().keyPressed(keyCode, scanCode, modifiers) 246 | } 247 | return true 248 | } 249 | return super.keyPressed(keyCode, scanCode, modifiers) 250 | } 251 | 252 | override fun charTyped(codePoint: Char, modifiers: Int): Boolean { 253 | if (col.getRenameBar().visible) { 254 | col.getRenameBar().isFocused = true 255 | return col.getRenameBar().charTyped(codePoint, modifiers) 256 | } 257 | return super.charTyped(codePoint, modifiers) 258 | } 259 | 260 | override fun onClick(mouseX: Double, mouseY: Double, button: Int) { 261 | val info = infoSupplier() ?: return 262 | if ( 263 | isHovered && 264 | mouseX > x.toDouble() + 50 && 265 | mouseX < x.toDouble() + 50 + 160 && 266 | mouseY > y.toDouble() + 1 && 267 | mouseY < y.toDouble() + 1 + 13 268 | ) { 269 | col.onRenameButtonClicked(info, index) 270 | } else { 271 | col.gui.selectInfo(info.loc) 272 | col.finishRename() 273 | } 274 | } 275 | 276 | override fun visitWidgets(consumer: Consumer) { 277 | super.visitWidgets(consumer) 278 | bindButton.x = x + 190 279 | bindButton.width = 56 280 | bindButton.y = y + 14 281 | 282 | unbindButton.x = x + 190 283 | unbindButton.width = 56 284 | unbindButton.y = y + 14 285 | consumer.accept(bindButton) 286 | consumer.accept(unbindButton) 287 | } 288 | 289 | override fun accept(type: ClientTunnelInfo?) { 290 | PacketDistributor.sendToServer( 291 | C2SChangeP2PType(type?.index ?: TUNNEL_ANY, infoSupplier()!!.loc) 292 | ) 293 | col.gui.closeTypeSelector(type) 294 | } 295 | 296 | override fun getTooltipMessage(): MutableList { 297 | return infoSupplier()!!.hoverInfo.toMutableList() 298 | } 299 | 300 | override fun getTooltipArea(): Rect2i { 301 | return Rect2i(x, y, 20, height) 302 | } 303 | 304 | override fun isTooltipAreaVisible(): Boolean { 305 | return visible && infoSupplier() != null 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/lasm/betterp2p/client/gui/GuiAdvancedMemoryCard.kt: -------------------------------------------------------------------------------- 1 | package dev.lasm.betterp2p.client.gui 2 | 3 | import appeng.client.gui.Tooltip 4 | import appeng.client.gui.style.Blitter 5 | import appeng.client.gui.widgets.ITooltip 6 | import appeng.parts.p2p.FluidP2PTunnelPart 7 | import appeng.parts.p2p.MEP2PTunnelPart 8 | import com.mojang.blaze3d.systems.RenderSystem 9 | import dev.lasm.betterp2p.BetterP2P 10 | import dev.lasm.betterp2p.BetterP2P.MOD_ID 11 | import dev.lasm.betterp2p.client.AdvancedMemoryCardMenu 12 | import dev.lasm.betterp2p.client.ClientCache 13 | import dev.lasm.betterp2p.client.gui.widget.* 14 | import dev.lasm.betterp2p.item.BetterMemoryCardModes 15 | import dev.lasm.betterp2p.item.MAX_TOOLTIP_LENGTH 16 | import dev.lasm.betterp2p.network.data.MemoryInfo 17 | import dev.lasm.betterp2p.network.data.P2PInfo 18 | import dev.lasm.betterp2p.network.data.P2PLocation 19 | import dev.lasm.betterp2p.network.data.TUNNEL_ANY 20 | import dev.lasm.betterp2p.network.packet.C2SCloseGui 21 | import dev.lasm.betterp2p.network.packet.C2SRefreshP2PList 22 | import dev.lasm.betterp2p.network.packet.C2SUpdateMemoryInfo 23 | import dev.lasm.betterp2p.util.p2p.ClientTunnelInfo 24 | import java.util.Optional 25 | import kotlin.jvm.optionals.getOrNull 26 | import net.minecraft.ChatFormatting 27 | import net.minecraft.client.gui.Font 28 | import net.minecraft.client.gui.GuiGraphics 29 | import net.minecraft.client.gui.components.Button 30 | import net.minecraft.client.gui.components.ComponentRenderUtils 31 | import net.minecraft.client.gui.components.EditBox 32 | import net.minecraft.client.gui.components.events.ContainerEventHandler 33 | import net.minecraft.client.gui.screens.Screen 34 | import net.minecraft.client.gui.screens.inventory.MenuAccess 35 | import net.minecraft.client.renderer.Rect2i 36 | import net.minecraft.client.resources.language.I18n 37 | import net.minecraft.network.chat.Component 38 | import net.minecraft.network.chat.Style 39 | import net.minecraft.resources.ResourceLocation 40 | import net.minecraft.util.FormattedCharSequence 41 | import net.neoforged.neoforge.network.PacketDistributor 42 | 43 | val TEXTURE = ResourceLocation.fromNamespaceAndPath(MOD_ID, "textures/gui/advanced_memory_card.png") 44 | const val GUI_WIDTH = 288 45 | const val GUI_TEX_HEIGHT = 264 46 | 47 | val TEXTURE_BLITTER = Blitter.texture(TEXTURE, GUI_WIDTH, GUI_TEX_HEIGHT) 48 | 49 | class GuiAdvancedMemoryCard(val theMenu: AdvancedMemoryCardMenu) : 50 | Screen(Component.empty()), MenuAccess, ContainerEventHandler { 51 | private var ySize: Int = 0 52 | private var leftPos: Int = 0 53 | private var topPos: Int = 0 54 | 55 | private val tableX = 9 56 | private val tableY = 19 57 | 58 | private var type: ClientTunnelInfo? = 59 | BetterP2P.proxy.getP2PFromIndex(menu.memoryInfo.type) as? ClientTunnelInfo 60 | 61 | var memoryInfo = theMenu.memoryInfo 62 | var scale = memoryInfo.guiScale 63 | 64 | val resizeButton = IconButton(scale.ordinal * 32, 200, this::onResize) 65 | 66 | val typeButton = P2PTypeButton(::type, this::onChangeType, this::openTypeSelector) 67 | 68 | val refreshButton = IconButton(160, 200, this::onRefresh) 69 | 70 | fun onRefresh(_button: Button?) { 71 | PacketDistributor.sendToServer(C2SRefreshP2PList(type?.index ?: TUNNEL_ANY)) 72 | } 73 | 74 | private fun onChangeType(button: Button) { 75 | val button = button as P2PTypeButton 76 | type = button.nextType(false) 77 | button.commitType() 78 | } 79 | 80 | private val searchText: String 81 | get() = searchBar.value 82 | 83 | private val infos = InfoList(menu.infos.map(::InfoWrapper), ::searchText, ::mode) 84 | 85 | private val typeSelector: WidgetTypeSelector 86 | 87 | private var mode = menu.memoryInfo.mode 88 | 89 | private val sortRules: List by lazy { 90 | listOf( 91 | Component.translatable("gui.advanced_memory_card.sortinfo1") 92 | .withStyle(Style.EMPTY.withColor(ChatFormatting.AQUA).withUnderlined(true)), 93 | Component.literal("@in") 94 | .withStyle(ChatFormatting.BLUE) 95 | .append( 96 | Component.literal(" - ") 97 | .withStyle(ChatFormatting.GRAY) 98 | .append(Component.translatable("gui.advanced_memory_card.sortinfo2")) 99 | ), 100 | Component.literal("@out") 101 | .withStyle(ChatFormatting.GOLD) 102 | .append( 103 | Component.literal(" - ") 104 | .withStyle(ChatFormatting.GRAY) 105 | .append(Component.translatable("gui.advanced_memory_card.sortinfo3")) 106 | ), 107 | Component.literal("@b") 108 | .withStyle(ChatFormatting.GREEN) 109 | .append( 110 | Component.literal(" - ") 111 | .withStyle(ChatFormatting.GRAY) 112 | .append(Component.translatable("gui.advanced_memory_card.sortinfo4")) 113 | ), 114 | Component.literal("@u") 115 | .withStyle(ChatFormatting.RED) 116 | .append( 117 | Component.literal(" - ") 118 | .withStyle(ChatFormatting.GRAY) 119 | .append(Component.translatable("gui.advanced_memory_card.sortinfo5")) 120 | ), 121 | Component.literal("@type=[;;]...") 122 | .withStyle(ChatFormatting.YELLOW) 123 | .append( 124 | Component.literal(" - ") 125 | .withStyle(ChatFormatting.GRAY) 126 | .append(Component.translatable("gui.advanced_memory_card.sortinfo6")) 127 | ), 128 | Component.translatable("gui.advanced_memory_card.sortinfo7") 129 | .withStyle(ChatFormatting.GRAY) 130 | ) 131 | } 132 | 133 | val scrollBar = WidgetScrollBar(tableX, tableY) 134 | 135 | val searchBar: EditBox by lazy { 136 | object : EditBox(font, 0, 0, 100, 10, Component.empty()), ITooltip { 137 | override fun getTooltipMessage(): MutableList { 138 | return sortRules.toMutableList() 139 | } 140 | 141 | override fun getTooltipArea(): Rect2i { 142 | return Rect2i(x, y, width, height) 143 | } 144 | 145 | override fun isTooltipAreaVisible(): Boolean { 146 | return isVisible 147 | } 148 | } 149 | } 150 | 151 | fun getFont(): Font = font 152 | 153 | val modeButton by lazy { IconButton((mode.ordinal + 3) * 32, 232, ::onChangeMode) } 154 | 155 | val col by lazy { WidgetP2PColumn(this, infos, 0, 0, ::selectedInfo, ::mode, scrollBar) } 156 | 157 | private val selectedInfo: InfoWrapper? 158 | get() = infos.selectedInfo 159 | 160 | init { 161 | val typeSelectorList = mutableListOf() 162 | var toAdd = 163 | BetterP2P.proxy.getP2PFromClass(MEP2PTunnelPart::class.java) as? ClientTunnelInfo 164 | if (toAdd != null) { 165 | typeSelectorList.add(toAdd) 166 | } 167 | toAdd = BetterP2P.proxy.getP2PFromClass(FluidP2PTunnelPart::class.java) as? ClientTunnelInfo 168 | if (toAdd != null) { 169 | typeSelectorList.add(toAdd) 170 | } 171 | BetterP2P.proxy.getP2PTypeList().forEach { 172 | if (!typeSelectorList.contains(it)) { 173 | typeSelectorList.add(it as ClientTunnelInfo) 174 | } 175 | } 176 | typeSelector = WidgetTypeSelector(0, 0, this, typeSelectorList) 177 | } 178 | 179 | private val modeDescriptions: List> = 180 | listOf( 181 | fmtTooltips( 182 | title = BetterMemoryCardModes.OUTPUT.unlocalizedName, 183 | maxChars = MAX_TOOLTIP_LENGTH, 184 | keys = BetterMemoryCardModes.OUTPUT.unlocalizedDesc 185 | ), 186 | fmtTooltips( 187 | title = BetterMemoryCardModes.INPUT.unlocalizedName, 188 | maxChars = MAX_TOOLTIP_LENGTH, 189 | keys = BetterMemoryCardModes.INPUT.unlocalizedDesc 190 | ), 191 | fmtTooltips( 192 | title = BetterMemoryCardModes.COPY.unlocalizedName, 193 | maxChars = MAX_TOOLTIP_LENGTH, 194 | keys = BetterMemoryCardModes.COPY.unlocalizedDesc 195 | ), 196 | fmtTooltips( 197 | title = BetterMemoryCardModes.UNBIND.unlocalizedName, 198 | maxChars = MAX_TOOLTIP_LENGTH, 199 | keys = BetterMemoryCardModes.UNBIND.unlocalizedDesc 200 | ) 201 | ) 202 | 203 | fun onChangeMode(button: Button) { 204 | val button = button as IconButton 205 | mode = mode.next() 206 | button.texX = (mode.ordinal + 3) * 32 207 | button.texY = 232 208 | button.messages = modeDescriptions[mode.ordinal].toMutableList() 209 | syncMemoryInfo() 210 | } 211 | 212 | fun onResize(button: Button) { 213 | scale = 214 | when (scale) { 215 | GuiScale.DYNAMIC -> GuiScale.LARGE 216 | GuiScale.LARGE -> GuiScale.NORMAL 217 | GuiScale.NORMAL -> GuiScale.SMALL 218 | GuiScale.SMALL -> GuiScale.DYNAMIC 219 | } 220 | repositionElements() 221 | } 222 | 223 | override fun init() { 224 | checkInfo() 225 | 226 | val h = height.coerceAtLeast(256) 227 | if (scale.minHeight > h) { 228 | scale = GuiScale.DYNAMIC 229 | } 230 | 231 | val numEntries = scale.size(height - 75) 232 | this.ySize = (numEntries * P2PEntryConstants.HEIGHT) + 75 + (numEntries - 1) 233 | 234 | this.leftPos = (this.width - GUI_WIDTH) / 2 235 | this.topPos = (this.height - ySize) / 2 236 | 237 | searchBar.x = leftPos + 163 238 | searchBar.y = topPos + 5 239 | searchBar.setResponder { 240 | infos.refresh() 241 | col.entries.forEach { it.updateButtonVisibility() } 242 | scrollBar.height = numEntries * P2PEntryConstants.HEIGHT + (numEntries - 1) - 7 243 | scrollBar.setRange( 244 | 0, 245 | infos.filtered.size.coerceIn( 246 | 0, 247 | (infos.filtered.size - numEntries).coerceAtLeast(0) 248 | ), 249 | 23 250 | ) 251 | } 252 | 253 | scrollBar.x = leftPos + 268 254 | scrollBar.y = topPos + 19 255 | 256 | resizeButton.x = leftPos - 32 257 | resizeButton.y = topPos + 2 258 | resizeButton.texX = scale.ordinal * 32 259 | resizeButton.messages[0] = Component.literal("Resize") 260 | 261 | modeButton.x = leftPos - 32 262 | modeButton.y = topPos + 34 263 | modeButton.texX = (mode.ordinal + 3) * 32 264 | modeButton.messages = modeDescriptions[mode.ordinal].toMutableList() 265 | 266 | typeButton.setPosition(leftPos - 32, topPos + 66) 267 | 268 | refreshButton.setPosition(leftPos - 32, topPos + 98) 269 | refreshButton.messages[0] = Component.literal("Refresh") 270 | 271 | col.resize(scale, h - 75) 272 | col.setPosition(leftPos + tableX, topPos + tableY) 273 | 274 | infos.select(memoryInfo.selectedEntry.getOrNull()) 275 | infos.refresh() 276 | 277 | scrollBar.height = numEntries * P2PEntryConstants.HEIGHT + (numEntries - 1) - 7 278 | scrollBar.setRange( 279 | 0, 280 | infos.filtered.size.coerceIn(0, (infos.filtered.size - numEntries).coerceAtLeast(0)), 281 | 23 282 | ) 283 | 284 | col.entries.forEach { it.updateButtonVisibility() } 285 | 286 | typeSelector.parent = typeButton 287 | typeSelector.visible = false 288 | 289 | checkInfo() 290 | refreshOverlay() 291 | 292 | selectInfo(memoryInfo.selectedEntry.getOrNull()) 293 | 294 | col.visitWidgets(::addRenderableWidget) 295 | addRenderableWidget(refreshButton) 296 | addRenderableWidget(scrollBar) 297 | addRenderableWidget(typeButton) 298 | addRenderableWidget(resizeButton) 299 | addRenderableWidget(modeButton) 300 | addRenderableWidget(searchBar) 301 | addRenderableWidget(typeSelector) 302 | } 303 | 304 | override fun isPauseScreen(): Boolean { 305 | return false 306 | } 307 | 308 | override fun afterMouseAction() { 309 | super.afterMouseAction() 310 | } 311 | 312 | override fun renderMenuBackground(graphics: GuiGraphics) { 313 | super.renderMenuBackground(graphics) 314 | graphics.blit(TEXTURE, leftPos, topPos, 0, 0.0f, 0.0f, GUI_WIDTH, 60, 288, 264) 315 | 316 | val p2pHeight = P2PEntryConstants.HEIGHT + 1 317 | for (i in 0 until scale.size(ySize - 75) - 2) { 318 | graphics.blit( 319 | TEXTURE, 320 | leftPos, 321 | topPos + 60 + p2pHeight * i, 322 | 0, 323 | 0.0f, 324 | 60.0f, 325 | GUI_WIDTH, 326 | 102 - 60, 327 | 288, 328 | 264 329 | ) 330 | } 331 | graphics.blit( 332 | TEXTURE, 333 | leftPos, 334 | topPos + ySize - 98, 335 | 0, 336 | 0.0f, 337 | 102.0f, 338 | GUI_WIDTH, 339 | 98, 340 | 288, 341 | 264 342 | ) 343 | } 344 | 345 | override fun mouseScrolled( 346 | mouseX: Double, 347 | mouseY: Double, 348 | scrollX: Double, 349 | scrollY: Double 350 | ): Boolean { 351 | return scrollBar.mouseScrolled(mouseX, mouseY, scrollX, scrollY) || 352 | super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) 353 | } 354 | 355 | override fun render(graphics: GuiGraphics, i: Int, j: Int, f: Float) { 356 | this.renderMenuBackground(graphics) 357 | 358 | graphics.drawString( 359 | font, 360 | Component.translatable("item.betterp2p.advanced_memory_card"), 361 | leftPos + tableX, 362 | topPos + 6, 363 | 0x404040, 364 | false 365 | ) 366 | 367 | RenderSystem.disableDepthTest() 368 | RenderSystem.depthMask(false) 369 | 370 | super.render(graphics, i, j, f) 371 | 372 | renderTooltips(graphics, i, j) 373 | 374 | RenderSystem.depthMask(true) 375 | RenderSystem.enableDepthTest() 376 | } 377 | 378 | private fun syncMemoryInfo() { 379 | PacketDistributor.sendToServer( 380 | C2SUpdateMemoryInfo( 381 | MemoryInfo( 382 | Optional.ofNullable(infos.selectedEntry), 383 | selectedInfo?.frequency ?: 0, 384 | mode, 385 | scale, 386 | type?.index ?: TUNNEL_ANY 387 | ) 388 | ) 389 | ) 390 | } 391 | 392 | private fun checkInfo() { 393 | 394 | infos.filtered.forEach { 395 | it.error = 396 | it.frequency != 0.toShort() && 397 | if (it.output) { 398 | col.findInput(it.frequency) == null 399 | } else { 400 | col.findOutput(it.frequency) == null 401 | } 402 | } 403 | } 404 | 405 | fun refreshInfo(infos: List) { 406 | this.infos.rebuild(infos.map(::InfoWrapper), scrollBar, scale.size(height - 75)) 407 | checkInfo() 408 | refreshOverlay() 409 | } 410 | 411 | fun updateInfo(infos: List) { 412 | this.infos.update(infos.map(::InfoWrapper), scrollBar, scale.size(height - 75)) 413 | checkInfo() 414 | refreshOverlay() 415 | } 416 | 417 | private fun renderTooltips(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int) { 418 | for (c in children()) { 419 | if (c is ITooltip) { 420 | if (!c.isTooltipAreaVisible) { 421 | continue 422 | } 423 | 424 | val area: Rect2i = c.tooltipArea 425 | if ( 426 | mouseX >= area.x && 427 | mouseY >= area.y && 428 | mouseX < area.x + area.width && 429 | mouseY < area.y + area.height 430 | ) { 431 | val tooltip = Tooltip(c.tooltipMessage) 432 | if (tooltip.content.isNotEmpty()) { 433 | drawTooltipWithHeader(guiGraphics, tooltip, mouseX, mouseY) 434 | } 435 | } 436 | } 437 | } 438 | 439 | // // Widget-container uses screen-relative coordinates while the rest uses 440 | // window-relative 441 | // val tooltip: Tooltip = this.widgets.getTooltip(mouseX - leftPos, mouseY - topPos) 442 | // if (tooltip != null) { 443 | // drawTooltipWithHeader(guiGraphics, tooltip, mouseX, mouseY) 444 | // } 445 | } 446 | 447 | private fun drawTooltipWithHeader( 448 | guiGraphics: GuiGraphics, 449 | tooltip: Tooltip, 450 | mouseX: Int, 451 | mouseY: Int 452 | ) { 453 | drawTooltipWithHeader(guiGraphics, mouseX, mouseY, tooltip.content) 454 | } 455 | 456 | override fun onClose() { 457 | ClientCache.searchText = searchBar.value 458 | col.onGuiClosed() 459 | syncMemoryInfo() 460 | PacketDistributor.sendToServer(C2SCloseGui()) 461 | super.onClose() 462 | } 463 | 464 | fun drawTooltip(guiGraphics: GuiGraphics, x: Int, y: Int, lines: List) { 465 | if (lines.isEmpty()) { 466 | return 467 | } 468 | 469 | // Max width should be half screen with some padding. 470 | // Vanilla will place the tooltip on the right or left of the cursor 471 | // automatically, but uses a 12px offset (we use 40px for some extra space) 472 | val maxWidth = width / 2 - 40 473 | 474 | // Make the first line white 475 | // All lines after the first are colored gray 476 | val styledLines: MutableList = java.util.ArrayList(lines.size) 477 | for (line in lines) { 478 | styledLines.addAll(ComponentRenderUtils.wrapComponents(line, maxWidth, font)) 479 | } 480 | guiGraphics.renderTooltip(font, styledLines, x, y) 481 | } 482 | 483 | fun drawTooltipWithHeader(guiGraphics: GuiGraphics, x: Int, y: Int, lines: List) { 484 | if (lines.isEmpty()) { 485 | return 486 | } 487 | 488 | val formattedLines = ArrayList(lines.size) 489 | for (i in lines.indices) { 490 | if (i == 0) { 491 | formattedLines.add( 492 | lines[i].copy().withStyle { s: Style -> s.withColor(ChatFormatting.WHITE) } 493 | ) 494 | } else { 495 | formattedLines.add( 496 | lines[i].copy().withStyle { s: Style -> 497 | if (s.color != null) { 498 | return@withStyle s 499 | } else { 500 | return@withStyle s.withColor(ChatFormatting.GRAY) 501 | } 502 | } 503 | ) 504 | } 505 | } 506 | drawTooltip(guiGraphics, x, y, formattedLines) 507 | } 508 | 509 | private fun refreshOverlay() { 510 | if (selectedInfo == null) { 511 | ClientCache.selectedPosition = null 512 | ClientCache.selectedFacing = null 513 | } else { 514 | ClientCache.selectedPosition = selectedInfo?.loc?.pos 515 | ClientCache.selectedFacing = selectedInfo?.loc?.facing 516 | } 517 | ClientCache.positions.clear() 518 | ClientCache.positions.addAll( 519 | infos.sorted 520 | .filter { 521 | it.frequency == selectedInfo?.frequency && 522 | it != selectedInfo && 523 | it.loc.dim == minecraft?.player?.level()?.dimension() 524 | } 525 | .filter { 526 | val d = 527 | minecraft?.player?.blockPosition()?.let { pos -> it.loc.pos.distSqr(pos) } 528 | (d?.compareTo(50.0) ?: 1) < 0 // Distance < 50 529 | } 530 | .map { it.loc.pos to it.loc.facing } 531 | .take(200) 532 | ) 533 | } 534 | 535 | fun openTypeSelector(parent: ITypeReceiver, useAny: Boolean) { 536 | typeSelector.parent = parent 537 | if (parent is WidgetP2PDevice) typeSelector.setPosition(parent.x + 20, parent.y) 538 | else typeSelector.setPosition(parent.x + parent.width, parent.y) 539 | typeSelector.useAny = useAny 540 | typeSelector.visible = true 541 | } 542 | 543 | override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { 544 | if (typeSelector.mouseClicked(mouseX, mouseY, button)) return true 545 | else closeTypeSelector(type) 546 | return super.mouseClicked(mouseX, mouseY, button) 547 | } 548 | 549 | fun closeTypeSelector(type: ClientTunnelInfo?) { 550 | if (this.type != type) { 551 | this.type = type 552 | PacketDistributor.sendToServer(C2SRefreshP2PList(type?.index ?: TUNNEL_ANY)) 553 | } 554 | 555 | typeSelector.visible = false 556 | } 557 | 558 | fun getTypeID(): Int { 559 | return 0 560 | } 561 | 562 | override fun getMenu(): AdvancedMemoryCardMenu = theMenu 563 | 564 | fun openTypeSelector(button: Button) { 565 | openTypeSelector(typeButton, true) 566 | } 567 | 568 | fun selectInfo(loc: P2PLocation?) { 569 | infos.select(loc) 570 | syncMemoryInfo() 571 | refreshOverlay() 572 | } 573 | } 574 | 575 | /** Format multiple lines of tooltips by the given max chars. */ 576 | fun fmtTooltips(title: String, vararg keys: String, maxChars: Int): List { 577 | val result: MutableList = mutableListOf() 578 | result.add(Component.translatable(title)) 579 | for (key in keys) { 580 | val words = I18n.get(key).split(' ') 581 | var i = 0 582 | if (key.length < maxChars) { 583 | result.add(Component.literal(key)) 584 | } 585 | while (i < words.size) { 586 | val s = StringBuilder() 587 | perWord@ while (s.length < maxChars) { 588 | s.append(words[i]) 589 | i += 1 590 | if (i >= words.size) break@perWord 591 | s.append(" ") 592 | } 593 | val c = Component.literal(s.toString()) 594 | result.add(if (!s.startsWith('§')) c.withStyle(ChatFormatting.GRAY) else c) 595 | } 596 | } 597 | return result 598 | } 599 | --------------------------------------------------------------------------------