├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── README_zh.md ├── TEMPLATE_LICENSE.txt ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── crafting.png ├── storage.png └── ui.png ├── settings.gradle └── src ├── frontend └── frontend │ ├── assets │ ├── index-C1o9Z9di.js │ └── index-EpFq6eZJ.css │ ├── favicon.png │ ├── index.html │ └── lang │ ├── all.json │ ├── en_ud.json │ ├── en_us.json │ └── zh_cn.json ├── generated └── resources │ ├── assets │ └── appwebterminal │ │ ├── blockstates │ │ └── web_terminal.json │ │ ├── lang │ │ ├── en_ud.json │ │ └── en_us.json │ │ └── models │ │ └── item │ │ └── web_terminal.json │ └── data │ ├── appwebterminal │ ├── advancements │ │ └── recipes │ │ │ └── misc │ │ │ ├── cable_web_terminal_from_block.json │ │ │ ├── cable_web_terminal_from_part.json │ │ │ └── web_terminal.json │ ├── loot_tables │ │ └── blocks │ │ │ └── web_terminal.json │ └── recipes │ │ ├── cable_web_terminal_from_block.json │ │ ├── cable_web_terminal_from_part.json │ │ └── web_terminal.json │ └── minecraft │ └── tags │ └── blocks │ ├── mineable │ └── pickaxe.json │ └── needs_stone_tool.json └── main ├── java └── icu │ └── takeneko │ └── appwebterminal │ ├── api │ └── ImageProvider.java │ ├── config │ ├── AppWebTerminalConfig.java │ └── MinecraftAssetsApi.java │ └── mixins │ ├── LevelRendererMixin.java │ └── RenderStateShardMixin.java ├── kotlin └── icu │ └── takeneko │ └── appwebterminal │ ├── AppWebTerminal.kt │ ├── all │ ├── BlockEntities.kt │ ├── Blocks.kt │ ├── Commands.kt │ ├── CreativeTabs.kt │ ├── Events.kt │ ├── Items.kt │ ├── KeyImageProviders.kt │ ├── Networking.kt │ └── Registries.kt │ ├── api │ ├── KeyImageProviderLoader.kt │ └── crafting │ │ ├── CraftingCpuContainer.kt │ │ ├── CraftingCpuContainerHelper.kt │ │ └── impl │ │ └── AECraftingCpuContainerImpl.kt │ ├── block │ ├── LateInitSupported.kt │ ├── WebTerminalBlock.kt │ ├── entity │ │ └── WebTerminalBlockEntity.kt │ └── part │ │ └── WebTerminalPart.kt │ ├── client │ ├── ClientEvents.kt │ ├── all │ │ └── Shaders.kt │ ├── gui │ │ └── WebTerminalScreen.kt │ └── rendering │ │ ├── AEKeyImageProvider.kt │ │ ├── AEKeyRenderer.kt │ │ ├── JProgressWindow.kt │ │ ├── RenderProgressListener.kt │ │ ├── foundation │ │ ├── BlurPostProcess.kt │ │ ├── FrameBuffer.kt │ │ ├── FullyBufferedBufferSource.kt │ │ ├── PostProcess.kt │ │ └── SamplingBlurPostProcess.kt │ │ └── providers │ │ ├── AEFluidKeyImageProvider.kt │ │ └── AEItemKeyImageProvider.kt │ ├── compat │ ├── advancedae │ │ └── AAECraftingCpuContainerImpl.kt │ ├── appbot │ │ └── ManaKeyImageProvider.kt │ ├── appflux │ │ └── FluxKeyImageProvider.kt │ ├── appmek │ │ └── ChemicalKeyImageProvider.kt │ └── arseng │ │ └── SourceKeyImageProvider.kt │ ├── data │ └── DataGenerator.kt │ ├── networking │ ├── OpenWebTerminalScreenPacket.kt │ └── UpdateWebTerminalNamePacket.kt │ ├── resource │ ├── CacheProvider.kt │ ├── LanguageFileDownloader.kt │ └── MinecraftVersion.kt │ ├── support │ ├── AENetworkAccess.kt │ ├── AENetworkSupport.kt │ ├── Data.kt │ ├── MECraftingServiceView.kt │ └── http │ │ ├── Application.kt │ │ ├── HttpServerLifecycleSupport.kt │ │ ├── plugins │ │ ├── HTTP.kt │ │ ├── Monitoring.kt │ │ ├── Security.kt │ │ ├── Serialization.kt │ │ └── Sockets.kt │ │ ├── routing │ │ ├── AEServiceRouting.kt │ │ ├── FrontendSupportRouting.kt │ │ ├── Routing.kt │ │ └── SecuritySupportRouting.kt │ │ └── websocket │ │ ├── Protocol.kt │ │ └── WebsocketSession.kt │ └── util │ ├── AEUtil.kt │ ├── CachedModFile.kt │ ├── CommandUtils.kt │ ├── FormattingUtil.kt │ ├── I18nUtil.kt │ ├── KRegistrate.kt │ ├── KubejsI18nSupport.kt │ ├── LanguageInstance.kt │ ├── MinecraftI18nSupport.kt │ ├── QueryJWTAuth.kt │ ├── RenderingUtil.kt │ ├── ResourceUtil.kt │ ├── SerializationUtils.kt │ ├── ServerI18nSupport.kt │ └── StateUtils.kt ├── resources ├── META-INF │ └── versions │ │ └── 8 │ │ └── DONT_DELETE_ME ├── appwebterminal.mixins.json ├── assets │ └── appwebterminal │ │ ├── ae2guide │ │ ├── structures │ │ │ └── web_terminal.snbt │ │ └── web_terminal.md │ │ ├── lang │ │ ├── ja_jp.json │ │ └── zh_cn.json │ │ ├── models │ │ ├── block │ │ │ ├── web_terminal.json │ │ │ └── web_terminal_offline.json │ │ ├── item │ │ │ └── cable_web_terminal.json │ │ └── part │ │ │ ├── web_terminal_off.json │ │ │ └── web_terminal_on.json │ │ ├── shaders │ │ └── core │ │ │ ├── blur.fsh │ │ │ ├── blur.json │ │ │ ├── blur.vsh │ │ │ ├── rendertype_textured_round_rect.fsh │ │ │ ├── rendertype_textured_round_rect.json │ │ │ └── rendertype_textured_round_rect.vsh │ │ └── textures │ │ ├── block │ │ ├── web_terminal_bottom.png │ │ ├── web_terminal_side.png │ │ ├── web_terminal_top.png │ │ ├── web_terminal_top.png.mcmeta │ │ └── web_terminal_top_offline.png │ │ └── gui │ │ └── blank.png └── icon.png └── templates ├── META-INF └── mods.toml └── pack.mcmeta /.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable autocrlf on generated files, they always generate with LF 2 | # Add any extra files or paths here to make git stop saying they 3 | # are changed when only line endings change. 4 | src/generated/**/.cache/cache text eol=lf 5 | src/generated/**/*.json text eol=lf 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | # from: https://github.com/Fallen-Breath/fabric-mod-template/tree/master/.github/ISSUE_TEMPLATE 2 | name: Bug Report 3 | description: Something doesn't seem correct and it might be a bug 4 | labels: ["🐛bug"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Bug description 10 | description: | 11 | A clear and concise description of what the bug is. 12 | Is it a game crash, an unexpected behavior, or has something gone wrong? 13 | If applicable, add screenshots to help explain the bug. 14 | placeholder: Tell us what you see! 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: to-reproduce 19 | attributes: 20 | label: Steps to reproduce 21 | description: Steps to reproduce the bug 22 | placeholder: | 23 | 1. Create a world 24 | 2. Wait until midnight 25 | 3. Hug a creeper 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: expected-behavior 30 | attributes: 31 | label: Expected behavior 32 | description: What did you expect to happen? 33 | placeholder: The creeper explodes 34 | - type: textarea 35 | id: actual-behavior 36 | attributes: 37 | label: Actual behavior 38 | description: What actually happened? 39 | placeholder: The creeper launches itself into the sky 40 | - type: textarea 41 | id: logs 42 | attributes: 43 | label: Relevant logs 44 | description: |- 45 | If it's a crash, send the corresponding Minecraft log in the `logs` folder, or crash report in the `crash-reports` folder, here. 46 | Please upload the log file as an attachment, or upload the log to [pastebin](https://pastebin.com/) / [mclo.gs](https://mclo.gs/) and paste the url here. 47 | Please refrain from pasting the entire log file directly. 48 | Leave empty if there is none. 49 | placeholder: https://pastebin.com/J6b7lKxR 50 | - type: input 51 | id: minecraft-version 52 | attributes: 53 | label: Minecraft version 54 | description: The Minecraft version(s) where this bug occurs in. 55 | placeholder: 1.20.1 56 | validations: 57 | required: true 58 | - type: input 59 | id: mod-version 60 | attributes: 61 | label: TemplateMod version 62 | description: The TemplateMod version(s) where this bug occurs in. 63 | placeholder: 1.1.0 64 | validations: 65 | required: true 66 | - type: textarea 67 | id: other-information 68 | attributes: 69 | label: Other information 70 | description: Other useful information to this bug report, e.g. other related mod version(s). Leave empty if there is none. 71 | placeholder: The issue only occurs if the player is in survival mode 72 | - type: checkboxes 73 | id: check-list 74 | attributes: 75 | label: Check list 76 | options: 77 | - label: I have verified that the issue persists in the latest version of the mod. 78 | required: true 79 | - label: I have searched the existing issues and confirmed that this is not a duplicate. 80 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | # from: https://github.com/Fallen-Breath/fabric-mod-template/tree/master/.github/ISSUE_TEMPLATE 2 | name: Feature Request 3 | description: Suggest an idea for this project 4 | labels: ["💡enhancement"] 5 | body: 6 | - type: textarea 7 | id: motivation 8 | attributes: 9 | label: Motivation 10 | description: Why do you want this feature? What problem do you want to solve? How can the suggested feature help with that? 11 | placeholder: Tell us what you want! 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: Description 18 | description: Describe the feature you want, as detailed as possible 19 | placeholder: I want to see a fancy starry sky when I look up at midnight. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: other-information 24 | attributes: 25 | label: Other information 26 | description: Other useful information to this feature request. Leave empty if there is none. -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI For AppliedWebTerminal 2 | on: 3 | push: 4 | paths: 5 | - .github/workflows/** 6 | - src/** 7 | - gradle/scripts/** 8 | - build.gradle 9 | - gradle.properties 10 | - settings.gradle 11 | branches: 12 | - main 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: write-all 18 | env: 19 | GITHUB_ACTION_NUMBER: ${{ vars.GITHUB_RUN_NUMBER }} 20 | steps: 21 | - name: checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Java 21 25 | uses: actions/setup-java@v4.2.1 26 | with: 27 | distribution: zulu 28 | java-version: 21 29 | 30 | - name: make gradle wrapper executable 31 | if: ${{ runner.os != 'Windows' }} 32 | run: chmod +x ./gradlew 33 | 34 | - name: Build with Gradle 35 | uses: gradle/actions/setup-gradle@v4.3.1 36 | 37 | - name: Build 38 | run: ./gradlew build 39 | 40 | - name: Read Properties 41 | id: 'properties' 42 | uses: christian-draeger/read-properties@1.1.1 43 | with: 44 | path: gradle.properties 45 | properties: 'mod_id mod_name' 46 | 47 | - name: Get Version 48 | id: var 49 | run: | 50 | MESSAGE=$(ls build/libs/* | grep sources.jar -v | grep dev.jar -v | grep slim.jar -v | awk -F '${{ steps.properties.outputs.mod_id }}-|.jar' '{print $2}') 51 | echo version=$MESSAGE >> $GITHUB_OUTPUT 52 | 53 | - name: Upload a Build Artifact 54 | uses: actions/upload-artifact@v4.3.3 55 | with: 56 | name: "${{ steps.properties.outputs.mod_name }} ${{ steps.var.outputs.version }}" 57 | path: | 58 | build/libs/${{ steps.properties.outputs.mod_id }}-${{ steps.var.outputs.version }}.jar 59 | build/libs/${{ steps.properties.outputs.mod_id }}-${{ steps.var.outputs.version }}-sources.jar 60 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Applied Web Terminal 2 | on: 3 | release: 4 | types: 5 | - published 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: write-all 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Java 21 16 | uses: actions/setup-java@v4.2.1 17 | with: 18 | distribution: zulu 19 | java-version: 21 20 | 21 | - name: make gradle wrapper executable 22 | if: ${{ runner.os != 'Windows' }} 23 | run: chmod +x ./gradlew 24 | 25 | - name: Build with Gradle 26 | uses: gradle/actions/setup-gradle@v4.3.1 27 | 28 | - name: Build 29 | run: ./gradlew build 30 | 31 | - name: Read Properties 32 | id: 'properties' 33 | uses: christian-draeger/read-properties@1.1.1 34 | with: 35 | path: gradle.properties 36 | properties: 'mod_id mod_name' 37 | 38 | - name: Get Version 39 | id: var 40 | run: | 41 | MESSAGE=$(ls build/libs/* | grep sources.jar -v | grep dev.jar -v | grep slim.jar -v | awk -F '${{ steps.properties.outputs.mod_id }}-|.jar' '{print $2}') 42 | echo version=$MESSAGE >> $GITHUB_OUTPUT 43 | 44 | - name: "publish mc mod" 45 | uses: Kir-Antipov/mc-publish@v3.3.0 46 | with: 47 | name: "${{ steps.properties.outputs.mod_name }} v${{ steps.var.outputs.version }}" 48 | version: ${{ steps.var.outputs.version }} 49 | version-type: release 50 | 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | modrinth-token: ${{ secrets.MODRINTH_TOKEN }} 53 | curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} 54 | 55 | files: | 56 | build/libs/${{ steps.properties.outputs.mod_id }}-${{ steps.var.outputs.version }}.jar 57 | build/libs/${{ steps.properties.outputs.mod_id }}-${{ steps.var.outputs.version }}-dev.jar 58 | build/libs/${{ steps.properties.outputs.mod_id }}-${{ steps.var.outputs.version }}-sources.jar 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | 16 | # gradle 17 | build 18 | .gradle 19 | 20 | # other 21 | eclipse 22 | run 23 | runs 24 | run-data 25 | 26 | repo 27 | .idea/ 28 | .kotlin/ 29 | .cache/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend"] 2 | path = frontend 3 | url = https://github.com/MercuryGryph/AppliedWebTerminal-WebPanel 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Applied Web Terminal 2 | 3 |
4 | 5 | curseforge 6 | 7 | 8 | modrinth 9 | 10 | forge 11 | fabric 12 |
13 | 14 | English | [简体中文](README_zh.md) 15 | 16 | Move your AE terminal to the web! 17 | 18 | 19 | * [Applied Web Terminal](#applied-web-terminal) 20 | * [Dependencies](#dependencies) 21 | * [Usage](#usage) 22 | * [Images](#images) 23 | * [Storage Page](#storage-page) 24 | * [Crafting Status Page](#crafting-status-page) 25 | * [In-Game GUI](#in-game-gui) 26 | * [Configs](#configs) 27 | 28 | 29 | ## Dependencies 30 | 31 | - [AE2](https://modrinth.com/mod/ae2) 32 | - [Kotlin For Forge](https://modrinth.com/mod/kotlin-for-forge) 33 | - [Configuration](https://modrinth.com/mod/configuration) 34 | 35 | ## Usage 36 | 37 | - Download the latest version of this mod from [Releases](https://github.com/ZhuRuoLing/AppliedWebTerminal/releases) and install the dependencies 38 | - If you are playing in single-player mode: 39 | - Enter your single-player world 40 | - Run the command `/appwebterminal resources render` to generate frontend resources (only needs to be run once) 41 | - If you are playing on a server: 42 | - Create a new single-player world 43 | - Run the command `/appwebterminal resources render` to generate frontend resources (only needs to be run once) 44 | - Upload the `aeKeyResources` folder from your game root directory to your server 45 | - Connect the `ME Web Terminal` to the AE network 46 | - Right-click to open the GUI and configure the name and password 47 | - Open your web browser, access the network terminal address (default port `11451`), and log in with the password 48 | - All done! 49 | 50 | ## Images 51 |
52 | 53 | Images 54 | 55 | 56 | ### Storage Page 57 | 58 | Storage Page 59 | 60 | ### Crafting Status Page 61 | Crafting Page 62 | 63 | ### In-Game GUI 64 | In-Game GUI 65 | 66 |
67 | 68 | ## Configs 69 | 70 | Config file is `.minecraft/config/appwebterminal.yaml` 71 | 72 |
73 | 74 | Details 75 | 76 | 77 | > Http Server port for ME Web Terminal. 78 | 79 | `httpPort`: `11451` 80 | - - - 81 | > Web Terminal Title 82 | 83 | `frontendTitle`: `Applied Web Terminal` 84 | - - - 85 | > `Websocket url` for frontend to connect 86 | > 87 | > Use `~` for auto detecting at frontend (use the same host of website) 88 | > 89 | > Example: `ws://example.com/` 90 | 91 | `backendWebsocketEndpoint`: `~` 92 | - - - 93 | > Add PinIn support to languages 94 | 95 | `needPinInLanguage`: 96 | 97 |   `- zh_cn` 98 | 99 |   `- zh_tw` 100 | 101 |   `- zh_hk` 102 | - - - 103 |
104 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Applied Web Terminal 2 | 3 | [English](README.md) | 简体中文 4 | 5 | 把你的AE终端搬到网页上! 6 | 7 | 8 | * [Applied Web Terminal](#applied-web-terminal) 9 | * [依赖](#依赖) 10 | * [使用](#使用) 11 | * [图片](#图片) 12 | * [存储页面](#存储页面) 13 | * [合成状态页面](#合成状态页面) 14 | * [游戏内GUI](#游戏内gui) 15 | * [配置](#配置) 16 | 17 | 18 | ## 依赖 19 | 20 | - [AE2](https://modrinth.com/mod/ae2) 21 | - [Kotlin For Forge](https://modrinth.com/mod/kotlin-for-forge) 22 | - [Configuration](https://modrinth.com/mod/configuration) 23 | 24 | ## 使用 25 | 26 | - 在[Releases](https://github.com/ZhuRuoLing/AppliedWebTerminal/releases)下载本模组的最新版本 并安装依赖 27 | - 若阁下在游玩单人世界 28 | - 进入您的单人世界 29 | - 运行 `/appwebterminal resources render` 命令以生成前端资源(仅需运行一次) 30 | - 若阁下在游玩服务器 31 | - 新建一个单人世界 32 | - 运行 `/appwebterminal resources render` 命令以生成前端资源(仅需运行一次) 33 | - 将游戏根目录下的 `aeKeyResources` 文件夹上传至您的服务端 34 | - 将 `ME 网络终端` 接入AE网络 35 | - 右键打开gui,并配置名字及密码 36 | - 打开网页浏览器,访问网络终端地址(默认端口`11451`),并输入密码登录 37 | - 大功告成! 38 | 39 | ## 图片 40 |
41 | 42 | 图片 43 | 44 | 45 | ### 存储页面 46 | 47 | Storage Page 48 | 49 | ### 合成状态页面 50 | Crafting Page 51 | 52 | ### 游戏内GUI 53 | In-Game ui 54 | 55 |
56 | 57 | ## 配置 58 | 59 | 配置文件位于`.minecraft/config/appwebterminal.yaml` 60 | 61 |
62 | 63 | 详细配置 64 | 65 | 66 | > ME Web Terminal 的 Http服务器端口 67 | 68 | `httpPort`: `11451` 69 | - - - 70 | > 前端网页标题 71 | 72 | `frontendTitle`: `Applied Web Terminal` 73 | - - - 74 | > 前端连接的 `Websocket url` 75 | > 76 | > 填入 `~` 则前端自动判断(使用和网页相同的host) 77 | > 78 | > 示例:`ws://example.com/` 79 | 80 | `backendWebsocketEndpoint`: `~` 81 | - - - 82 | > 使用拼音搜索的语言 83 | 84 | `needPinInLanguage`: 85 | 86 |   `- zh_cn` 87 | 88 |   `- zh_tw` 89 | 90 |   `- zh_hk` 91 | - - - 92 |
93 | -------------------------------------------------------------------------------- /TEMPLATE_LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NeoForged project 4 | 5 | This license applies to the template files as supplied by github.com/NeoForged/MDK 6 | 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Sets default memory used for gradle commands. Can be overridden by user or command line properties. 2 | org.gradle.jvmargs=-Xmx1G 3 | org.gradle.daemon=true 4 | org.gradle.parallel=true 5 | org.gradle.caching=true 6 | org.gradle.configuration-cache=true 7 | 8 | #read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment 9 | # you can also find the latest versions at: https://parchmentmc.org/docs/getting-started 10 | parchment_minecraft_version=1.20.1 11 | parchment_mappings_version=2023.09.03 12 | # Environment Properties 13 | # You can find the latest versions here: https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html 14 | # The Minecraft version must agree with the Forge version to get a valid artifact 15 | minecraft_version=1.20.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.20.1 20 | # The Forge version must agree with the Minecraft version to get a valid artifact 21 | forge_version=47.2.0 22 | # The Forge version range can use any version of Forge as bounds 23 | forge_version_range=[47.1.3,) 24 | # The loader version range can only use the major version of FML as bounds 25 | loader_version_range=[4.11,) 26 | 27 | ## Mod Properties 28 | 29 | # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} 30 | # Must match the String constant located in the main mod class annotated with @Mod. 31 | mod_id=appwebterminal 32 | # The human-readable display name for the mod. 33 | mod_name=Applied Web Terminal 34 | # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. 35 | mod_license=MIT License 36 | # The mod version. See https://semver.org/ 37 | mod_version=1.2.3 38 | # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. 39 | # This should match the base package used for the mod sources. 40 | # See https://maven.apache.org/guides/mini/guide-naming-conventions.html 41 | mod_group_id=icu.takeneko.appwebterminal 42 | # The authors of the mod. This is a simple text string that is used for display purposes in the mod list. 43 | mod_authors=ZhuRuoLing, DancingSnow0517, MercuryGryph 44 | # The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. 45 | mod_description=Web Terminal for AppEng2 46 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | registrate = "MC1.20-1.3.11" 3 | jei = "15.20.0.105" 4 | ae2 = "15.3.4" 5 | kotlin-version = "2.1.20" 6 | ktor-version = "3.1.2" 7 | 8 | [libraries] 9 | registrate = { module = "com.tterrag.registrate:Registrate", version.ref = "registrate" } 10 | jei-common-api = { module = "mezz.jei:jei-1.20.1-common-api", version.ref = "jei" } 11 | jei-forge-api = { module = "mezz.jei:jei-1.20.1-forge-api", version.ref = "jei" } 12 | jei-forge-impl = { module = "mezz.jei:jei-1.20.1-forge", version.ref = "jei" } 13 | ae2 = { module = "appeng:appliedenergistics2-forge", version.ref = "ae2" } 14 | ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor-version" } 15 | ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor-version" } 16 | ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor-version" } 17 | ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor-version" } 18 | ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor-version" } 19 | ktor-server-forwarded-header = { module = "io.ktor:ktor-server-forwarded-header", version.ref = "ktor-version" } 20 | ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor-version" } 21 | ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers", version.ref = "ktor-version" } 22 | ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor-version" } 23 | ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor-version" } 24 | ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor-version" } 25 | ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor-version" } 26 | 27 | [plugins] 28 | ktor = { id = "io.ktor.plugin", version.ref = "ktor-version" } 29 | 30 | [bundles] 31 | jei = ["jei-common-api", "jei-forge-api", "jei-forge-impl"] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /images/crafting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/images/crafting.png -------------------------------------------------------------------------------- /images/storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/images/storage.png -------------------------------------------------------------------------------- /images/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/images/ui.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' 3 | } 4 | 5 | include "frontend" -------------------------------------------------------------------------------- /src/frontend/frontend/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/frontend/frontend/favicon.png -------------------------------------------------------------------------------- /src/frontend/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Applied Web Terminal 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /src/frontend/frontend/lang/all.json: -------------------------------------------------------------------------------- 1 | [ 2 | "en_us.json", 3 | "zh_cn.json", 4 | "en_ud.json" 5 | ] -------------------------------------------------------------------------------- /src/frontend/frontend/lang/en_ud.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang.name": "(uʍoᗡ ǝpısd∩)ɥsıןbuƎ", 3 | "common.button.cancel": "ןǝɔuɐƆ", 4 | "common.button.confirm": "ɯɹıɟuoƆ", 5 | "main.terminals.title": "ןɐuıɯɹǝ⟘ Ɐ ʇɔǝןǝS", 6 | "main.terminals.login.title": "ןɐuıɯɹǝ⟘ uıboꞀ", 7 | "main.terminals.login.label.terminal_name": ":ǝɯɐN ןɐuıɯɹǝ⟘", 8 | "main.terminals.login.button.login": "uıboꞀ", 9 | "main.terminals.login.placeholder.password": "pɹoʍssɐԀ", 10 | "main.terminals.login.button.remember_password": "pɹoʍssɐԀ ɹǝqɯǝɯǝᴚ", 11 | "language.selector.title": "ǝbɐnbuɐꞀ", 12 | "language.selector.button.add": "ppⱯ", 13 | "language.add.title": "ǝbɐnbuɐꞀ ppⱯ", 14 | "language.add.placeholder.lang": "ᗡI ǝbɐnbuɐꞀ", 15 | "language.add.label.test": "ʇsǝ⟘", 16 | "language.add.button.test": "ʇsǝ⟘", 17 | "language.add.button.add": "ppⱯ", 18 | "language.add.placeholder.test_key": "(ʎǝʞ)", 19 | "language.add.placeholder.result": "(ʇןnsǝɹ)", 20 | "terminal.button.crafting_page": "snʇɐʇS buıʇɟɐɹƆ", 21 | "terminal.button.storage_page": "ǝbɐɹoʇS", 22 | "terminal.input.search": "˙˙˙ɥɔɹɐǝS", 23 | "ae.crafting.status.missing": "%s :buıssıW", 24 | "ae.crafting.status.available": "%s :ǝןqɐןıɐʌⱯ", 25 | "ae.crafting.status.to_craft": "%s :ʇɟɐɹƆ o⟘", 26 | "ae.crafting.status.crafting": "%s :buıʇɟɐɹƆ", 27 | "ae.crafting.status.scheduled": "%s :pǝןnpǝɥɔS", 28 | "ae.tooltip.stored": "%s :pǝɹoʇS", 29 | "ae.tooltip.craftable": "ǝןqɐʇɟɐɹƆ", 30 | "ae.tooltip.craft": "ʇɟɐɹƆ", 31 | "ae.crafting.cpu": "%d# ∩ԀƆ", 32 | "ae.crafting.status.stored": "%s :pǝɹoʇS", 33 | "ae.crafting.status.active": "%s :buıʇɟɐɹƆ", 34 | "ae.crafting.status.pending": "%s :pǝןnpǝɥɔS", 35 | "websocket.connection.error": "pǝןıɐℲ uoıʇɔǝuuoƆ", 36 | "websocket.connection.established": "pǝɥsıןqɐʇsƎ uoıʇɔǝuuoƆ", 37 | "websocket.connection.disconnected": "ɹǝʌɹǝs ɯoɹɟ pǝʇɔǝuuoɔsıᗡ", 38 | "ae.cpu.storage": "ǝbɐɹoʇS ", 39 | "ae.cpu.coprocessors": "sɹossǝɔoɹԀ-oƆ ", 40 | "ae.cpu.crafting": " buıʇɟɐɹƆ", 41 | "ae.cpu.crafting_progress": " pǝʇɟɐɹƆ", 42 | "ae.cpu.crafting_progress.in": " uı ", 43 | "ae.crafting.plan.title": "uɐןԀ buıʇɟɐɹƆ", 44 | "ae.crafting.plan.title_with_bytes": "pǝs∩ sǝʇʎᗺ %s - uɐןԀ buıʇɟɐɹƆ", 45 | "ae.crafting.plan.calculating": "˙˙˙ʇıɐM ǝsɐǝןԀ buıʇɐןnɔןɐƆ", 46 | "ae.crafting.plan.cancel": "ןǝɔuɐƆ", 47 | "ae.crafting.plan.start": "ʇɹɐʇS", 48 | "ae.crafting.plan.select_amount": "ʇunoɯⱯ ʇɔǝןǝS", 49 | "ae.crafting.plan.next": "ʇxǝN", 50 | "ae.crafting.plan.submit_successes": "ʎןןnɟssǝɔɔnS ʇıɯqnS uɐןԀ ʇɟɐɹƆ", 51 | "ae.crafting.plan.submit_failure": "ǝɹnןıɐℲ ʇıɯqnS uɐןԀ ʇɟɐɹƆ", 52 | "ae.crafting.plan.submit_failed_by": "%s ʎq pǝןıɐɟ ʇıɯqnS uɐןԀ ʇɟɐɹƆ", 53 | "settings.title": "sbuıʇʇǝS", 54 | "settings.item.terminal": "ןɐuıɯɹǝ⟘", 55 | "settings.item.terminal.refresh_interval": "(sʞɔıʇ) ןɐʌɹǝʇuI ɥsǝɹɟǝᴚ", 56 | "settings.item.status": "snʇɐʇS", 57 | "settings.item.language": "ǝbɐnbuɐꞀ", 58 | "settings.item.language.configure": "ǝɹnbıɟuoƆ", 59 | "settings.item.terminal.sorting": "buıʇɹoS", 60 | "settings.item.terminal.order": "ɹǝpɹO", 61 | "settings.item.terminal.sorting.by_name": "ǝɯɐN ɯǝʇI", 62 | "settings.item.terminal.sorting.by_count": "sɯǝʇı ɟo ɹǝqɯnN", 63 | "settings.item.terminal.sorting.by_mod": "poW", 64 | "settings.item.terminal.sorting.ascending": "buıpuǝɔsⱯ", 65 | "settings.item.terminal.sorting.descending": "buıpuǝɔsǝᗡ", 66 | "ae.crafting.sidebar.open": "ɹɐqǝpıS uǝdO", 67 | "ae.crafting.sidebar.close": "ɹɐqǝpıS ǝsdɐןןoƆ" 68 | } -------------------------------------------------------------------------------- /src/frontend/frontend/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang.name": "English (US)", 3 | "common.button.cancel": "Cancel", 4 | "common.button.confirm": "Confirm", 5 | "main.terminals.title": "Select A Terminal", 6 | "main.terminals.login.title": "Login Terminal", 7 | "main.terminals.login.label.terminal_name": "Terminal Name:", 8 | "main.terminals.login.button.login": "Login", 9 | "main.terminals.login.placeholder.password": "Password", 10 | "main.terminals.login.button.remember_password": "Remember Password", 11 | "language.selector.title": "Language", 12 | "language.selector.button.add": "Add", 13 | "language.add.title": "Add Language", 14 | "language.add.placeholder.lang": "Language ID", 15 | "language.add.label.test": "Test", 16 | "language.add.button.test": "Test", 17 | "language.add.button.add": "Add", 18 | "language.add.placeholder.test_key": "(key)", 19 | "language.add.placeholder.result": "(result)", 20 | "terminal.button.crafting_page": "Crafting Status", 21 | "terminal.button.storage_page": "Storage", 22 | "terminal.input.search": "Search...", 23 | "ae.crafting.status.missing": "Missing: %s", 24 | "ae.crafting.status.available": "Available: %s", 25 | "ae.crafting.status.to_craft": "To Craft: %s", 26 | "ae.crafting.status.crafting": "Crafting: %s", 27 | "ae.crafting.status.scheduled": "Scheduled: %s", 28 | "ae.tooltip.stored": "Stored: %s", 29 | "ae.tooltip.craftable": "Craftable", 30 | "ae.tooltip.craft": "Craft", 31 | "ae.crafting.cpu": "CPU #%d", 32 | "ae.crafting.status.stored": "Stored: %s", 33 | "ae.crafting.status.active": "Crafting: %s", 34 | "ae.crafting.status.pending": "Scheduled: %s", 35 | "websocket.connection.error": "Connection Failed", 36 | "websocket.connection.established": "Connection Established", 37 | "websocket.connection.disconnected": "Disconnected from server", 38 | "ae.cpu.storage": " Storage", 39 | "ae.cpu.coprocessors": " Co-Processors", 40 | "ae.cpu.crafting": "Crafting ", 41 | "ae.cpu.crafting_progress": "Crafted ", 42 | "ae.cpu.crafting_progress.in": " in ", 43 | "ae.crafting.plan.title": "Crafting Plan", 44 | "ae.crafting.plan.title_with_bytes": "Crafting Plan - %s Bytes Used", 45 | "ae.crafting.plan.calculating": "Calculating Please Wait...", 46 | "ae.crafting.plan.cancel": "Cancel", 47 | "ae.crafting.plan.start": "Start", 48 | "ae.crafting.plan.select_amount": "Select Amount", 49 | "ae.crafting.plan.next": "Next", 50 | "ae.crafting.plan.submit_successes": "Craft Plan Submit Successfully", 51 | "ae.crafting.plan.submit_failure": "Craft Plan Submit Failure", 52 | "ae.crafting.plan.submit_failed_by": "Craft Plan Submit failed by %s", 53 | "settings.title": "Settings", 54 | "settings.item.terminal": "Terminal", 55 | "settings.item.terminal.refresh_interval": "Refresh Interval (ticks)", 56 | "settings.item.status": "Status", 57 | "settings.item.language": "Language", 58 | "settings.item.language.configure": "Configure", 59 | "settings.item.terminal.sorting": "Sorting", 60 | "settings.item.terminal.order": "Order", 61 | "settings.item.terminal.sorting.by_name":"Item Name", 62 | "settings.item.terminal.sorting.by_count":"Number of items", 63 | "settings.item.terminal.sorting.by_mod":"Mod", 64 | "settings.item.terminal.sorting.ascending": "Ascending", 65 | "settings.item.terminal.sorting.descending": "Descending", 66 | "ae.crafting.sidebar.open": "Open Sidebar", 67 | "ae.crafting.sidebar.close": "Collapse Sidebar" 68 | } -------------------------------------------------------------------------------- /src/frontend/frontend/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang.name": "简体中文 (中国大陆)", 3 | "common.button.cancel": "取消", 4 | "common.button.confirm": "确认", 5 | "main.terminals.title": "选择终端", 6 | "main.terminals.login.title": "登录终端", 7 | "main.terminals.login.label.terminal_name": "终端:", 8 | "main.terminals.login.button.login": "登录", 9 | "main.terminals.login.placeholder.password": "密码", 10 | "main.terminals.login.button.remember_password": "记住密码", 11 | "language.selector.title": "语言", 12 | "language.selector.button.add": "添加", 13 | "language.add.title": "添加语言", 14 | "language.add.placeholder.lang": "语言 ID", 15 | "language.add.label.test": "测试", 16 | "language.add.button.test": "测试", 17 | "language.add.button.add": "添加", 18 | "language.add.placeholder.test_key": "(key)", 19 | "language.add.placeholder.result": "(结果)", 20 | "terminal.button.crafting_page": "合成状态", 21 | "terminal.button.storage_page": "存储", 22 | "terminal.input.search": "搜索...", 23 | "ae.crafting.status.missing": "缺少数量:%s", 24 | "ae.crafting.status.available": "可用数量:%s", 25 | "ae.crafting.status.to_craft": "合成数量:%s", 26 | "ae.crafting.status.crafting": "正在合成:%s", 27 | "ae.crafting.status.scheduled": "计划合成:%s", 28 | "ae.tooltip.stored": "已存储:%s", 29 | "ae.tooltip.craftable": "可合成", 30 | "ae.tooltip.craft": "合成", 31 | "ae.crafting.status.stored": "可用数量: %s", 32 | "ae.crafting.status.active": "正在合成: %s", 33 | "ae.crafting.status.pending": "计划合成: %s", 34 | "websocket.connection.error": "连接失败", 35 | "websocket.connection.established": "已连接", 36 | "websocket.connection.disconnected": "连接已断开", 37 | "ae.cpu.storage": " 存储容量", 38 | "ae.cpu.coprocessors": " 并行处理单元", 39 | "ae.cpu.crafting": "正在合成", 40 | "ae.cpu.crafting_progress": "已合成 ", 41 | "ae.crafting.cpu": "处理器 #%d", 42 | "ae.cpu.crafting_progress.in": ",用时 ", 43 | "ae.crafting.plan.title": "合成计划", 44 | "ae.crafting.plan.title_with_bytes": "合成计划 - %s 字节已使用", 45 | "ae.crafting.plan.calculating": "计算中,请稍后...", 46 | "ae.crafting.plan.cancel": "取消", 47 | "ae.crafting.plan.start": "开始", 48 | "ae.crafting.plan.select_amount": "选择数量", 49 | "ae.crafting.plan.next": "下一步", 50 | "ae.crafting.plan.submit_successes": "合成计划提交成功", 51 | "ae.crafting.plan.submit_failure": "合成计划提交失败", 52 | "ae.crafting.plan.submit_failed_by": "由于 %s,合成计划提交失败", 53 | "settings.title": "设置", 54 | "settings.item.terminal": "终端", 55 | "settings.item.terminal.refresh_interval": "更新间隔", 56 | "settings.item.status": "状态", 57 | "settings.item.language": "语言", 58 | "settings.item.language.configure": "配置", 59 | "ae.crafting.sidebar.open": "展开侧边栏", 60 | "ae.crafting.sidebar.close": "折叠侧边栏", 61 | "settings.item.terminal.sorting": "排序", 62 | "settings.item.terminal.sorting.ascending": "增序", 63 | "settings.item.terminal.sorting.descending": "降序", 64 | "settings.item.terminal.order": "排序顺序", 65 | "settings.item.terminal.sorting.by_name": "物品名称", 66 | "settings.item.terminal.sorting.by_count": "物品数量", 67 | "settings.item.terminal.sorting.by_mod": "模组" 68 | } -------------------------------------------------------------------------------- /src/generated/resources/assets/appwebterminal/blockstates/web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "variants": { 3 | "facing=down,online=false": { 4 | "model": "appwebterminal:block/web_terminal_offline", 5 | "x": 180 6 | }, 7 | "facing=down,online=true": { 8 | "model": "appwebterminal:block/web_terminal", 9 | "x": 180 10 | }, 11 | "facing=east,online=false": { 12 | "model": "appwebterminal:block/web_terminal_offline", 13 | "x": 90, 14 | "y": 90 15 | }, 16 | "facing=east,online=true": { 17 | "model": "appwebterminal:block/web_terminal", 18 | "x": 90, 19 | "y": 90 20 | }, 21 | "facing=north,online=false": { 22 | "model": "appwebterminal:block/web_terminal_offline", 23 | "x": 90 24 | }, 25 | "facing=north,online=true": { 26 | "model": "appwebterminal:block/web_terminal", 27 | "x": 90 28 | }, 29 | "facing=south,online=false": { 30 | "model": "appwebterminal:block/web_terminal_offline", 31 | "x": 90, 32 | "y": 180 33 | }, 34 | "facing=south,online=true": { 35 | "model": "appwebterminal:block/web_terminal", 36 | "x": 90, 37 | "y": 180 38 | }, 39 | "facing=up,online=false": { 40 | "model": "appwebterminal:block/web_terminal_offline" 41 | }, 42 | "facing=up,online=true": { 43 | "model": "appwebterminal:block/web_terminal" 44 | }, 45 | "facing=west,online=false": { 46 | "model": "appwebterminal:block/web_terminal_offline", 47 | "x": 90, 48 | "y": 270 49 | }, 50 | "facing=west,online=true": { 51 | "model": "appwebterminal:block/web_terminal", 52 | "x": 90, 53 | "y": 270 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/generated/resources/assets/appwebterminal/lang/en_ud.json: -------------------------------------------------------------------------------- 1 | { 2 | "appwebterminal.button.done": "ǝuoᗡ", 3 | "appwebterminal.gui.me_network_offline": "ǝuıןɟɟO", 4 | "appwebterminal.gui.me_network_online": "ǝuıןuO", 5 | "appwebterminal.hint.name": " :ǝɯɐN", 6 | "appwebterminal.hint.password": " :pɹoʍssɐԀ", 7 | "appwebterminal.message.render_complete": "˙ǝʇǝןdɯoɔ ɹǝpuǝᴚ", 8 | "appwebterminal.message.rendering": "˙buıʞɹoʍ ʎןʇuǝɹɹnɔ sı ɹǝɹǝpuǝɹ Ɐ", 9 | "appwebterminal.message.started": "˙ɹǝɹǝpuǝɹ pǝʇɹɐʇS", 10 | "appwebterminal.screen.title": "ןɐuıɯɹǝ⟘ qǝM ƎW", 11 | "block.appwebterminal.web_terminal": "ןɐuıɯɹǝ⟘ qǝM ƎW", 12 | "config.appwebterminal.option.assetsSource": "ǝɔɹnoS sʇǝssⱯ", 13 | "config.appwebterminal.option.backendWebsocketEndpoint": "ʇuıodpuƎ ʇǝʞɔosqǝM puǝʞɔɐᗺ", 14 | "config.appwebterminal.option.frontendTitle": "ǝןʇı⟘ puǝʇuoɹℲ", 15 | "config.appwebterminal.option.httpPort": "ʇɹoԀ dʇʇH", 16 | "config.appwebterminal.option.needPinInLanguage": "ǝbɐnbuɐꞀ uI uıԀ pǝǝN", 17 | "config.screen.appwebterminal": "sbuıʇʇǝS ןɐuıɯɹǝ⟘qǝMpǝıןddⱯ", 18 | "item.appwebterminal.cable_web_terminal": "ןɐuıɯɹǝ⟘ qǝM ƎW", 19 | "itemGroup.appwebterminal.applied_web_terminal": "ןɐuıɯɹǝ⟘ qǝM pǝıןddⱯ" 20 | } -------------------------------------------------------------------------------- /src/generated/resources/assets/appwebterminal/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "appwebterminal.button.done": "Done", 3 | "appwebterminal.gui.me_network_offline": "Offline", 4 | "appwebterminal.gui.me_network_online": "Online", 5 | "appwebterminal.hint.name": "Name: ", 6 | "appwebterminal.hint.password": "Password: ", 7 | "appwebterminal.message.render_complete": "Render complete.", 8 | "appwebterminal.message.rendering": "A renderer is currently working.", 9 | "appwebterminal.message.started": "Started renderer.", 10 | "appwebterminal.screen.title": "ME Web Terminal", 11 | "block.appwebterminal.web_terminal": "ME Web Terminal", 12 | "config.appwebterminal.option.assetsSource": "Assets Source", 13 | "config.appwebterminal.option.backendWebsocketEndpoint": "Backend Websocket Endpoint", 14 | "config.appwebterminal.option.frontendTitle": "Frontend Title", 15 | "config.appwebterminal.option.httpPort": "Http Port", 16 | "config.appwebterminal.option.needPinInLanguage": "Need Pin In Language", 17 | "config.screen.appwebterminal": "AppliedWebTerminal Settings", 18 | "item.appwebterminal.cable_web_terminal": "ME Web Terminal", 19 | "itemGroup.appwebterminal.applied_web_terminal": "Applied Web Terminal" 20 | } -------------------------------------------------------------------------------- /src/generated/resources/assets/appwebterminal/models/item/web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "appwebterminal:block/web_terminal" 3 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/advancements/recipes/misc/cable_web_terminal_from_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:recipes/root", 3 | "criteria": { 4 | "has_item": { 5 | "conditions": { 6 | "items": [ 7 | { 8 | "items": [ 9 | "appwebterminal:web_terminal" 10 | ] 11 | } 12 | ] 13 | }, 14 | "trigger": "minecraft:inventory_changed" 15 | }, 16 | "has_the_recipe": { 17 | "conditions": { 18 | "recipe": "appwebterminal:cable_web_terminal_from_block" 19 | }, 20 | "trigger": "minecraft:recipe_unlocked" 21 | } 22 | }, 23 | "requirements": [ 24 | [ 25 | "has_item", 26 | "has_the_recipe" 27 | ] 28 | ], 29 | "rewards": { 30 | "recipes": [ 31 | "appwebterminal:cable_web_terminal_from_block" 32 | ] 33 | }, 34 | "sends_telemetry_event": false 35 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/advancements/recipes/misc/cable_web_terminal_from_part.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:recipes/root", 3 | "criteria": { 4 | "has_item": { 5 | "conditions": { 6 | "items": [ 7 | { 8 | "items": [ 9 | "appwebterminal:cable_web_terminal" 10 | ] 11 | } 12 | ] 13 | }, 14 | "trigger": "minecraft:inventory_changed" 15 | }, 16 | "has_the_recipe": { 17 | "conditions": { 18 | "recipe": "appwebterminal:cable_web_terminal_from_part" 19 | }, 20 | "trigger": "minecraft:recipe_unlocked" 21 | } 22 | }, 23 | "requirements": [ 24 | [ 25 | "has_item", 26 | "has_the_recipe" 27 | ] 28 | ], 29 | "rewards": { 30 | "recipes": [ 31 | "appwebterminal:cable_web_terminal_from_part" 32 | ] 33 | }, 34 | "sends_telemetry_event": false 35 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/advancements/recipes/misc/web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:recipes/root", 3 | "criteria": { 4 | "has_item": { 5 | "conditions": { 6 | "items": [ 7 | { 8 | "items": [ 9 | "ae2:wireless_access_point" 10 | ] 11 | } 12 | ] 13 | }, 14 | "trigger": "minecraft:inventory_changed" 15 | }, 16 | "has_the_recipe": { 17 | "conditions": { 18 | "recipe": "appwebterminal:web_terminal" 19 | }, 20 | "trigger": "minecraft:recipe_unlocked" 21 | } 22 | }, 23 | "requirements": [ 24 | [ 25 | "has_item", 26 | "has_the_recipe" 27 | ] 28 | ], 29 | "rewards": { 30 | "recipes": [ 31 | "appwebterminal:web_terminal" 32 | ] 33 | }, 34 | "sends_telemetry_event": false 35 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/loot_tables/blocks/web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minecraft:block", 3 | "pools": [ 4 | { 5 | "bonus_rolls": 0.0, 6 | "conditions": [ 7 | { 8 | "condition": "minecraft:survives_explosion" 9 | } 10 | ], 11 | "entries": [ 12 | { 13 | "type": "minecraft:item", 14 | "name": "appwebterminal:web_terminal" 15 | } 16 | ], 17 | "rolls": 1.0 18 | } 19 | ], 20 | "random_sequence": "appwebterminal:blocks/web_terminal" 21 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/recipes/cable_web_terminal_from_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minecraft:crafting_shapeless", 3 | "category": "misc", 4 | "ingredients": [ 5 | { 6 | "item": "appwebterminal:web_terminal" 7 | } 8 | ], 9 | "result": { 10 | "item": "appwebterminal:cable_web_terminal" 11 | } 12 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/recipes/cable_web_terminal_from_part.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minecraft:crafting_shapeless", 3 | "category": "misc", 4 | "ingredients": [ 5 | { 6 | "item": "appwebterminal:cable_web_terminal" 7 | } 8 | ], 9 | "result": { 10 | "item": "appwebterminal:web_terminal" 11 | } 12 | } -------------------------------------------------------------------------------- /src/generated/resources/data/appwebterminal/recipes/web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minecraft:crafting_shaped", 3 | "category": "misc", 4 | "key": { 5 | "A": { 6 | "item": "ae2:charged_certus_quartz_crystal" 7 | }, 8 | "B": { 9 | "item": "ae2:wireless_access_point" 10 | }, 11 | "C": { 12 | "item": "ae2:engineering_processor" 13 | }, 14 | "D": { 15 | "item": "ae2:crafting_card" 16 | }, 17 | "E": { 18 | "item": "ae2:quartz_vibrant_glass" 19 | }, 20 | "F": { 21 | "item": "ae2:crafting_monitor" 22 | } 23 | }, 24 | "pattern": [ 25 | "ABA", 26 | "CDC", 27 | "EFE" 28 | ], 29 | "result": { 30 | "item": "appwebterminal:web_terminal" 31 | }, 32 | "show_notification": true 33 | } -------------------------------------------------------------------------------- /src/generated/resources/data/minecraft/tags/blocks/mineable/pickaxe.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [ 3 | "appwebterminal:web_terminal" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/generated/resources/data/minecraft/tags/blocks/needs_stone_tool.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [ 3 | "appwebterminal:web_terminal" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/main/java/icu/takeneko/appwebterminal/api/ImageProvider.java: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.api; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ImageProvider { 11 | String value(); 12 | String modid(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/icu/takeneko/appwebterminal/config/AppWebTerminalConfig.java: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.config; 2 | 3 | import dev.toma.configuration.config.Config; 4 | import dev.toma.configuration.config.Configurable; 5 | import dev.toma.configuration.config.UpdateRestrictions; 6 | import icu.takeneko.appwebterminal.AppWebTerminal; 7 | 8 | @Config(id = AppWebTerminal.MOD_ID) 9 | public class AppWebTerminalConfig { 10 | @Configurable 11 | @Configurable.Comment({"Http Server port for ME Web Terminal."}) 12 | @Configurable.Range( 13 | min = 1L, 14 | max = 65536L 15 | ) 16 | @Configurable.UpdateRestriction(UpdateRestrictions.MAIN_MENU) 17 | public int httpPort = 11451; 18 | 19 | @Configurable 20 | @Configurable.Comment({"Web Terminal Title"}) 21 | public String frontendTitle = "Applied Web Terminal"; 22 | 23 | @Configurable 24 | @Configurable.Comment({"Websocket url for frontend to connect", "Use \"~\" for auto detecting"}) 25 | public String backendWebsocketEndpoint = "~"; 26 | 27 | @Configurable 28 | @Configurable.Comment({"Add PinIn support to languages"}) 29 | public String[] needPinInLanguage = {"zh_cn", "zh_tw", "zh_hk"}; 30 | 31 | @Configurable 32 | @Configurable.Comment({"Used api to download minecraft langague files"}) 33 | @Configurable.UpdateRestriction(UpdateRestrictions.GAME_RESTART) 34 | public MinecraftAssetsApi assetsSource = MinecraftAssetsApi.MOJANG; 35 | 36 | @SuppressWarnings("unused") 37 | public int getHttpPort() { 38 | return httpPort; 39 | } 40 | 41 | @SuppressWarnings("unused") 42 | public String getFrontendTitle() { 43 | return frontendTitle; 44 | } 45 | 46 | @SuppressWarnings("unused") 47 | public String getBackendWebsocketEndpoint() { 48 | return backendWebsocketEndpoint; 49 | } 50 | 51 | @SuppressWarnings("unused") 52 | public String[] getNeedPinInLanguage() { 53 | return needPinInLanguage; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/icu/takeneko/appwebterminal/config/MinecraftAssetsApi.java: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.config; 2 | 3 | import java.util.function.Function; 4 | 5 | public enum MinecraftAssetsApi { 6 | MOJANG( 7 | "https://piston-meta.mojang.com", 8 | Function.identity(), 9 | Function.identity(), 10 | Function.identity() 11 | ), 12 | BMCLAPI( 13 | "https://bmclapi2.bangbang93.com", 14 | s -> s.replace("https://piston-meta.mojang.com", "https://piston-meta.bmclapi.com"), 15 | s -> s.replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com"), 16 | s -> s.replace("https://resources.download.minecraft.net", "https://bmclapi2.bangbang93.com/assets") 17 | ); 18 | 19 | private final String endpoint; 20 | private final Function urlManifestReplacer; 21 | private final Function urlIndexReplacer; 22 | private final Function urlAssetsReplacer; 23 | 24 | MinecraftAssetsApi(String endpoint, Function urlManifestReplacer, Function urlIndexReplacer, Function urlAssetsReplacer) { 25 | this.endpoint = endpoint; 26 | this.urlManifestReplacer = urlManifestReplacer; 27 | this.urlIndexReplacer = urlIndexReplacer; 28 | this.urlAssetsReplacer = urlAssetsReplacer; 29 | } 30 | 31 | public String getEndpoint() { 32 | return endpoint; 33 | } 34 | 35 | public Function getUrlManifestReplacer() { 36 | return urlManifestReplacer; 37 | } 38 | 39 | public Function getUrlIndexReplacer() { 40 | return urlIndexReplacer; 41 | } 42 | 43 | public Function getUrlAssetsReplacer() { 44 | return urlAssetsReplacer; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/icu/takeneko/appwebterminal/mixins/LevelRendererMixin.java: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.mixins; 2 | 3 | import icu.takeneko.appwebterminal.client.all.ShadersKt; 4 | import icu.takeneko.appwebterminal.client.rendering.foundation.PostProcess; 5 | import net.minecraft.client.renderer.LevelRenderer; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 10 | 11 | @Mixin(LevelRenderer.class) 12 | public class LevelRendererMixin { 13 | @Inject(method = "resize", at = @At("HEAD")) 14 | private void onResize(int width, int height, CallbackInfo ci) { 15 | PostProcess instance = ShadersKt.getBlurPostProcessInstance(); 16 | if (instance != null) { 17 | instance.resize(width, height); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/icu/takeneko/appwebterminal/mixins/RenderStateShardMixin.java: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.mixins; 2 | 3 | import icu.takeneko.appwebterminal.client.rendering.AEKeyRenderer; 4 | import net.minecraft.client.renderer.RenderStateShard; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | @Mixin(RenderStateShard.class) 11 | public class RenderStateShardMixin { 12 | @Inject( 13 | method = "setupRenderState", 14 | at = @At("HEAD"), 15 | cancellable = true 16 | ) 17 | private void invalidateOutputStateShardSetup(CallbackInfo ci) { 18 | //noinspection ConstantValue 19 | if ((Object) this instanceof RenderStateShard.OutputStateShard && AEKeyRenderer.Companion.getRendering()) { 20 | ci.cancel(); 21 | } 22 | } 23 | 24 | @Inject( 25 | method = "clearRenderState", 26 | at = @At("HEAD"), 27 | cancellable = true 28 | ) 29 | private void invalidateOutputStateShardClear(CallbackInfo ci) { 30 | //noinspection ConstantValue 31 | if ((Object) this instanceof RenderStateShard.OutputStateShard && AEKeyRenderer.Companion.getRendering()) { 32 | ci.cancel(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/AppWebTerminal.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal 2 | 3 | import com.mojang.logging.LogUtils 4 | import dev.toma.configuration.Configuration 5 | import dev.toma.configuration.config.format.ConfigFormats 6 | import icu.takeneko.appwebterminal.all.onAddRegistries 7 | import icu.takeneko.appwebterminal.all.onChunkUnloaded 8 | import icu.takeneko.appwebterminal.all.onCommonSetup 9 | import icu.takeneko.appwebterminal.all.onRegister 10 | import icu.takeneko.appwebterminal.all.onServerStart 11 | import icu.takeneko.appwebterminal.all.onServerStop 12 | import icu.takeneko.appwebterminal.all.onServerTickPost 13 | import icu.takeneko.appwebterminal.all.registerBlockEntities 14 | import icu.takeneko.appwebterminal.all.registerBlocks 15 | import icu.takeneko.appwebterminal.all.registerClientCommand 16 | import icu.takeneko.appwebterminal.all.registerCreativeTab 17 | import icu.takeneko.appwebterminal.all.registerItems 18 | import icu.takeneko.appwebterminal.all.registerKeyImageProviders 19 | import icu.takeneko.appwebterminal.all.registerNetworking 20 | import icu.takeneko.appwebterminal.api.KeyImageProviderLoader 21 | import icu.takeneko.appwebterminal.api.crafting.impl.AECraftingCpuContainerImpl 22 | import icu.takeneko.appwebterminal.client.onLoadShaders 23 | import icu.takeneko.appwebterminal.client.onRenderLevelPost 24 | import icu.takeneko.appwebterminal.compat.advancedae.registerAAECompat 25 | import icu.takeneko.appwebterminal.config.AppWebTerminalConfig 26 | import icu.takeneko.appwebterminal.data.configureDataGeneration 27 | import icu.takeneko.appwebterminal.resource.LanguageFileDownloader 28 | import icu.takeneko.appwebterminal.util.KRegistrate 29 | import net.minecraft.resources.ResourceLocation 30 | import net.minecraftforge.api.distmarker.Dist 31 | import net.minecraftforge.common.MinecraftForge 32 | import net.minecraftforge.eventbus.api.IEventBus 33 | import net.minecraftforge.fml.common.Mod 34 | import org.slf4j.Logger 35 | import thedarkcolour.kotlinforforge.forge.FORGE_BUS 36 | import thedarkcolour.kotlinforforge.forge.MOD_BUS 37 | import thedarkcolour.kotlinforforge.forge.runWhenOn 38 | 39 | val registrate = KRegistrate.create(AppWebTerminal.MOD_ID) 40 | 41 | 42 | @Mod(AppWebTerminal.MOD_ID) 43 | object AppWebTerminal { 44 | const val MOD_ID = "appwebterminal" 45 | private val LOGGER: Logger = LogUtils.getLogger() 46 | val modBus: IEventBus = MOD_BUS 47 | internal val configHolder = Configuration.registerConfig(AppWebTerminalConfig::class.java, ConfigFormats.YAML) 48 | val config: AppWebTerminalConfig 49 | get() = configHolder.configInstance 50 | 51 | fun location(location: String): ResourceLocation = ResourceLocation(MOD_ID, location) 52 | 53 | init { 54 | registerBlockEntities() 55 | registerBlocks() 56 | registerItems() 57 | registerCreativeTab() 58 | registerNetworking() 59 | configureDataGeneration() 60 | registerKeyImageProviders(modBus) 61 | modBus.addListener(::onCommonSetup) 62 | modBus.addListener(::onAddRegistries) 63 | modBus.addListener(::onRegister) 64 | MinecraftForge.EVENT_BUS.addListener(::onServerStart) 65 | MinecraftForge.EVENT_BUS.addListener(::onServerStop) 66 | MinecraftForge.EVENT_BUS.addListener(::onChunkUnloaded) 67 | MinecraftForge.EVENT_BUS.addListener(::onServerTickPost) 68 | LanguageFileDownloader(config.assetsSource).start() 69 | KeyImageProviderLoader.compileContents() 70 | LOGGER.info("AppWebTerminal initialized") 71 | AECraftingCpuContainerImpl.register() 72 | registerAAECompat() 73 | runWhenOn(Dist.CLIENT) { 74 | FORGE_BUS.addListener(::registerClientCommand) 75 | FORGE_BUS.addListener(::onRenderLevelPost) 76 | modBus.addListener(::onLoadShaders) 77 | } 78 | } 79 | 80 | 81 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/BlockEntities.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import com.tterrag.registrate.util.entry.BlockEntityEntry 4 | import icu.takeneko.appwebterminal.block.entity.WebTerminalBlockEntity 5 | import icu.takeneko.appwebterminal.registrate 6 | 7 | val MEWebTerminalBlockEntity: BlockEntityEntry = 8 | registrate.blockEntity("web_terminal", ::WebTerminalBlockEntity) 9 | .validBlock(MEWebTerminal) 10 | .register() 11 | 12 | fun registerBlockEntities() { 13 | 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/Blocks.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import appeng.core.definitions.AEBlocks 4 | import appeng.core.definitions.AEItems 5 | import com.tterrag.registrate.providers.RegistrateRecipeProvider 6 | import com.tterrag.registrate.util.entry.BlockEntry 7 | import icu.takeneko.appwebterminal.AppWebTerminal 8 | import icu.takeneko.appwebterminal.block.WebTerminalBlock 9 | import icu.takeneko.appwebterminal.registrate 10 | import icu.takeneko.appwebterminal.util.get 11 | import net.minecraft.data.recipes.RecipeCategory 12 | import net.minecraft.data.recipes.ShapedRecipeBuilder 13 | import net.minecraft.data.recipes.ShapelessRecipeBuilder 14 | import net.minecraft.tags.BlockTags 15 | import net.minecraft.world.level.block.Blocks 16 | import net.minecraftforge.client.model.generators.ModelFile.UncheckedModelFile 17 | 18 | val MEWebTerminal: BlockEntry = registrate.block("web_terminal", ::WebTerminalBlock) 19 | .initialProperties { Blocks.IRON_BLOCK } 20 | .blockstate { dataGenContext, registrateBlockstateProvider -> 21 | registrateBlockstateProvider.directionalBlock(dataGenContext.get()) { 22 | UncheckedModelFile( 23 | AppWebTerminal.location( 24 | if (it[WebTerminalBlock.ONLINE]) 25 | "block/web_terminal" 26 | else 27 | "block/web_terminal_offline" 28 | ) 29 | ) 30 | } 31 | } 32 | .lang("ME Web Terminal") 33 | .properties { 34 | it.lightLevel { bs -> 35 | if (bs[WebTerminalBlock.ONLINE]) 5 else 0 36 | } 37 | } 38 | .tag(BlockTags.MINEABLE_WITH_PICKAXE) 39 | .tag(BlockTags.NEEDS_STONE_TOOL) 40 | .item() 41 | .model { dataGenContext, registrateItemModelProvider -> 42 | registrateItemModelProvider.getBuilder(dataGenContext.name) 43 | .parent(UncheckedModelFile(AppWebTerminal.location("block/web_terminal"))) 44 | } 45 | .recipe { ctx, prov -> 46 | ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ctx.get()) 47 | .pattern("ABA") 48 | .pattern("CDC") 49 | .pattern("EFE") 50 | .define('A', AEItems.CERTUS_QUARTZ_CRYSTAL_CHARGED) 51 | .define('B', AEBlocks.WIRELESS_ACCESS_POINT) 52 | .define('C', AEItems.ENGINEERING_PROCESSOR) 53 | .define('D', AEItems.CRAFTING_CARD) 54 | .define('E', AEBlocks.QUARTZ_VIBRANT_GLASS) 55 | .define('F', AEBlocks.CRAFTING_MONITOR) 56 | .unlockedBy("has_item", RegistrateRecipeProvider.has(AEBlocks.WIRELESS_ACCESS_POINT)) 57 | .save(prov) 58 | ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ctx.get()) 59 | .requires(MEWebTerminalPartItem) 60 | .unlockedBy("has_item", RegistrateRecipeProvider.has(MEWebTerminalPartItem)) 61 | .save(prov, AppWebTerminal.location("cable_web_terminal_from_part")) 62 | } 63 | .build() 64 | .register() 65 | 66 | 67 | fun registerBlocks() { 68 | //intentionally empty 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/Commands.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import appeng.api.stacks.AEKey 4 | import icu.takeneko.appwebterminal.client.rendering.AEKeyRenderer 5 | import icu.takeneko.appwebterminal.client.rendering.JProgressWindow 6 | import icu.takeneko.appwebterminal.client.rendering.RenderProgressListener 7 | import icu.takeneko.appwebterminal.util.CommandContext 8 | import icu.takeneko.appwebterminal.util.LiteralCommand 9 | import icu.takeneko.appwebterminal.util.S 10 | import icu.takeneko.appwebterminal.util.execute 11 | import icu.takeneko.appwebterminal.util.integerArgument 12 | import icu.takeneko.appwebterminal.util.literal 13 | import icu.takeneko.appwebterminal.util.sendError 14 | import icu.takeneko.appwebterminal.util.sendFeedback 15 | import net.minecraft.client.Minecraft 16 | import net.minecraft.commands.CommandSourceStack 17 | import net.minecraft.network.chat.Component 18 | import net.minecraftforge.client.event.RegisterClientCommandsEvent 19 | import org.slf4j.LoggerFactory 20 | import java.awt.HeadlessException 21 | import kotlin.io.path.Path 22 | 23 | val AppWebTerminalCommand = LiteralCommand("appwebterminal") { 24 | literal("resources") { 25 | literal("upload") { 26 | execute { 27 | val src: CommandSourceStack by this 28 | if (Minecraft.getInstance().isLocalServer) { 29 | sendError(Component.translatable("appwebterminal.message.join_server_required")) 30 | return@execute 1 31 | } 32 | 33 | 0 34 | } 35 | } 36 | literal("render") { 37 | integerArgument("limit") { 38 | integerArgument("size") { 39 | execute { 40 | val limit: Int by this 41 | val size: Int by this 42 | executeRender(limit, size) 43 | } 44 | } 45 | execute { 46 | val limit: Int by this 47 | executeRender(limit, 256) 48 | } 49 | } 50 | execute { 51 | executeRender(1, 256) 52 | } 53 | } 54 | literal("listRegistered") { 55 | execute { 56 | KeyImageProviderRegistry.keys.forEach { 57 | sendFeedback(Component.literal(it.toString())) 58 | } 59 | return@execute 1 60 | } 61 | } 62 | } 63 | } 64 | 65 | private class ProgressListenerImpl(private val src: CommandSourceStack) : RenderProgressListener { 66 | private var total: Int = 0 67 | private val logger = LoggerFactory.getLogger("RenderProgress") 68 | val progressWindow: JProgressWindow? = try { 69 | JProgressWindow("Rendering Resources") 70 | } catch (_: HeadlessException) { 71 | null 72 | } 73 | 74 | override fun notifyTotalCount(size: Int) { 75 | logger.info("Total: $size") 76 | src.sendSystemMessage(Component.literal("Total: $size")) 77 | total = size 78 | progressWindow?.notifyTotalCount(size) 79 | } 80 | 81 | override fun notifyProgress(current: Int, what: AEKey) { 82 | logger.info("Progress: $current/$total, Current: ${what.type}/${what.id}") 83 | src.sendSystemMessage(Component.literal("Progress: $current/$total, Current: ${what.type}/${what.id}")) 84 | progressWindow?.notifyProgress(current, what) 85 | } 86 | 87 | override fun notifyCompleted() { 88 | logger.info("Render Completed") 89 | progressWindow?.notifyCompleted() 90 | } 91 | } 92 | 93 | fun registerClientCommand(event: RegisterClientCommandsEvent) { 94 | event.dispatcher.register(AppWebTerminalCommand.node) 95 | } 96 | 97 | fun CommandContext.executeRender(limit: Int, size: Int): Int { 98 | if (AEKeyRenderer.instance != null) { 99 | sendError(Component.translatable("appwebterminal.message.rendering")) 100 | return 1 101 | } 102 | System.setProperty("java.awt.headless", "false") 103 | val progressListener = ProgressListenerImpl(this.delegate.source) 104 | progressListener.progressWindow?.show() 105 | val renderer = AEKeyRenderer(size, size).apply { 106 | submitRenderTasks(Path("./aeKeyResources"), progressListener) 107 | } 108 | renderer.taskPullLimit = limit 109 | renderer.renderTasks += { 110 | sendFeedback(Component.translatable("appwebterminal.message.render_complete")) 111 | renderer.dispose() 112 | AEKeyRenderer.instance = null 113 | } 114 | AEKeyRenderer.instance = renderer 115 | progressListener.progressWindow?.dismiss() 116 | sendFeedback(Component.translatable("appwebterminal.message.started")) 117 | return 1 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/CreativeTabs.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import icu.takeneko.appwebterminal.registrate 4 | 5 | val appWebCreativeTab = registrate 6 | .defaultCreativeTab("applied_web_terminal") { 7 | it.icon(MEWebTerminal::asStack) 8 | it.displayItems { parameters, output -> 9 | output.accept(MEWebTerminal) 10 | output.accept(MEWebTerminalPartItem) 11 | } 12 | } 13 | .register() 14 | 15 | fun registerCreativeTab() { 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/Events.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import icu.takeneko.appwebterminal.AppWebTerminal 4 | import icu.takeneko.appwebterminal.api.KeyImageProviderLoader 5 | import icu.takeneko.appwebterminal.block.LateInitSupported 6 | import icu.takeneko.appwebterminal.block.entity.WebTerminalBlockEntity 7 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 8 | import icu.takeneko.appwebterminal.registrate 9 | import icu.takeneko.appwebterminal.support.AENetworkSupport 10 | import icu.takeneko.appwebterminal.support.http.HttpServerLifecycleSupport 11 | import icu.takeneko.appwebterminal.util.KubejsI18nSupport 12 | import net.minecraft.core.registries.Registries 13 | import net.minecraft.server.level.ServerLevel 14 | import net.minecraft.world.level.chunk.LevelChunk 15 | import net.minecraftforge.event.TickEvent 16 | import net.minecraftforge.event.TickEvent.ServerTickEvent 17 | import net.minecraftforge.event.level.ChunkEvent 18 | import net.minecraftforge.event.server.ServerStartedEvent 19 | import net.minecraftforge.event.server.ServerStoppedEvent 20 | import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent 21 | import net.minecraftforge.registries.NewRegistryEvent 22 | import net.minecraftforge.registries.RegisterEvent 23 | import net.minecraftforge.registries.RegistryBuilder 24 | 25 | fun onCommonSetup(event: FMLCommonSetupEvent) { 26 | registrate.getAll(Registries.BLOCK).forEach { 27 | val block = it.get() 28 | if (block is LateInitSupported) { 29 | block.lateInit() 30 | } 31 | } 32 | KubejsI18nSupport.init() 33 | } 34 | 35 | fun onAddRegistries(event: NewRegistryEvent) { 36 | event.create( 37 | RegistryBuilder>().setName(KeyImageProviderRegistryKey.location()) 38 | ) { _KeyImageProviderRegistry = { it } } 39 | } 40 | 41 | fun onChunkUnloaded(event: ChunkEvent.Unload) { 42 | if (event.level is ServerLevel) { 43 | if (event.chunk is LevelChunk) { 44 | (event.chunk as LevelChunk).blockEntities.forEach { (_, blockEntity) -> 45 | if (blockEntity is WebTerminalBlockEntity) { 46 | AENetworkSupport.remove(blockEntity) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | fun onServerTickPost(event: ServerTickEvent) { 54 | if (event.phase != TickEvent.Phase.END) return 55 | AENetworkSupport.tick() 56 | } 57 | 58 | fun onRegister(event: RegisterEvent) { 59 | if (event.registryKey == KeyImageProviderRegistryKey) { 60 | KeyImageProviderLoader.providers.forEach { t, u -> 61 | event.register( 62 | KeyImageProviderRegistryKey, 63 | t, 64 | u 65 | ) 66 | } 67 | } 68 | } 69 | 70 | fun onServerStart(event: ServerStartedEvent) { 71 | HttpServerLifecycleSupport.launch(AppWebTerminal.config.httpPort) 72 | } 73 | 74 | fun onServerStop(event: ServerStoppedEvent) { 75 | HttpServerLifecycleSupport.stop() 76 | AENetworkSupport.reset() 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/Items.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import appeng.api.parts.PartModels 4 | import appeng.items.parts.PartItem 5 | import appeng.items.parts.PartModelsHelper 6 | import com.tterrag.registrate.providers.RegistrateRecipeProvider 7 | import icu.takeneko.appwebterminal.AppWebTerminal 8 | import icu.takeneko.appwebterminal.block.part.WebTerminalPart 9 | import icu.takeneko.appwebterminal.registrate 10 | import net.minecraft.data.recipes.RecipeCategory 11 | import net.minecraft.data.recipes.ShapelessRecipeBuilder 12 | 13 | val MEWebTerminalPartItem = registrate 14 | .item>("cable_web_terminal") { 15 | PartItem( 16 | it, 17 | WebTerminalPart::class.java 18 | ) { item -> WebTerminalPart(item) } 19 | }.lang("ME Web Terminal") 20 | .model { _, _ -> } 21 | .recipe { dataGenContext, registrateRecipeProvider -> 22 | ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, dataGenContext.get()) 23 | .requires(MEWebTerminal) 24 | .unlockedBy("has_item", RegistrateRecipeProvider.has(MEWebTerminal)) 25 | .save(registrateRecipeProvider, AppWebTerminal.location("cable_web_terminal_from_block")) 26 | }.register() 27 | 28 | 29 | fun registerItems() { 30 | PartModels.registerModels(PartModelsHelper.createModels(WebTerminalPart::class.java)) 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/KeyImageProviders.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package icu.takeneko.appwebterminal.all 4 | 5 | import icu.takeneko.appwebterminal.AppWebTerminal 6 | import icu.takeneko.appwebterminal.api.KeyImageProviderLoader 7 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 8 | import icu.takeneko.appwebterminal.client.rendering.providers.AEFluidKeyImageProvider 9 | import icu.takeneko.appwebterminal.client.rendering.providers.AEItemKeyImageProvider 10 | import net.minecraft.resources.ResourceLocation 11 | import net.minecraftforge.eventbus.api.IEventBus 12 | import net.minecraftforge.registries.DeferredRegister 13 | import net.minecraftforge.registries.RegistryObject 14 | 15 | private val DR = DeferredRegister.create(KeyImageProviderRegistryKey, AppWebTerminal.MOD_ID) 16 | 17 | val itemKeyImageProvider: RegistryObject = DR.register("item") { 18 | AEItemKeyImageProvider 19 | } 20 | 21 | val fluidKeyImageProvider: RegistryObject = DR.register("fluid") { 22 | AEFluidKeyImageProvider 23 | } 24 | 25 | val extensionProviders = mutableMapOf>>() 26 | 27 | fun registerKeyImageProviders(modBus: IEventBus) { 28 | DR.register(modBus) 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/Networking.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import icu.takeneko.appwebterminal.AppWebTerminal 4 | import icu.takeneko.appwebterminal.networking.OpenWebTerminalScreenPacket 5 | import icu.takeneko.appwebterminal.networking.UpdateWebTerminalNamePacket 6 | import net.minecraftforge.network.NetworkRegistry 7 | import net.minecraftforge.network.simple.SimpleChannel 8 | 9 | private const val version = "1" 10 | 11 | val networkingChannel: SimpleChannel = NetworkRegistry.newSimpleChannel( 12 | AppWebTerminal.location("networking"), 13 | { version }, 14 | { it == version }, 15 | { it == version } 16 | ) 17 | 18 | private var id = 0 19 | 20 | fun registerNetworking() { 21 | networkingChannel.registerMessage( 22 | id++, 23 | OpenWebTerminalScreenPacket::class.java, 24 | OpenWebTerminalScreenPacket::encode, 25 | ::OpenWebTerminalScreenPacket, 26 | OpenWebTerminalScreenPacket::accept 27 | ) 28 | 29 | networkingChannel.registerMessage( 30 | id++, 31 | UpdateWebTerminalNamePacket::class.java, 32 | UpdateWebTerminalNamePacket::encode, 33 | ::UpdateWebTerminalNamePacket, 34 | UpdateWebTerminalNamePacket::accept 35 | ) 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/all/Registries.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.all 2 | 3 | import icu.takeneko.appwebterminal.AppWebTerminal 4 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 5 | import net.minecraft.core.Registry 6 | import net.minecraft.resources.ResourceKey 7 | import net.minecraftforge.registries.IForgeRegistry 8 | 9 | val KeyImageProviderRegistryKey: ResourceKey>> = 10 | ResourceKey.createRegistryKey>(AppWebTerminal.location("key_image_providers")) 11 | 12 | @Suppress("ObjectPropertyName") 13 | internal var _KeyImageProviderRegistry: (() -> IForgeRegistry>)? = null 14 | 15 | val KeyImageProviderRegistry: IForgeRegistry> 16 | get() = _KeyImageProviderRegistry!!() -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/api/KeyImageProviderLoader.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.api 2 | 3 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 4 | import net.minecraft.resources.ResourceLocation 5 | import net.minecraftforge.fml.loading.LoadingModList 6 | import org.slf4j.LoggerFactory 7 | import java.lang.annotation.ElementType 8 | 9 | object KeyImageProviderLoader { 10 | val providers = mutableMapOf AEKeyImageProvider<*>>() 11 | private val ANNOTATION_NAME = "L" + ImageProvider::class.java.name.replace(".", "/") + ";" 12 | private val logger = LoggerFactory.getLogger("KeyImageProviderLoader") 13 | 14 | internal fun compileContents() { 15 | for (modFile in LoadingModList.get().modFiles) { 16 | val scanData = modFile.file.getScanResult() 17 | for (annotation in scanData.annotations) { 18 | if (annotation.annotationType().descriptor == ANNOTATION_NAME 19 | && annotation.targetType() === ElementType.TYPE 20 | ) { 21 | val id = annotation.annotationData()["value"] as String 22 | val modid = annotation.annotationData()["modid"] as String 23 | logger.info("Considering AEKeyImageProvider {} for {}", annotation.memberName(), modid) 24 | if (LoadingModList.get().mods.any { it.modId == modid }) { 25 | this.providers[ResourceLocation(id)] = { 26 | Class.forName(annotation.memberName).run { 27 | this.getConstructor().apply { trySetAccessible() } 28 | }.newInstance() as AEKeyImageProvider<*> 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/api/crafting/CraftingCpuContainer.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.api.crafting 2 | 3 | import appeng.api.config.CpuSelectionMode 4 | import appeng.api.networking.crafting.ICraftingCPU 5 | import appeng.api.stacks.AEKey 6 | import appeng.api.stacks.KeyCounter 7 | import appeng.menu.me.common.IncrementalUpdateHelper 8 | import appeng.menu.me.crafting.CraftingStatus 9 | import java.util.function.Consumer 10 | 11 | interface CraftingCpuContainer { 12 | 13 | fun unwrap(): ICraftingCPU 14 | 15 | fun selectionMode(): CpuSelectionMode 16 | 17 | fun cantStoreItems(): Boolean 18 | 19 | fun createCraftingStatus(helper: IncrementalUpdateHelper): CraftingStatus 20 | 21 | fun removeUpdateListener(cons: Consumer) 22 | 23 | fun addUpdateListener(cons: Consumer) 24 | 25 | fun getAllItems(): KeyCounter 26 | 27 | @FunctionalInterface 28 | interface Constructor where T : ICraftingCPU { 29 | fun create(cpu: T): CraftingCpuContainer 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/api/crafting/CraftingCpuContainerHelper.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.api.crafting 2 | 3 | import appeng.api.networking.crafting.ICraftingCPU 4 | 5 | object CraftingCpuContainerHelper { 6 | private val constructors = 7 | mutableMapOf, CraftingCpuContainer.Constructor>() 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | fun registerConstructor( 11 | clazz: Class, 12 | constructor: CraftingCpuContainer.Constructor 13 | ) where T : ICraftingCPU { 14 | constructors.put(clazz, constructor as CraftingCpuContainer.Constructor) 15 | } 16 | 17 | fun create( 18 | instance: ICraftingCPU 19 | ): CraftingCpuContainer? { 20 | val clazz = instance.javaClass as Class 21 | return constructors[clazz]?.create(instance) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/api/crafting/impl/AECraftingCpuContainerImpl.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.api.crafting.impl 2 | 3 | import appeng.api.config.CpuSelectionMode 4 | import appeng.api.networking.crafting.ICraftingCPU 5 | import appeng.api.stacks.AEKey 6 | import appeng.api.stacks.KeyCounter 7 | import appeng.me.cluster.implementations.CraftingCPUCluster 8 | import appeng.menu.me.common.IncrementalUpdateHelper 9 | import appeng.menu.me.crafting.CraftingStatus 10 | import icu.takeneko.appwebterminal.api.crafting.CraftingCpuContainer 11 | import icu.takeneko.appwebterminal.api.crafting.CraftingCpuContainerHelper 12 | import java.util.function.Consumer 13 | 14 | class AECraftingCpuContainerImpl(private val cluster: CraftingCPUCluster) : CraftingCpuContainer { 15 | 16 | override fun unwrap(): ICraftingCPU = cluster 17 | 18 | override fun selectionMode(): CpuSelectionMode = cluster.selectionMode 19 | 20 | override fun cantStoreItems(): Boolean = cluster.craftingLogic.isCantStoreItems 21 | 22 | override fun createCraftingStatus(helper: IncrementalUpdateHelper): CraftingStatus { 23 | return CraftingStatus.create(helper, cluster.craftingLogic) 24 | } 25 | 26 | override fun removeUpdateListener(cons: Consumer) { 27 | cluster.craftingLogic.removeListener(cons) 28 | } 29 | 30 | override fun addUpdateListener(cons: Consumer) { 31 | cluster.craftingLogic.addListener(cons) 32 | } 33 | 34 | override fun getAllItems(): KeyCounter { 35 | val counter = KeyCounter() 36 | cluster.craftingLogic.getAllItems(counter) 37 | return counter 38 | } 39 | 40 | companion object { 41 | fun register() = 42 | CraftingCpuContainerHelper.registerConstructor( 43 | CraftingCPUCluster::class.java, 44 | object : CraftingCpuContainer.Constructor { 45 | override fun create(cpu: CraftingCPUCluster) = AECraftingCpuContainerImpl(cpu) 46 | } 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/block/LateInitSupported.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.block 2 | 3 | interface LateInitSupported { 4 | fun lateInit() 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/block/WebTerminalBlock.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.block 2 | 3 | import appeng.api.orientation.IOrientationStrategy 4 | import appeng.api.orientation.OrientationStrategies 5 | import appeng.block.AEBaseEntityBlock 6 | import icu.takeneko.appwebterminal.all.MEWebTerminalBlockEntity 7 | import icu.takeneko.appwebterminal.all.networkingChannel 8 | import icu.takeneko.appwebterminal.block.entity.WebTerminalBlockEntity 9 | import icu.takeneko.appwebterminal.networking.OpenWebTerminalScreenPacket 10 | import net.minecraft.core.BlockPos 11 | import net.minecraft.core.Direction 12 | import net.minecraft.server.level.ServerPlayer 13 | import net.minecraft.world.InteractionHand 14 | import net.minecraft.world.InteractionResult 15 | import net.minecraft.world.entity.player.Player 16 | import net.minecraft.world.item.ItemStack 17 | import net.minecraft.world.level.Level 18 | import net.minecraft.world.level.block.Block 19 | import net.minecraft.world.level.block.state.BlockState 20 | import net.minecraft.world.level.block.state.StateDefinition 21 | import net.minecraft.world.level.block.state.properties.BlockStateProperties 22 | import net.minecraft.world.level.block.state.properties.BooleanProperty 23 | import net.minecraft.world.level.block.state.properties.DirectionProperty 24 | import net.minecraft.world.phys.BlockHitResult 25 | import net.minecraftforge.network.PacketDistributor 26 | 27 | class WebTerminalBlock(properties: Properties) : AEBaseEntityBlock(properties), 28 | LateInitSupported { 29 | init { 30 | registerDefaultState(stateDefinition.any().setValue(ONLINE, false).setValue(FACING, Direction.UP)) 31 | } 32 | 33 | override fun lateInit() { 34 | setBlockEntity( 35 | WebTerminalBlockEntity::class.java, 36 | MEWebTerminalBlockEntity.get(), 37 | null, 38 | null 39 | ) 40 | } 41 | 42 | override fun getOrientationStrategy(): IOrientationStrategy { 43 | return OrientationStrategies.facing() 44 | } 45 | 46 | override fun onActivated( 47 | level: Level, 48 | pos: BlockPos, 49 | player: Player, 50 | hand: InteractionHand, 51 | heldItem: ItemStack?, 52 | hit: BlockHitResult 53 | ): InteractionResult { 54 | if (level.isClientSide) { 55 | return InteractionResult.SUCCESS 56 | } 57 | val be = level.getBlockEntity(pos) as WebTerminalBlockEntity 58 | networkingChannel.send( 59 | PacketDistributor.PLAYER.with { player as ServerPlayer }, 60 | OpenWebTerminalScreenPacket(be.displayName, be.getId(), pos, be.password, be.mainNode.isOnline) 61 | ) 62 | return InteractionResult.SUCCESS 63 | } 64 | 65 | override fun createBlockStateDefinition(builder: StateDefinition.Builder) { 66 | builder.add(ONLINE, FACING) 67 | } 68 | 69 | companion object { 70 | val ONLINE: BooleanProperty = BooleanProperty.create("online") 71 | val FACING: DirectionProperty = BlockStateProperties.FACING 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/block/entity/WebTerminalBlockEntity.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.block.entity 2 | 3 | import appeng.api.networking.GridFlags 4 | import appeng.api.networking.IGrid 5 | import appeng.api.networking.IGridNodeListener 6 | import appeng.api.orientation.BlockOrientation 7 | import appeng.blockentity.grid.AENetworkBlockEntity 8 | import icu.takeneko.appwebterminal.all.MEWebTerminal 9 | import icu.takeneko.appwebterminal.block.WebTerminalBlock 10 | import icu.takeneko.appwebterminal.support.AENetworkAccess 11 | import icu.takeneko.appwebterminal.support.AENetworkSupport 12 | import icu.takeneko.appwebterminal.util.get 13 | import io.ktor.util.* 14 | import net.minecraft.core.BlockPos 15 | import net.minecraft.core.Direction 16 | import net.minecraft.nbt.CompoundTag 17 | import net.minecraft.server.level.ServerLevel 18 | import net.minecraft.world.entity.player.Player 19 | import net.minecraft.world.level.Level 20 | import net.minecraft.world.level.block.Block 21 | import net.minecraft.world.level.block.entity.BlockEntityType 22 | import net.minecraft.world.level.block.state.BlockState 23 | import java.util.UUID 24 | 25 | class WebTerminalBlockEntity( 26 | blockEntityType: BlockEntityType<*>, 27 | blockPos: BlockPos, 28 | blockState: BlockState 29 | ) : AENetworkBlockEntity(blockEntityType, blockPos, blockState), AENetworkAccess { 30 | 31 | private var id: UUID = UUID.randomUUID() 32 | var displayName: String = "ME Web Terminal" 33 | private set 34 | var password: String = "AppliedWebTerminal" 35 | private set 36 | 37 | private var nonce = generateNonce() 38 | private var registered = false 39 | 40 | init { 41 | this.mainNode.setExposedOnSides(Direction.entries.toMutableSet() - blockState[WebTerminalBlock.FACING]) 42 | this.mainNode.setVisualRepresentation(MEWebTerminal.asStack()) 43 | this.mainNode.setIdlePowerUsage(3.0) 44 | this.mainNode.setFlags(GridFlags.REQUIRE_CHANNEL) 45 | } 46 | 47 | override fun getGridConnectableSides(orientation: BlockOrientation): Set { 48 | return Direction.entries.toMutableSet() - (level?.getBlockState(worldPosition) 49 | ?: blockState)[WebTerminalBlock.FACING] 50 | } 51 | 52 | private fun updateExposedSides() { 53 | this.mainNode.setExposedOnSides(Direction.entries.toMutableSet() - level!!.getBlockState(worldPosition)[WebTerminalBlock.FACING]) 54 | } 55 | 56 | override fun saveAdditional(data: CompoundTag) { 57 | super.saveAdditional(data) 58 | data.putString("Name", displayName) 59 | data.putUUID("UUID", id) 60 | data.putString("Password", password) 61 | } 62 | 63 | override fun loadTag(data: CompoundTag) { 64 | super.loadTag(data) 65 | this.displayName = if (data.contains("Name")) data.getString("Name") else "ME Web Terminal" 66 | this.id = if (data.hasUUID("UUID")) data.getUUID("UUID") else UUID.randomUUID() 67 | this.password = if (data.contains("Password")) data.getString("Password") else "AppliedWebTerminal" 68 | } 69 | 70 | override fun onReady() { 71 | super.onReady() 72 | updateState() 73 | } 74 | 75 | override fun onMainNodeStateChanged(reason: IGridNodeListener.State) { 76 | updateState() 77 | } 78 | 79 | private fun updateState() { 80 | level!!.setBlock( 81 | this.worldPosition, 82 | level!!.getBlockState(blockPos).setValue(WebTerminalBlock.ONLINE, mainNode.isOnline), 83 | Block.UPDATE_CLIENTS 84 | ) 85 | if (!registered && mainNode.isOnline) { 86 | AENetworkSupport.register(this) 87 | registered = true 88 | } 89 | if (registered && !mainNode.isOnline) { 90 | AENetworkSupport.remove(this) 91 | registered = false 92 | } 93 | 94 | } 95 | 96 | override fun setOwner(owner: Player?) { 97 | super.setOwner(owner) 98 | val ownerName = owner?.gameProfile?.name 99 | if ( ownerName != null) { 100 | this.displayName = "${ownerName}'s Web Terminal" 101 | markDirty() 102 | } 103 | } 104 | 105 | override fun onOrientationChanged(orientation: BlockOrientation) { 106 | super.onOrientationChanged(orientation) 107 | updateExposedSides() 108 | } 109 | 110 | override fun setRemoved() { 111 | super.setRemoved() 112 | if (this.level is ServerLevel) { 113 | AENetworkSupport.remove(this) 114 | } 115 | } 116 | 117 | override fun markDirty() { 118 | setChanged() 119 | } 120 | 121 | override fun getGrid(): IGrid? { 122 | return this.mainNode.grid 123 | } 124 | 125 | override fun auth(password: String): Boolean { 126 | return password == this.password 127 | } 128 | 129 | override fun update(displayName: String, password: String): Boolean { 130 | this.displayName = displayName 131 | val oldPassword = this.password 132 | this.password = password 133 | return if (oldPassword != password) { 134 | AENetworkSupport.requestSessionReset(this) 135 | this.nonce = generateNonce() 136 | true 137 | } else { 138 | false 139 | } 140 | } 141 | 142 | override fun validateNonce(nonce: String): Boolean { 143 | return nonce == this.nonce 144 | } 145 | 146 | override fun getNonce(): String { 147 | return nonce 148 | } 149 | 150 | override fun getTerminalName(): String { 151 | return displayName 152 | } 153 | 154 | override fun level(): Level? { 155 | return getLevel() 156 | } 157 | 158 | override fun getId(): UUID = id 159 | 160 | override fun worldPosition(): BlockPos = worldPosition 161 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/ClientEvents.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client 2 | 3 | import com.mojang.blaze3d.vertex.DefaultVertexFormat 4 | import icu.takeneko.appwebterminal.AppWebTerminal 5 | import icu.takeneko.appwebterminal.client.all.BlurPostProcessInstance 6 | import icu.takeneko.appwebterminal.client.all.BlurShader 7 | import icu.takeneko.appwebterminal.client.all.TexturedRoundRect 8 | import icu.takeneko.appwebterminal.client.all.blurShaderLoaded 9 | import icu.takeneko.appwebterminal.client.all.createPostProcess 10 | import icu.takeneko.appwebterminal.client.all.roundRectShaderLoaded 11 | import icu.takeneko.appwebterminal.client.gui.WebTerminalScreen 12 | import icu.takeneko.appwebterminal.client.rendering.AEKeyRenderer 13 | import net.minecraft.client.Minecraft 14 | import net.minecraft.client.renderer.ShaderInstance 15 | import net.minecraftforge.client.event.RegisterShadersEvent 16 | import net.minecraftforge.client.event.RenderLevelStageEvent 17 | 18 | 19 | fun onLoadShaders(event: RegisterShadersEvent) { 20 | print("11111") 21 | val res = event.resourceProvider 22 | event.registerShader( 23 | ShaderInstance( 24 | res, 25 | AppWebTerminal.location("blur"), 26 | DefaultVertexFormat.POSITION 27 | ) 28 | ) { 29 | BlurShader = it 30 | blurShaderLoaded = true 31 | createPostProcess() 32 | } 33 | 34 | event.registerShader( 35 | ShaderInstance( 36 | res, 37 | AppWebTerminal.location("rendertype_textured_round_rect"), 38 | DefaultVertexFormat.POSITION 39 | ) 40 | ) { 41 | TexturedRoundRect = it 42 | roundRectShaderLoaded = true 43 | createPostProcess() 44 | } 45 | } 46 | 47 | fun onRenderLevelPost(event: RenderLevelStageEvent) { 48 | if (event.stage == RenderLevelStageEvent.Stage.AFTER_LEVEL) { 49 | if (Minecraft.getInstance().screen is WebTerminalScreen) { 50 | val mainRenderTarget = Minecraft.getInstance().mainRenderTarget 51 | BlurPostProcessInstance?.apply(mainRenderTarget.colorTextureId) 52 | mainRenderTarget.bindWrite(true) 53 | } 54 | AEKeyRenderer.instance?.next() 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/all/Shaders.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.all 2 | 3 | import icu.takeneko.appwebterminal.client.rendering.foundation.BlurPostProcess 4 | import icu.takeneko.appwebterminal.client.rendering.foundation.SamplingBlurPostProcess 5 | import net.minecraft.client.Minecraft 6 | import net.minecraft.client.renderer.ShaderInstance 7 | 8 | internal lateinit var TexturedRoundRect: ShaderInstance 9 | internal lateinit var BlurShader: ShaderInstance 10 | 11 | internal var roundRectShaderLoaded = false 12 | internal var blurShaderLoaded = false 13 | 14 | internal var BlurPostProcessInstance: BlurPostProcess? = null 15 | 16 | internal fun createPostProcess() { 17 | if (roundRectShaderLoaded && blurShaderLoaded) { 18 | val window = Minecraft.getInstance().window 19 | BlurPostProcessInstance = BlurPostProcess(window.width, window.height) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/AEKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering 2 | 3 | import appeng.api.client.AEKeyRenderHandler 4 | import appeng.api.client.AEKeyRendering 5 | import appeng.api.stacks.AEKey 6 | import appeng.api.stacks.AEKeyType 7 | import com.mojang.blaze3d.vertex.PoseStack 8 | import net.minecraft.client.Minecraft 9 | import net.minecraft.client.renderer.LightTexture 10 | import net.minecraft.client.renderer.MultiBufferSource.BufferSource 11 | import kotlin.math.min 12 | 13 | interface AEKeyImageProvider { 14 | val keyType: AEKeyType 15 | fun renderImage( 16 | aeKey: T, 17 | poseStack: PoseStack, 18 | bufferSource: BufferSource, 19 | canvasSizeX: Int, 20 | canvasSizeY: Int 21 | ) { 22 | @Suppress("UNCHECKED_CAST") 23 | val renderer = AEKeyRendering.getOrThrow(keyType) as AEKeyRenderHandler 24 | poseStack.translate( 25 | canvasSizeX / 2f, 26 | canvasSizeY / 2f, 27 | 500f 28 | ) 29 | poseStack.scale(1f, -1f, 10f) 30 | renderer.drawOnBlockFace( 31 | poseStack, 32 | bufferSource, 33 | aeKey, 34 | (min(canvasSizeX, canvasSizeY) * 0.8).toFloat(), 35 | LightTexture.FULL_BLOCK, 36 | Minecraft.getInstance().level 37 | ) 38 | } 39 | 40 | fun getAllEntries(): Iterable 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/AEKeyRenderer.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering 2 | 3 | import appeng.api.stacks.AEKey 4 | import com.mojang.blaze3d.platform.Lighting 5 | import com.mojang.blaze3d.platform.NativeImage 6 | import com.mojang.blaze3d.systems.RenderSystem 7 | import com.mojang.blaze3d.vertex.PoseStack 8 | import icu.takeneko.appwebterminal.all.KeyImageProviderRegistry 9 | import icu.takeneko.appwebterminal.client.rendering.foundation.FrameBuffer 10 | import icu.takeneko.appwebterminal.client.rendering.foundation.FullyBufferedBufferSource 11 | import net.minecraft.client.Minecraft 12 | import org.joml.Matrix4f 13 | import org.slf4j.LoggerFactory 14 | import java.nio.file.Path 15 | import java.util.ArrayDeque 16 | import kotlin.io.path.createFile 17 | import kotlin.io.path.createParentDirectories 18 | import kotlin.io.path.deleteIfExists 19 | import kotlin.io.path.div 20 | 21 | class AEKeyRenderer( 22 | private val sizeX: Int, 23 | private val sizeY: Int 24 | ) { 25 | private val frameBuffer = FrameBuffer( 26 | sizeX, 27 | sizeY 28 | ) 29 | private val nativeImage = NativeImage( 30 | sizeX, 31 | sizeY, 32 | false 33 | ) 34 | 35 | private val projectionMatrix = Matrix4f().setOrtho( 36 | 0f, 37 | sizeX.toFloat(), 38 | sizeY.toFloat(), 39 | 0f, 40 | -1000f, 41 | 1000f 42 | ) 43 | 44 | private val logger = LoggerFactory.getLogger("AEKeyRenderer") 45 | val renderTasks = ArrayDeque<() -> Unit>() 46 | var taskPullLimit = 1 47 | private var lastNotify: Long = 0 48 | 49 | fun submitRenderTasks(basePath: Path, progressListener: RenderProgressListener) { 50 | rendering = true 51 | KeyImageProviderRegistry.values.toList().forEach { prov -> 52 | val entries = prov.getAllEntries().toList() 53 | renderTasks += { 54 | progressListener.notifyTotalCount(entries.size) 55 | } 56 | entries.forEachIndexed { index, it -> 57 | renderTasks += { 58 | val id = it.id 59 | if (System.currentTimeMillis() - lastNotify > 200) { 60 | progressListener.notifyProgress(index, it) 61 | lastNotify = System.currentTimeMillis() 62 | } 63 | @Suppress("UNCHECKED_CAST") 64 | renderSingle( 65 | it, 66 | prov as AEKeyImageProvider, 67 | basePath 68 | / it.type.id.toString().replace(":", "_") 69 | / "$id.png".replace(":", "_") 70 | ) 71 | } 72 | } 73 | } 74 | renderTasks += { 75 | progressListener.notifyCompleted() 76 | rendering = false 77 | } 78 | } 79 | 80 | fun next() { 81 | var limit = taskPullLimit 82 | while (renderTasks.isNotEmpty() && limit >= 0){ 83 | renderTasks.pop()() 84 | limit-- 85 | } 86 | } 87 | 88 | fun renderSingle(key: T, provider: AEKeyImageProvider, path: Path) { 89 | val bufferSource = FullyBufferedBufferSource() 90 | val poseStack = PoseStack() 91 | poseStack.pushPose() 92 | frameBuffer.clear() 93 | Lighting.setupForEntityInInventory() 94 | RenderSystem.enableCull() 95 | RenderSystem.setShaderColor(0.99f, 0.99f, 0.99f, 1f) 96 | frameBuffer.bindWrite(true) 97 | try { 98 | provider.renderImage( 99 | key, 100 | poseStack, 101 | bufferSource, 102 | sizeX, 103 | sizeY 104 | ) 105 | } catch (e: Exception) { 106 | logger.error("Error while rendering ${key.type.id}/${key.id}", e) 107 | } 108 | val uploadResult = bufferSource.upload() 109 | uploadResult.forEach { t, u -> 110 | t.setupRenderState() 111 | frameBuffer.bindWrite(true) 112 | u.drawWithShader( 113 | Matrix4f(), 114 | projectionMatrix, 115 | RenderSystem.getShader() 116 | ) 117 | t.clearRenderState() 118 | } 119 | frameBuffer.unbindWrite() 120 | Minecraft.getInstance().mainRenderTarget.bindWrite(true) 121 | frameBuffer.bindRead() 122 | nativeImage.downloadTexture(0, true) 123 | nativeImage.flipY() 124 | nativeImage.applyToAllPixels { 125 | if (it == -16777216) { 126 | return@applyToAllPixels 0 127 | } 128 | it 129 | } 130 | path.apply { 131 | createParentDirectories() 132 | deleteIfExists() 133 | createFile() 134 | nativeImage.writeToFile(this) 135 | } 136 | } 137 | 138 | fun dispose() { 139 | frameBuffer.dispose() 140 | nativeImage.close() 141 | } 142 | 143 | companion object { 144 | var rendering = false 145 | private set 146 | var instance: AEKeyRenderer? = null 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/JProgressWindow.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering 2 | 3 | import appeng.api.stacks.AEKey 4 | import javax.swing.BoxLayout 5 | import javax.swing.JFrame 6 | import javax.swing.JLabel 7 | import javax.swing.JPanel 8 | import javax.swing.JProgressBar 9 | 10 | class JProgressWindow( 11 | val title: String 12 | ) : RenderProgressListener { 13 | private val window: JFrame 14 | private val progressText: JLabel 15 | private val progressBar: JProgressBar 16 | private var totalCount:Int = 0 17 | 18 | init { 19 | window = JFrame(title) 20 | window.setSize(450, 200) 21 | val pane = JPanel() 22 | progressText = JLabel() 23 | progressBar = JProgressBar() 24 | progressText.text = "Progress: ?" 25 | pane.layout = BoxLayout(pane, BoxLayout.Y_AXIS) 26 | pane.add(progressText) 27 | pane.add(progressBar) 28 | window.add(pane) 29 | window.defaultCloseOperation = JFrame.DO_NOTHING_ON_CLOSE 30 | progressBar.isIndeterminate = true 31 | progressBar.isStringPainted = true 32 | } 33 | 34 | override fun notifyTotalCount(size: Int) { 35 | progressBar.isIndeterminate = false 36 | totalCount = size 37 | progressText.text = "Progress: $totalCount/?" 38 | progressBar.minimum = 0 39 | progressBar.maximum = totalCount 40 | progressBar.value = 0 41 | } 42 | 43 | override fun notifyProgress(current: Int, what: AEKey) { 44 | progressText.text = "Progress: $totalCount/$current Current: ${what.id}" 45 | progressBar.value = current 46 | } 47 | 48 | override fun notifyCompleted() { 49 | dismiss() 50 | } 51 | 52 | fun show() { 53 | window.isVisible = true 54 | } 55 | 56 | fun dismiss() { 57 | window.isVisible = false 58 | window.dispose() 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/RenderProgressListener.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering 2 | 3 | import appeng.api.stacks.AEKey 4 | 5 | interface RenderProgressListener { 6 | fun notifyTotalCount(size: Int) 7 | 8 | fun notifyProgress(current: Int, what: AEKey) 9 | 10 | fun notifyCompleted() 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/foundation/BlurPostProcess.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering.foundation 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem 4 | import icu.takeneko.appwebterminal.client.all.BlurShader 5 | import net.minecraft.client.Minecraft 6 | import net.minecraft.client.renderer.FogRenderer 7 | import net.minecraft.client.renderer.ShaderInstance 8 | 9 | class BlurPostProcess(xSize: Int, ySize: Int) : PostProcess(xSize, ySize) { 10 | 11 | private val swap1 = FrameBuffer(xSize, ySize) 12 | private val swap2 = FrameBuffer(xSize, ySize) 13 | val output = FrameBuffer(xSize, ySize) 14 | private val blurShader = BlurShader 15 | 16 | 17 | override fun resize(xSize: Int, ySize: Int) { 18 | super.resize(xSize, ySize) 19 | swap1.resize(xSize, ySize) 20 | swap2.resize(xSize, ySize) 21 | output.resize(xSize, ySize) 22 | } 23 | 24 | override fun apply(inputTexture: Int) { 25 | FogRenderer.levelFogColor() 26 | val background = RenderSystem.getShaderFogColor() 27 | swap1.setClearColor(background[0], background[1], background[2], background[3]) 28 | swap2.setClearColor(background[0], background[1], background[2], background[3]) 29 | output.setClearColor(background[0], background[1], background[2], background[3]) 30 | swap1.clear() 31 | swap2.clear() 32 | output.clear() 33 | processOnce( 34 | blurShader, 35 | inputTexture, 36 | swap1, 37 | ) { 38 | applyCommonUniforms(this) 39 | safeGetUniform("BlurDir").set(0f, 1f) 40 | } 41 | processOnce( 42 | blurShader, 43 | swap1.colorTextureId, 44 | swap2, 45 | ) { 46 | applyCommonUniforms(this) 47 | safeGetUniform("BlurDir").set(1f, 0f) 48 | } 49 | processOnce( 50 | blurShader, 51 | swap2.colorTextureId, 52 | swap1, 53 | ) { 54 | applyCommonUniforms(this) 55 | safeGetUniform("BlurDir").set(0f, 1f) 56 | } 57 | processOnce( 58 | blurShader, 59 | swap1.colorTextureId, 60 | swap2, 61 | ) { 62 | applyCommonUniforms(this) 63 | safeGetUniform("BlurDir").set(1f, 0f) 64 | } 65 | output.copyColorsFrom(swap2.framebufferId) 66 | } 67 | 68 | fun applyCommonUniforms(instance: ShaderInstance) { 69 | instance.safeGetUniform("ProjMat").set(projectionMatrix) 70 | instance.safeGetUniform("InSize").set(xSize.toFloat(), ySize.toFloat()) 71 | instance.safeGetUniform("OutSize").set(xSize.toFloat(), ySize.toFloat()) 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/foundation/FullyBufferedBufferSource.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering.foundation 2 | 3 | import com.mojang.blaze3d.vertex.BufferBuilder 4 | import com.mojang.blaze3d.vertex.VertexBuffer 5 | import com.mojang.blaze3d.vertex.VertexConsumer 6 | import it.unimi.dsi.fastutil.objects.Reference2IntMap 7 | import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap 8 | import net.minecraft.MethodsReturnNonnullByDefault 9 | import net.minecraft.client.renderer.MultiBufferSource 10 | import net.minecraft.client.renderer.RenderType 11 | import org.lwjgl.system.MemoryUtil 12 | import org.lwjgl.system.MemoryUtil.MemoryAllocator 13 | import java.util.function.Function 14 | import javax.annotation.ParametersAreNonnullByDefault 15 | 16 | @ParametersAreNonnullByDefault 17 | @MethodsReturnNonnullByDefault 18 | class FullyBufferedBufferSource : MultiBufferSource.BufferSource(null, null) { 19 | private val bufferBuilders: MutableMap = mutableMapOf() 20 | val indexCountMap: Reference2IntMap = Reference2IntOpenHashMap() 21 | 22 | override fun getBuffer(renderType: RenderType): VertexConsumer { 23 | return bufferBuilders.computeIfAbsent( 24 | renderType 25 | ) { BufferBuilder(15720).also { it.begin(renderType.mode(), renderType.format()) } } 26 | } 27 | 28 | val isEmpty: Boolean 29 | get() = !bufferBuilders.isEmpty() 30 | && bufferBuilders.values.stream().noneMatch { it.isCurrentBatchEmpty() } 31 | 32 | override fun endBatch(renderType: RenderType) { 33 | } 34 | 35 | override fun endBatch() { 36 | } 37 | 38 | override fun endLastBatch() { 39 | } 40 | 41 | fun upload(): Map { 42 | val result = mutableMapOf() 43 | bufferBuilders.forEach { (key, value) -> 44 | if (value.isCurrentBatchEmpty) return@forEach 45 | val mesh = value.end() 46 | val vertexBuffer = VertexBuffer(VertexBuffer.Usage.STATIC) 47 | vertexBuffer.bind() 48 | vertexBuffer.upload(mesh) 49 | result[key] = vertexBuffer 50 | } 51 | return result 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/foundation/PostProcess.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering.foundation 2 | 3 | import com.mojang.blaze3d.shaders.ProgramManager 4 | import com.mojang.blaze3d.systems.RenderSystem 5 | import com.mojang.blaze3d.vertex.BufferBuilder 6 | import com.mojang.blaze3d.vertex.BufferUploader 7 | import com.mojang.blaze3d.vertex.DefaultVertexFormat 8 | import com.mojang.blaze3d.vertex.Tesselator 9 | import com.mojang.blaze3d.vertex.VertexFormat 10 | import io.ktor.websocket.Frame 11 | import net.minecraft.client.renderer.ShaderInstance 12 | import org.joml.Matrix4f 13 | import org.lwjgl.opengl.GL11 14 | 15 | abstract class PostProcess( 16 | protected var xSize: Int, 17 | protected var ySize: Int 18 | ) { 19 | 20 | protected val projectionMatrix: Matrix4f = Matrix4f().setOrtho( 21 | 0f, 22 | xSize.toFloat(), 23 | 0f, 24 | ySize.toFloat(), 25 | 0.1f, 26 | 1000f 27 | ) 28 | 29 | protected val mvMat = Matrix4f() 30 | 31 | abstract fun apply(inputTexture: Int) 32 | 33 | open fun resize(xSize: Int, ySize: Int){ 34 | this.xSize = xSize 35 | this.ySize = ySize 36 | projectionMatrix.setOrtho( 37 | 0f, 38 | xSize.toFloat(), 39 | 0f, 40 | ySize.toFloat(), 41 | 0.1f, 42 | 1000f 43 | ) 44 | } 45 | 46 | protected inline fun processOnce( 47 | shader: ShaderInstance, 48 | inputTexture: Int, 49 | writeFramebuffer: FrameBuffer, 50 | crossinline uniformSetter: ShaderInstance.() -> Unit 51 | ) { 52 | RenderSystem.setShader { shader } 53 | shader.setSampler("DiffuseSampler", inputTexture) 54 | shader.uniformSetter() 55 | RenderSystem.depthFunc(GL11.GL_ALWAYS) 56 | RenderSystem.disableDepthTest() 57 | val bufferBuilder: BufferBuilder = Tesselator.getInstance().builder 58 | bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION) 59 | bufferBuilder.vertex(0.0, 0.0, 500.0).endVertex() 60 | bufferBuilder.vertex(xSize.toDouble(), 0.0, 500.0).endVertex() 61 | bufferBuilder.vertex(xSize.toDouble(), ySize.toDouble(), 500.0).endVertex() 62 | bufferBuilder.vertex(0.0, ySize.toDouble(), 500.0).endVertex() 63 | 64 | shader.apply() 65 | writeFramebuffer.bindWrite(true) 66 | BufferUploader.draw(bufferBuilder.end()) 67 | writeFramebuffer.unbindWrite() 68 | RenderSystem.depthFunc(GL11.GL_LEQUAL) 69 | RenderSystem.enableDepthTest() 70 | RenderSystem.setShader { null } 71 | shader.clear() 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/foundation/SamplingBlurPostProcess.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering.foundation 2 | 3 | import org.lwjgl.opengl.GL30 4 | import org.slf4j.LoggerFactory 5 | 6 | class SamplingBlurPostProcess( 7 | xSize: Int, 8 | ySize: Int, 9 | private val iterations: Int 10 | ) : PostProcess(xSize, ySize) { 11 | 12 | private val logger = LoggerFactory.getLogger("SamplingBlurPostProcess") 13 | private var buffers = createFramebuffers() 14 | 15 | val output: FrameBuffer 16 | get() = buffers[0] 17 | 18 | private fun createFramebuffers() = buildList { 19 | for (i in 0 until iterations) { 20 | val x = xSize shr i 21 | val y = ySize shr i 22 | if (x < 5 || y < 5) { 23 | logger.warn("Iterations too big for current size!") 24 | break 25 | } 26 | add(FrameBuffer(x, y, false)) 27 | } 28 | } 29 | 30 | override fun resize(xSize: Int, ySize: Int) { 31 | buffers.forEach { it.dispose() } 32 | this.xSize = xSize 33 | this.ySize = ySize 34 | buffers = createFramebuffers() 35 | } 36 | 37 | private fun blitNamed( 38 | from: Int, 39 | to: Int, 40 | x1: Int, 41 | y1: Int, 42 | x2: Int, 43 | y2: Int 44 | ) { 45 | GL30.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, from) 46 | GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, to) 47 | GL30.glBlitFramebuffer( 48 | 0, 0, 49 | x1, y1, 50 | 0, 0, 51 | x2, y2, 52 | GL30.GL_COLOR_BUFFER_BIT, 53 | GL30.GL_NEAREST 54 | ) 55 | } 56 | 57 | override fun apply(inputFramebuffer: Int) { 58 | blitNamed( 59 | inputFramebuffer, 60 | buffers[0].framebufferId, 61 | xSize, ySize, 62 | xSize, ySize 63 | ) 64 | for (i in 1 until buffers.size) { 65 | val from = buffers[i - 1] 66 | val to = buffers[i] 67 | blitNamed( 68 | from.framebufferId, 69 | to.framebufferId, 70 | from.xSize, from.ySize, 71 | to.xSize, to.xSize 72 | ) 73 | } 74 | for (i in (buffers.size - 1) downTo 1) { 75 | val from = buffers[i] 76 | val to = buffers[i - 1] 77 | blitNamed( 78 | from.framebufferId, 79 | to.framebufferId, 80 | from.xSize, from.ySize, 81 | to.xSize, to.xSize 82 | ) 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/providers/AEFluidKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering.providers 2 | 3 | import appeng.api.stacks.AEFluidKey 4 | import appeng.api.stacks.AEKeyType 5 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 6 | import net.minecraftforge.registries.ForgeRegistries 7 | 8 | object AEFluidKeyImageProvider : AEKeyImageProvider { 9 | override val keyType: AEKeyType = AEKeyType.fluids() 10 | 11 | override fun getAllEntries(): Iterable { 12 | return ForgeRegistries.FLUIDS.values.map { AEFluidKey.of(it) } 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/client/rendering/providers/AEItemKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.client.rendering.providers 2 | 3 | import appeng.api.stacks.AEItemKey 4 | import appeng.api.stacks.AEKeyType 5 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 6 | import net.minecraftforge.registries.ForgeRegistries 7 | 8 | object AEItemKeyImageProvider : AEKeyImageProvider { 9 | override val keyType: AEKeyType = AEKeyType.items() 10 | 11 | override fun getAllEntries(): Iterable = ForgeRegistries.ITEMS.map { AEItemKey.of(it) } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/compat/advancedae/AAECraftingCpuContainerImpl.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.compat.advancedae 2 | 3 | import appeng.api.config.CpuSelectionMode 4 | import appeng.api.networking.crafting.ICraftingCPU 5 | import appeng.api.stacks.AEKey 6 | import appeng.api.stacks.KeyCounter 7 | import appeng.menu.me.common.IncrementalUpdateHelper 8 | import appeng.menu.me.crafting.CraftingStatus 9 | import appeng.menu.me.crafting.CraftingStatusEntry 10 | import com.google.common.collect.ImmutableList 11 | import icu.takeneko.appwebterminal.api.crafting.CraftingCpuContainer 12 | import icu.takeneko.appwebterminal.api.crafting.CraftingCpuContainerHelper 13 | import net.minecraftforge.fml.ModList 14 | import net.minecraftforge.fml.loading.LoadingModList 15 | import net.pedroksl.advanced_ae.common.cluster.AdvCraftingCPU 16 | import net.pedroksl.advanced_ae.common.logic.AdvCraftingCPULogic 17 | import java.util.function.Consumer 18 | 19 | class AAECraftingCpuContainerImpl(private val cpu: AdvCraftingCPU) : CraftingCpuContainer { 20 | override fun unwrap(): ICraftingCPU = cpu 21 | 22 | override fun selectionMode(): CpuSelectionMode = cpu.selectionMode 23 | override fun cantStoreItems(): Boolean = cpu.craftingLogic.isCantStoreItems 24 | 25 | override fun createCraftingStatus(helper: IncrementalUpdateHelper): CraftingStatus { 26 | return createCraftingStatusInternal(helper, cpu.craftingLogic) 27 | } 28 | 29 | @Suppress("DEPRECATION") 30 | private fun createCraftingStatusInternal( 31 | changes: IncrementalUpdateHelper, 32 | logic: AdvCraftingCPULogic 33 | ): CraftingStatus { 34 | val full = changes.isFullUpdate() 35 | val newEntries = ImmutableList.builder() 36 | 37 | for (what in changes) { 38 | val storedCount = logic.getStored(what) 39 | val activeCount = logic.getWaitingFor(what) 40 | val pendingCount = logic.getPendingOutputs(what) 41 | var sentStack = what 42 | if (!full && changes.getSerial(what) != null) { 43 | sentStack = null 44 | } 45 | 46 | val entry = CraftingStatusEntry( 47 | changes.getOrAssignSerial(what), 48 | sentStack, 49 | storedCount, 50 | activeCount, 51 | pendingCount 52 | ) 53 | newEntries.add(entry) 54 | if (entry.isDeleted()) { 55 | changes.removeSerial(what) 56 | } 57 | } 58 | 59 | val elapsedTime = logic.getElapsedTimeTracker().elapsedTime 60 | val remainingItems = logic.getElapsedTimeTracker().remainingItemCount 61 | val startItems = logic.getElapsedTimeTracker().startItemCount 62 | return CraftingStatus( 63 | full, 64 | elapsedTime, 65 | remainingItems, 66 | startItems, 67 | newEntries.build() 68 | ) 69 | } 70 | 71 | override fun removeUpdateListener(cons: Consumer) { 72 | cpu.craftingLogic.removeListener(cons) 73 | } 74 | 75 | override fun addUpdateListener(cons: Consumer) { 76 | cpu.craftingLogic.addListener(cons) 77 | } 78 | 79 | override fun getAllItems(): KeyCounter { 80 | val keyCounter = KeyCounter() 81 | cpu.craftingLogic.getAllItems(keyCounter) 82 | return keyCounter 83 | } 84 | } 85 | 86 | fun registerAAECompat() { 87 | if (ModList.get().isLoaded("advanced_ae")) { 88 | CraftingCpuContainerHelper.registerConstructor( 89 | AdvCraftingCPU::class.java, 90 | object : CraftingCpuContainer.Constructor{ 91 | override fun create(cpu: AdvCraftingCPU): CraftingCpuContainer = AAECraftingCpuContainerImpl(cpu) 92 | } 93 | ) 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/compat/appbot/ManaKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.compat.appbot 2 | 3 | import appbot.ae2.ManaKey 4 | import appbot.ae2.ManaKeyType 5 | import appeng.api.stacks.AEKeyType 6 | import icu.takeneko.appwebterminal.api.ImageProvider 7 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 8 | 9 | @ImageProvider( 10 | value = "botania:mana", 11 | modid = "appbot" 12 | ) 13 | class ManaKeyImageProvider : AEKeyImageProvider { 14 | override val keyType: AEKeyType = ManaKeyType.TYPE 15 | 16 | override fun getAllEntries(): Iterable { 17 | return listOf(ManaKey.KEY as ManaKey) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/compat/appflux/FluxKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.compat.appflux 2 | 3 | import com.glodblock.github.appflux.common.me.key.FluxKey 4 | import com.glodblock.github.appflux.common.me.key.type.EnergyType 5 | import com.glodblock.github.appflux.common.me.key.type.FluxKeyType 6 | import icu.takeneko.appwebterminal.api.ImageProvider 7 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 8 | 9 | @ImageProvider( 10 | value = "appflux:flux", 11 | modid = "appflux" 12 | ) 13 | class FluxKeyImageProvider : AEKeyImageProvider { 14 | override val keyType: FluxKeyType = FluxKeyType.TYPE 15 | 16 | override fun getAllEntries(): Iterable { 17 | return EnergyType.entries.map { FluxKey.of(it)!! } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/compat/appmek/ChemicalKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.compat.appmek 2 | 3 | import appeng.api.stacks.AEKeyType 4 | import icu.takeneko.appwebterminal.api.ImageProvider 5 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 6 | import me.ramidzkh.mekae2.ae2.MekanismKey 7 | import me.ramidzkh.mekae2.ae2.MekanismKeyType 8 | import mekanism.api.MekanismAPI 9 | 10 | @ImageProvider( 11 | value = "appmek:chemical", 12 | modid = "appmek" 13 | ) 14 | class ChemicalKeyImageProvider : AEKeyImageProvider { 15 | override val keyType: AEKeyType = MekanismKeyType.TYPE 16 | 17 | override fun getAllEntries(): Iterable { 18 | return (MekanismAPI.gasRegistry() 19 | + MekanismAPI.infuseTypeRegistry() 20 | + MekanismAPI.pigmentRegistry() 21 | + MekanismAPI.slurryRegistry()).mapNotNull { 22 | MekanismKey.of(it.getStack(1000)) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/compat/arseng/SourceKeyImageProvider.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.compat.arseng 2 | 3 | import appeng.api.stacks.AEKeyType 4 | import gripe._90.arseng.me.key.SourceKey 5 | import gripe._90.arseng.me.key.SourceKeyType 6 | import icu.takeneko.appwebterminal.api.ImageProvider 7 | import icu.takeneko.appwebterminal.client.rendering.AEKeyImageProvider 8 | 9 | @ImageProvider( 10 | value = "arseng:source", 11 | modid = "arseng" 12 | ) 13 | class SourceKeyImageProvider : AEKeyImageProvider { 14 | override val keyType: AEKeyType = SourceKeyType.TYPE 15 | 16 | override fun getAllEntries(): Iterable { 17 | return listOf(SourceKey.KEY as SourceKey) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/data/DataGenerator.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.data 2 | 3 | import com.tterrag.registrate.providers.ProviderType 4 | import com.tterrag.registrate.providers.RegistrateLangProvider 5 | import dev.toma.configuration.config.value.ConfigValue 6 | import dev.toma.configuration.config.value.ObjectValue 7 | import icu.takeneko.appwebterminal.AppWebTerminal 8 | import icu.takeneko.appwebterminal.registrate 9 | import icu.takeneko.appwebterminal.util.toEnglishName 10 | import icu.takeneko.appwebterminal.util.toLowerCaseUnder 11 | 12 | fun configureDataGeneration() { 13 | registrate.addDataGenerator(ProviderType.LANG, ::handleLang) 14 | } 15 | 16 | fun handleLang(langProvider: RegistrateLangProvider) { 17 | langProvider.add("appwebterminal.screen.title", "ME Web Terminal") 18 | langProvider.add("appwebterminal.button.done", "Done") 19 | langProvider.add("appwebterminal.hint.name", "Name: ") 20 | langProvider.add("appwebterminal.hint.password", "Password: ") 21 | 22 | langProvider.add("appwebterminal.message.render_complete", "Render complete.") 23 | langProvider.add("appwebterminal.message.rendering", "A renderer is currently working.") 24 | langProvider.add("appwebterminal.message.started", "Started renderer.") 25 | 26 | langProvider.add("appwebterminal.gui.me_network_online", "Online") 27 | langProvider.add("appwebterminal.gui.me_network_offline", "Offline") 28 | 29 | langProvider.add("config.screen.${AppWebTerminal.MOD_ID}", "AppliedWebTerminal Settings") 30 | dfs(langProvider, mutableSetOf(), AppWebTerminal.configHolder.valueMap) 31 | } 32 | 33 | private fun dfs(provider: RegistrateLangProvider, added: MutableSet, map: Map>) { 34 | for ((_, value) in map) { 35 | val id = value.id 36 | if (added.add(id)) { 37 | provider.add("config.${AppWebTerminal.MOD_ID}.option.$id", id.toLowerCaseUnder().toEnglishName()) 38 | } 39 | if (value is ObjectValue) { 40 | dfs(provider, added, value.get()) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/networking/OpenWebTerminalScreenPacket.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.networking 2 | 3 | import icu.takeneko.appwebterminal.client.gui.WebTerminalScreen 4 | import net.minecraft.client.Minecraft 5 | import net.minecraft.core.BlockPos 6 | import net.minecraft.network.FriendlyByteBuf 7 | import net.minecraftforge.network.NetworkEvent 8 | import java.util.UUID 9 | import java.util.function.Supplier 10 | 11 | data class OpenWebTerminalScreenPacket( 12 | val initialName: String, 13 | val uuid: UUID, 14 | val blockPos: BlockPos, 15 | val password: String, 16 | val isOnline: Boolean 17 | ) { 18 | constructor(buf: FriendlyByteBuf) : this( 19 | buf.readUtf(), 20 | buf.readUUID(), 21 | buf.readBlockPos(), 22 | buf.readUtf(), 23 | buf.readBoolean() 24 | ) 25 | 26 | fun encode(buf: FriendlyByteBuf) { 27 | buf.writeUtf(initialName) 28 | buf.writeUUID(uuid) 29 | buf.writeBlockPos(blockPos) 30 | buf.writeUtf(password) 31 | buf.writeBoolean(isOnline) 32 | } 33 | 34 | fun accept(context: Supplier) { 35 | context.get().enqueueWork { 36 | Minecraft.getInstance().setScreen(WebTerminalScreen(initialName, uuid, password, isOnline)) 37 | } 38 | context.get().packetHandled = true 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/networking/UpdateWebTerminalNamePacket.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.networking 2 | 3 | import icu.takeneko.appwebterminal.support.AENetworkSupport 4 | import net.minecraft.network.FriendlyByteBuf 5 | import net.minecraftforge.network.NetworkEvent 6 | import java.util.UUID 7 | import java.util.function.Supplier 8 | 9 | data class UpdateWebTerminalNamePacket( 10 | val name: String, 11 | val uuid: UUID, 12 | val password: String 13 | ) { 14 | constructor(buf: FriendlyByteBuf) : this(buf.readUtf(), buf.readUUID(), buf.readUtf()) 15 | 16 | fun encode(buf: FriendlyByteBuf) { 17 | buf.writeUtf(name) 18 | buf.writeUUID(uuid) 19 | buf.writeUtf(password) 20 | } 21 | 22 | fun accept(context: Supplier) { 23 | context.get().enqueueWork { 24 | AENetworkSupport.update(uuid, name, password) 25 | } 26 | context.get().packetHandled = true 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/resource/LanguageFileDownloader.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.resource 2 | 3 | import com.mojang.logging.LogUtils 4 | import icu.takeneko.appwebterminal.config.MinecraftAssetsApi 5 | import io.ktor.util.* 6 | import kotlinx.coroutines.future.await 7 | import kotlinx.coroutines.runBlocking 8 | import java.util.regex.Pattern 9 | 10 | class LanguageFileDownloader(val assetsSource: MinecraftAssetsApi) : Thread("LanguageFileDownloader") { 11 | private val logger = LogUtils.getLogger() 12 | private val minecraftVersion = MinecraftVersion(assetsSource) 13 | override fun run() { 14 | runBlocking { 15 | CacheProvider.init() 16 | logger.info("Updating minecraft versions.") 17 | minecraftVersion.update().whenComplete { _, u -> 18 | if (u != null) { 19 | logger.error("Failed to update Minecraft versions.", u) 20 | } 21 | }.await() 22 | logger.info("Resolving version asset indexes.") 23 | val versionAssetIndex = minecraftVersion.resolveVersionAssetIndex("1.20.1")!!.whenComplete { t, u -> 24 | if (u != null) { 25 | logger.error("Failed to resolve version assetIndex.", u) 26 | } 27 | }.await() 28 | versionAssetIndex.objects.entries.forEach { (name, obj) -> 29 | if (languageFilePattern.matcher(name).matches()) { 30 | val encodedName = name.encodeBase64() 31 | fileNameMapping[name] = encodedName 32 | logger.info("Downloading $name to $encodedName") 33 | for (i in 0 until 3) { 34 | try { 35 | CacheProvider.downloadFile( 36 | FileMetadata( 37 | encodedName, 38 | assetsSource.urlAssetsReplacer.apply(obj.downloadUrl), 39 | obj.size, 40 | obj.hash 41 | ) 42 | ) 43 | break 44 | } catch (e: Exception) { 45 | logger.error("Failed to download $name to $encodedName", e) 46 | continue 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | companion object { 55 | private val languageFilePattern = Pattern.compile("minecraft/lang/(.+).json") 56 | val fileNameMapping = mutableMapOf() 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/resource/MinecraftVersion.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.resource 2 | 3 | import icu.takeneko.appwebterminal.config.MinecraftAssetsApi 4 | import io.ktor.http.* 5 | import io.ktor.util.* 6 | import kotlinx.serialization.json.Json 7 | import java.net.http.HttpClient 8 | import java.net.http.HttpRequest 9 | import java.net.http.HttpResponse.BodyHandlers 10 | import java.util.concurrent.CompletableFuture 11 | 12 | class MinecraftVersion(val assetsSource: MinecraftAssetsApi) { 13 | private val mojangApiUrl = assetsSource.endpoint 14 | private val Json = Json { 15 | ignoreUnknownKeys = true 16 | } 17 | private val versionManifestUrl = "$mojangApiUrl/mc/game/version_manifest_v2.json" 18 | lateinit var versionManifest: VersionManifest 19 | private val httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build() 20 | val versions = mutableMapOf() 21 | 22 | fun update(): CompletableFuture { 23 | val request = HttpRequest.newBuilder().GET().uri(Url(versionManifestUrl).toURI()).build() 24 | return httpClient.sendAsync(request, BodyHandlers.ofString()).thenApply { 25 | val resp = it.body() 26 | versionManifest = Json.decodeFromString(resp) 27 | for (version in versionManifest.versions) { 28 | versions[version.id] = version 29 | } 30 | return@thenApply versionManifest 31 | } 32 | } 33 | 34 | fun resolveVersionMetadata(versionName: String): CompletableFuture? { 35 | val version = versions[versionName] ?: return null 36 | val request = HttpRequest.newBuilder().GET().uri(Url(version.url).toURI()).build() 37 | return httpClient.sendAsync(request, BodyHandlers.ofString()).thenApply { 38 | val resp = it.body() 39 | return@thenApply Json.decodeFromString(resp) 40 | } 41 | } 42 | 43 | fun resolveVersionAssetIndex(version: String): CompletableFuture? { 44 | val versionMetadataFuture = resolveVersionMetadata(version) ?: return null 45 | return versionMetadataFuture.thenApply { 46 | HttpRequest.newBuilder().GET().uri(Url(assetsSource.urlIndexReplacer.apply(it.assetIndex.url)).toURI()).build() 47 | }.thenCompose { 48 | httpClient.sendAsync(it, BodyHandlers.ofString()) 49 | }.thenApply { 50 | return@thenApply Json.decodeFromString(it.body()) 51 | } 52 | } 53 | } 54 | 55 | @kotlinx.serialization.Serializable 56 | data class LatestData(val release: String, val snapshot: String) 57 | 58 | enum class VersionType { 59 | @kotlinx.serialization.SerialName("snapshot") 60 | SNAPSHOT, 61 | 62 | @kotlinx.serialization.SerialName("release") 63 | RELEASE, 64 | 65 | @kotlinx.serialization.SerialName("old_alpha") 66 | OLD_ALPHA, 67 | 68 | @kotlinx.serialization.SerialName("old_beta") 69 | OLD_BETA, 70 | } 71 | 72 | @kotlinx.serialization.Serializable 73 | data class AssetIndex( 74 | val objects: Map 75 | ) 76 | 77 | @kotlinx.serialization.Serializable 78 | data class AssetObject( 79 | val hash: String, 80 | val size: Long 81 | ){ 82 | val downloadUrl:String 83 | get() = "https://resources.download.minecraft.net/${hash.substring(0..1)}/$hash" 84 | } 85 | 86 | @kotlinx.serialization.Serializable 87 | data class VersionData( 88 | val id: String, 89 | val type: VersionType, 90 | val url: String, 91 | val releaseTime: String, 92 | val time: String 93 | ) 94 | 95 | @kotlinx.serialization.Serializable 96 | data class VersionMetadata( 97 | val assetIndex: AssetIndexMetadata 98 | ) 99 | 100 | @kotlinx.serialization.Serializable 101 | data class AssetIndexMetadata( 102 | val sha1: String, 103 | val size: Int, 104 | val url: String, 105 | val id: Int 106 | ) 107 | 108 | @kotlinx.serialization.Serializable 109 | data class VersionManifest(val latest: LatestData, val versions: MutableList) -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/AENetworkAccess.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support 2 | 3 | import appeng.api.networking.IGrid 4 | import net.minecraft.core.BlockPos 5 | import net.minecraft.world.level.Level 6 | import java.util.UUID 7 | 8 | interface AENetworkAccess { 9 | fun getId(): UUID 10 | 11 | fun worldPosition(): BlockPos 12 | 13 | fun markDirty() 14 | 15 | fun getGrid(): IGrid? 16 | 17 | fun auth(password: String): Boolean 18 | 19 | fun update(displayName: String, password: String):Boolean 20 | 21 | fun validateNonce(nonce: String): Boolean 22 | 23 | fun getNonce(): String 24 | 25 | fun getTerminalName(): String 26 | 27 | fun level(): Level? 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/AENetworkSupport.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support 2 | 3 | import appeng.api.networking.IGrid 4 | import appeng.api.networking.security.IActionHost 5 | import icu.takeneko.appwebterminal.support.http.websocket.WebsocketSession 6 | import kotlinx.coroutines.runBlocking 7 | import net.minecraft.world.level.Level 8 | import org.slf4j.LoggerFactory 9 | import java.util.UUID 10 | 11 | object AENetworkSupport { 12 | val accessors: MutableMap = mutableMapOf() 13 | private val logger = LoggerFactory.getLogger("AE Network Support") 14 | private val sessions = mutableMapOf>() 15 | 16 | fun register(accessor: AENetworkAccess) { 17 | logger.info("Registering $accessor(${accessor.getId()})") 18 | accessors[accessor.getId()] = accessor 19 | } 20 | 21 | fun remove(accessor: AENetworkAccess) { 22 | if (accessor !in accessors.values) return 23 | logger.info("Unegistering $accessor(${accessor.getId()})") 24 | accessors.remove(accessor.getId()) 25 | } 26 | 27 | fun notifySessionStarted(session: WebsocketSession) { 28 | logger.info("Starting new websocket session of {}({}).", session.owner, session.owner.getId()) 29 | sessions.computeIfAbsent(session.owner) { mutableListOf() } += session 30 | } 31 | 32 | fun notifySessionTerminated(session: WebsocketSession) { 33 | logger.info("Terminating websocket session $session of ${session.owner}") 34 | sessions.computeIfAbsent(session.owner) { mutableListOf() } -= session 35 | } 36 | 37 | fun tick() { 38 | synchronized(sessions) { 39 | sessions.values.flatten().forEach { 40 | it.tick() 41 | } 42 | } 43 | } 44 | 45 | fun update(uuid: UUID, name: String, password: String) { 46 | if ((accessors[uuid] ?: return).update(name, password)) { 47 | logger.info("Updating $uuid password into $password") 48 | logger.info("Updating $uuid nonce into ${accessors[uuid]?.getNonce()}") 49 | } 50 | accessors[uuid]?.markDirty() 51 | logger.info("Renaming $uuid into $name") 52 | } 53 | 54 | fun requestSessionReset(accessor: AENetworkAccess) { 55 | val sessions = sessions.computeIfAbsent(accessor) { mutableListOf() }.toList() 56 | sessions.forEach { it.close() } 57 | this.sessions.computeIfAbsent(accessor) { mutableListOf() }.clear() 58 | } 59 | 60 | fun listAllTerminals(): List { 61 | return accessors.map { TerminalInfo(it.value.getTerminalName(), it.value.getId().toString()) } 62 | } 63 | 64 | fun validateNonce(uuid: UUID, nonce: String): Boolean { 65 | return (accessors[uuid] ?: return false).validateNonce(nonce) 66 | } 67 | 68 | fun reset() { 69 | accessors.clear() 70 | } 71 | 72 | fun auth(uuid: UUID, password: String): Boolean { 73 | return if (uuid in accessors) { 74 | accessors[uuid]!!.auth(password) 75 | } else { 76 | false 77 | } 78 | } 79 | 80 | fun getNonce(uuid: UUID): String { 81 | return (accessors[uuid] ?: throw IllegalArgumentException("No such accessor owns uuid $uuid")).getNonce() 82 | } 83 | 84 | fun getActionHost(uuid: UUID): IActionHost? { 85 | return (accessors[uuid] ?: return null) as? IActionHost 86 | } 87 | 88 | fun getGrid(uuid: UUID): IGrid? { 89 | return (accessors[uuid] ?: return null).getGrid() 90 | } 91 | 92 | fun getLevel(uuid: UUID): Level? { 93 | return (accessors[uuid] ?: return null).level() 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/Data.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support 2 | 3 | import appeng.api.networking.crafting.CraftingJobStatus 4 | import appeng.api.networking.crafting.ICraftingCPU 5 | import appeng.api.stacks.AEKey 6 | import appeng.api.stacks.AEKeyType 7 | import appeng.api.stacks.GenericStack 8 | import appeng.api.stacks.KeyCounter 9 | import appeng.menu.me.crafting.CraftingStatus 10 | import appeng.menu.me.crafting.CraftingStatusEntry 11 | import icu.takeneko.appwebterminal.support.AEKeyObject.Companion.serializable 12 | import icu.takeneko.appwebterminal.support.MECraftingJobStatusBundle.Companion.serializable 13 | import icu.takeneko.appwebterminal.support.MECraftingStatusEntry.Companion.bundle 14 | import icu.takeneko.appwebterminal.support.MEStack.Companion.meStack 15 | import icu.takeneko.appwebterminal.util.ComponentSerializer 16 | import icu.takeneko.appwebterminal.util.ResourceLocationSerializer 17 | import icu.takeneko.appwebterminal.util.strip 18 | import net.minecraft.network.chat.Component 19 | import net.minecraft.resources.ResourceLocation 20 | import java.util.Objects 21 | 22 | @kotlinx.serialization.Serializable 23 | data class MECraftingStatusBundle( 24 | val fullStatus: Boolean, 25 | val elapsedTime: Long, 26 | val remainingItemCount: Long, 27 | val startItemCount: Long, 28 | val entries: List 29 | ) { 30 | companion object { 31 | val CraftingStatus.bundle: MECraftingStatusBundle? 32 | get() = if(this.entries.isEmpty()) null else MECraftingStatusBundle( 33 | this.isFullStatus, 34 | this.elapsedTime, 35 | this.remainingItemCount, 36 | this.startItemCount, 37 | this.entries.map { it.bundle } 38 | ) 39 | } 40 | } 41 | 42 | @kotlinx.serialization.Serializable 43 | data class MECraftingStatusEntry( 44 | val serial: Long, 45 | val what: AEKeyObject?, 46 | val storedAmount: Long, 47 | val activeAmount: Long, 48 | val pendingAmount: Long 49 | ) { 50 | companion object { 51 | val CraftingStatusEntry.bundle: MECraftingStatusEntry 52 | get() = MECraftingStatusEntry( 53 | this.serial, 54 | this.what?.serializable(), 55 | this.storedAmount, 56 | this.activeAmount, 57 | this.pendingAmount 58 | ) 59 | } 60 | } 61 | 62 | @kotlinx.serialization.Serializable 63 | data class MECpuStatusBundle( 64 | val id: Int, 65 | @kotlinx.serialization.Serializable(with = ComponentSerializer::class) 66 | val name: Component?, 67 | val busy: Boolean, 68 | val storageSize: Long, 69 | val coProcessorCount: Int, 70 | val craftingStatus: MECraftingJobStatusBundle? 71 | ) { 72 | companion object { 73 | fun ICraftingCPU.asStatus(id: Int): MECpuStatusBundle { 74 | return MECpuStatusBundle( 75 | id, 76 | this.name, 77 | this.isBusy, 78 | this.availableStorage, 79 | this.coProcessors, 80 | this.jobStatus?.serializable() 81 | ) 82 | } 83 | } 84 | } 85 | 86 | @kotlinx.serialization.Serializable 87 | data class MECraftingJobStatusBundle( 88 | val crafting: MEStack, 89 | val totalItems: Long, 90 | val progress: Long, 91 | val elapsedTimeNanos: Long 92 | ) { 93 | companion object { 94 | fun CraftingJobStatus.serializable() = MECraftingJobStatusBundle( 95 | this.crafting.meStack, 96 | this.totalItems, 97 | this.progress, 98 | this.elapsedTimeNanos 99 | ) 100 | } 101 | } 102 | 103 | @kotlinx.serialization.Serializable 104 | data class MEStack( 105 | val what: AEKeyObject, 106 | val amount: Long, 107 | var craftable: Boolean = false, 108 | ) { 109 | companion object { 110 | val GenericStack.meStack: MEStack 111 | get() = MEStack(this.what.serializable(), this.amount) 112 | val KeyCounter.meStacks: List 113 | get() = this.map { MEStack(it.key.serializable(), it.longValue) } 114 | } 115 | } 116 | 117 | @kotlinx.serialization.Serializable 118 | data class AEKeyTypeObject( 119 | @kotlinx.serialization.Serializable(with = ResourceLocationSerializer::class) 120 | val id: ResourceLocation, 121 | @kotlinx.serialization.Serializable(with = ComponentSerializer::class) 122 | val description: Component, 123 | ) { 124 | companion object { 125 | fun AEKeyType.serializable(): AEKeyTypeObject { 126 | return AEKeyTypeObject( 127 | this.id, 128 | this.description.strip() 129 | ) 130 | } 131 | } 132 | } 133 | 134 | @kotlinx.serialization.Serializable 135 | data class AEKeyObject( 136 | @kotlinx.serialization.Serializable(with = ResourceLocationSerializer::class) 137 | val id: ResourceLocation, 138 | @kotlinx.serialization.Serializable(with = ComponentSerializer::class) 139 | val displayName: Component, 140 | @kotlinx.serialization.Serializable(with = ResourceLocationSerializer::class) 141 | val type: ResourceLocation, 142 | ) { 143 | companion object { 144 | fun AEKey.serializable(): AEKeyObject { 145 | return AEKeyObject( 146 | this.id, 147 | this.displayName.strip(), 148 | this.type.id 149 | ) 150 | } 151 | } 152 | 153 | fun myHash(): Int { 154 | return Objects.hash(this.id, this.type) 155 | } 156 | } 157 | 158 | @kotlinx.serialization.Serializable 159 | data class TerminalInfo(val name: String, val uuid: String) 160 | 161 | @kotlinx.serialization.Serializable 162 | data class PageMeta(val total: Int, val page: Int, val limit: Int, val totalPages: Int) 163 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/MECraftingServiceView.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support 2 | 3 | import appeng.api.config.CpuSelectionMode 4 | import appeng.api.networking.IGrid 5 | import appeng.api.networking.crafting.ICraftingCPU 6 | import appeng.api.networking.crafting.ICraftingService 7 | import appeng.api.stacks.KeyCounter 8 | import appeng.me.cluster.implementations.CraftingCPUCluster 9 | import appeng.menu.me.common.IncrementalUpdateHelper 10 | import appeng.menu.me.crafting.CraftingStatus 11 | import com.google.common.collect.ImmutableSet 12 | import icu.takeneko.appwebterminal.api.crafting.CraftingCpuContainer 13 | import icu.takeneko.appwebterminal.api.crafting.CraftingCpuContainerHelper 14 | import icu.takeneko.appwebterminal.support.MECpuStatusBundle.Companion.asStatus 15 | import icu.takeneko.appwebterminal.support.MECraftingStatusBundle.Companion.bundle 16 | import icu.takeneko.appwebterminal.support.http.websocket.MECraftingServiceStatusBundle 17 | 18 | class MECraftingServiceView( 19 | val grid: IGrid, 20 | ) { 21 | private val craftingService: ICraftingService = grid.craftingService 22 | private var allCpus: ImmutableSet = craftingService.cpus 23 | private var cpuId = 1 24 | private val cpuMap = mutableMapOf() 25 | private var selectedCpuId = -1 26 | private var currentCpu: CraftingCpuContainer? = null 27 | private val incrementalUpdateHelper = IncrementalUpdateHelper() 28 | private val cpuUpdateListener = incrementalUpdateHelper::addChange 29 | private var cpuSchedulingMode = CpuSelectionMode.ANY 30 | private var craftingStatus: CraftingStatus? = null 31 | private var cantStoreItems: Boolean = false 32 | private var fullUpgradeSent = false 33 | 34 | init { 35 | allCpus.forEach { 36 | cpuMap[cpuId++] = it 37 | } 38 | } 39 | 40 | fun tick() { 41 | if (allCpus != craftingService.cpus) { 42 | allCpus = craftingService.cpus 43 | allCpus.forEach { 44 | if (it !in cpuMap.values) { 45 | cpuMap[cpuId++] = it 46 | } 47 | } 48 | cpuMap.entries.toList().forEach { (k, v) -> 49 | if (v !in allCpus) { 50 | cpuMap.remove(k) 51 | } 52 | } 53 | if (currentCpu?.unwrap() !in allCpus) { 54 | currentCpu = null 55 | craftingStatus = null 56 | } 57 | } 58 | if (this.currentCpu != null && fullUpgradeSent) { 59 | this.cpuSchedulingMode = this.currentCpu!!.selectionMode() 60 | this.cantStoreItems = this.currentCpu!!.cantStoreItems(); 61 | this.craftingStatus = this.currentCpu!!.createCraftingStatus(this.incrementalUpdateHelper) 62 | this.incrementalUpdateHelper.commitChanges() 63 | } 64 | } 65 | 66 | fun selectCpu(id: Int) { 67 | if (id == -1) { 68 | this.selectedCpuId = -1 69 | this.currentCpu = null 70 | this.currentCpu?.removeUpdateListener(cpuUpdateListener) 71 | this.incrementalUpdateHelper.reset() 72 | this.fullUpgradeSent = false 73 | this.craftingStatus = null 74 | } 75 | this.cpuMap[id]?.let { 76 | this.selectedCpuId = id 77 | selectCpu(it) 78 | } 79 | } 80 | 81 | private fun selectCpu(cpu: ICraftingCPU) { 82 | this.currentCpu?.removeUpdateListener(cpuUpdateListener) 83 | this.incrementalUpdateHelper.reset() 84 | this.fullUpgradeSent = false 85 | this.craftingStatus = null 86 | val selection = CraftingCpuContainerHelper.create(cpu) 87 | if (selection != null) { 88 | this.currentCpu = selection 89 | val keyCounter = this.currentCpu!!.getAllItems() 90 | keyCounter.forEach { 91 | this.incrementalUpdateHelper.addChange(it.key) 92 | } 93 | this.currentCpu!!.addUpdateListener(cpuUpdateListener) 94 | this.craftingStatus = selection.createCraftingStatus(this.incrementalUpdateHelper) 95 | } else { 96 | this.currentCpu = null 97 | } 98 | } 99 | 100 | public fun getCpu(id: Int): ICraftingCPU? { 101 | return cpuMap[id] 102 | } 103 | 104 | fun createUpdateMessage(): MECraftingServiceStatusBundle { 105 | val data = MECraftingServiceStatusBundle( 106 | cpuMap.map { (k, v) -> v.asStatus(k) }, 107 | craftingStatus?.bundle 108 | ) 109 | if (data.craftingStatus?.entries != null) { 110 | fullUpgradeSent = true 111 | } 112 | return data 113 | } 114 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/Application.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http 2 | 3 | import icu.takeneko.appwebterminal.support.http.plugins.configureHTTP 4 | import icu.takeneko.appwebterminal.support.http.plugins.configureMonitoring 5 | import icu.takeneko.appwebterminal.support.http.plugins.configureSecurity 6 | import icu.takeneko.appwebterminal.support.http.plugins.configureSerialization 7 | import icu.takeneko.appwebterminal.support.http.plugins.configureSockets 8 | import icu.takeneko.appwebterminal.support.http.routing.configureRouting 9 | import io.ktor.server.application.* 10 | import io.ktor.server.cio.* 11 | import io.ktor.server.engine.* 12 | import org.slf4j.LoggerFactory 13 | 14 | class HttpServer( 15 | private val port: Int 16 | ) : Thread("WebTerminalHttpServer") { 17 | val logger = LoggerFactory.getLogger("HttpServer") 18 | val server = embeddedServer( 19 | CIO, 20 | port = port, 21 | host = "0.0.0.0", 22 | module = Application::module 23 | ) 24 | 25 | override fun run() { 26 | try { 27 | logger.info("Starting server at port $port") 28 | server.start(true) 29 | } catch (e: Throwable) { 30 | if (e !is InterruptedException) { 31 | logger.error("Unable to launch http server.", e) 32 | } 33 | } 34 | } 35 | 36 | fun gracefullyStop() { 37 | logger.info("Stopping http server because minecraft server is stopping.") 38 | server.stop() 39 | } 40 | } 41 | 42 | fun Application.module() { 43 | configureSerialization() 44 | configureMonitoring() 45 | configureHTTP() 46 | configureSecurity() 47 | configureSockets() 48 | configureRouting() 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/HttpServerLifecycleSupport.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http 2 | 3 | object HttpServerLifecycleSupport { 4 | var serverInstance: HttpServer? = null 5 | 6 | fun launch(port: Int) { 7 | serverInstance?.interrupt() 8 | serverInstance = HttpServer(port) 9 | serverInstance!!.start() 10 | } 11 | 12 | fun stop() { 13 | serverInstance?.gracefullyStop() 14 | serverInstance = null 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/plugins/HTTP.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.http.content.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.plugins.cachingheaders.* 7 | import io.ktor.server.plugins.cors.routing.* 8 | import io.ktor.server.plugins.forwardedheaders.* 9 | 10 | fun Application.configureHTTP() { 11 | install(ForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy 12 | install(XForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy 13 | install(CORS) { 14 | allowMethod(HttpMethod.Options) 15 | allowMethod(HttpMethod.Put) 16 | allowMethod(HttpMethod.Delete) 17 | allowMethod(HttpMethod.Patch) 18 | allowMethod(HttpMethod.Post) 19 | allowMethod(HttpMethod.Get) 20 | allowHeader(HttpHeaders.Authorization) 21 | allowHeader(HttpHeaders.ContentType) 22 | anyHost() // @TODO: Don't do this in production if possible. Try to limit it. 23 | } 24 | install(CachingHeaders) { 25 | options { call, outgoingContent -> 26 | when (outgoingContent.contentType?.withoutParameters()) { 27 | ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60)) 28 | ContentType.Image.PNG -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 7 * 24 * 60 * 60)) 29 | else -> null 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/plugins/Monitoring.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.plugins 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.plugins.calllogging.* 5 | import io.ktor.server.request.* 6 | import org.slf4j.event.Level 7 | 8 | fun Application.configureMonitoring() { 9 | install(CallLogging) { 10 | level = Level.INFO 11 | filter { call -> call.request.path().startsWith("/") } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/plugins/Security.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.plugins 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.algorithms.Algorithm 5 | import com.auth0.jwt.interfaces.Payload 6 | import icu.takeneko.appwebterminal.support.AENetworkSupport 7 | import icu.takeneko.appwebterminal.util.queryJwt 8 | import io.ktor.http.* 9 | import io.ktor.server.application.* 10 | import io.ktor.server.auth.* 11 | import io.ktor.server.auth.jwt.* 12 | import io.ktor.server.response.* 13 | import io.ktor.util.* 14 | import org.slf4j.LoggerFactory 15 | import java.util.UUID 16 | 17 | const val jwtAudience = "WebTerminalFrontend" 18 | val jwtSecret = generateNonce() 19 | private val logger = LoggerFactory.getLogger("configureSecurity") 20 | 21 | private val jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)) 22 | .withAudience(jwtAudience) 23 | .withIssuer("AppliedWebTerminal") 24 | .build() 25 | 26 | fun Application.configureSecurity() { 27 | // Please read the jwt property from the config file if you are using EngineMain 28 | logger.info("Using $jwtSecret as JWT secret.") 29 | install(Authentication) { 30 | jwt("jwt") { 31 | realm = "AppliedWebTerminal" 32 | verifier(jwtVerifier) 33 | validate { credential -> 34 | if (credential.payload.audience.contains(jwtAudience) && validateJwt(credential.payload)) 35 | Principal( 36 | credential.payload, 37 | UUID.fromString(credential.payload.getClaim("uuid").asString()), 38 | credential.payload.getClaim("nonce").asString() 39 | ) 40 | else 41 | null 42 | } 43 | challenge { defaultScheme, realm -> 44 | call.respond( 45 | HttpStatusCode.Unauthorized, 46 | "Token is not valid or has expired" 47 | ) 48 | } 49 | } 50 | queryJwt("query_jwt") { 51 | verifier( 52 | JWT.require(Algorithm.HMAC256(jwtSecret)) 53 | .withAudience(jwtAudience) 54 | .withIssuer("AppliedWebTerminal") 55 | .build() 56 | ) 57 | validate { credential -> 58 | if (credential.payload.audience.contains(jwtAudience) && validateJwt(credential.payload)) 59 | Principal( 60 | credential.payload, 61 | UUID.fromString(credential.payload.getClaim("uuid").asString()), 62 | credential.payload.getClaim("nonce").asString() 63 | ) 64 | else 65 | null 66 | } 67 | challenge { 68 | call.respond( 69 | HttpStatusCode.Unauthorized, 70 | "Token is not valid or has expired" 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | 77 | fun validateJwt(payload: Payload): Boolean { 78 | val uuidClaim = payload.getClaim("uuid") 79 | val nonceClaim = payload.getClaim("nonce") 80 | if (uuidClaim.isNull || nonceClaim.isNull) return false 81 | return try { 82 | val uuid = UUID.fromString(uuidClaim.asString()) 83 | val nonce = nonceClaim.asString() 84 | AENetworkSupport.validateNonce(uuid, nonce) 85 | } catch (e: IllegalArgumentException) { 86 | logger.warn("Could not validate jwt token: ", e) 87 | false 88 | } 89 | } 90 | 91 | class Principal( 92 | payload: Payload, 93 | val uuid: UUID, 94 | val nonce: String 95 | ) : JWTPayloadHolder(payload) -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/plugins/Serialization.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.plugins 2 | 3 | import io.ktor.serialization.kotlinx.json.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.contentnegotiation.* 6 | import io.ktor.server.response.* 7 | import io.ktor.server.routing.* 8 | 9 | fun Application.configureSerialization() { 10 | install(ContentNegotiation) { 11 | json() 12 | } 13 | routing { 14 | get("/json/kotlinx-serialization") { 15 | call.respond(mapOf("hello" to "world")) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/plugins/Sockets.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.plugins 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.auth.* 5 | import io.ktor.server.routing.* 6 | import io.ktor.server.websocket.* 7 | import io.ktor.websocket.* 8 | import kotlin.time.Duration.Companion.seconds 9 | 10 | fun Application.configureSockets() { 11 | install(WebSockets) { 12 | pingPeriod = 15.seconds 13 | timeout = 15.seconds 14 | maxFrameSize = Long.MAX_VALUE 15 | masking = false 16 | } 17 | routing { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/routing/FrontendSupportRouting.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.routing 2 | 3 | import icu.takeneko.appwebterminal.AppWebTerminal 4 | import icu.takeneko.appwebterminal.util.I18nUtil 5 | import icu.takeneko.appwebterminal.util.MinecraftI18nSupport 6 | import icu.takeneko.appwebterminal.util.ServerI18nSupport 7 | import icu.takeneko.appwebterminal.util.staticResourceForModContainer 8 | import io.ktor.http.* 9 | import io.ktor.server.application.* 10 | import io.ktor.server.response.* 11 | import io.ktor.server.routing.* 12 | import java.io.File 13 | import kotlin.io.path.Path 14 | import kotlin.io.path.div 15 | import kotlin.io.path.notExists 16 | 17 | fun Application.configureFrontendSupportRouting() { 18 | routing { 19 | staticResourceForModContainer() 20 | get("/settings") { 21 | return@get call.respond( 22 | FrontendSettings( 23 | AppWebTerminal.config.frontendTitle, 24 | AppWebTerminal.config.backendWebsocketEndpoint 25 | ) 26 | ) 27 | } 28 | get("/aeResource/{keyType}/{key...}") { 29 | val keyType = call.parameters["keyType"] ?: return@get 30 | val key = call.parameters.getAll("key")?.joinToString(File.separator) ?: return@get 31 | val filePath = Path("./aeKeyResources") / 32 | keyType.replace(":", "_") / 33 | "$key.png".replace(":", "_") 34 | if (filePath.notExists() || !filePath.normalize().toString().startsWith("aeKeyResources")) { 35 | return@get call.respond(HttpStatusCode.NotFound) 36 | } 37 | return@get call.respondFile(filePath.toFile()) 38 | } 39 | get("/translate/{language}/{key...}") { 40 | val language = call.parameters["language"] ?: return@get call.respond( 41 | HttpStatusCode.BadRequest, 42 | "Expected parameter 'language'" 43 | ) 44 | val key = call.parameters.getAll("key")?.joinToString(File.separator) 45 | ?: return@get call.respond( 46 | HttpStatusCode.BadRequest, 47 | "Expected parameter 'key'" 48 | ) 49 | return@get call.respond(I18nUtil.get(language, key)) 50 | } 51 | } 52 | } 53 | 54 | @kotlinx.serialization.Serializable 55 | private data class FrontendSettings(val title: String, val webSocketUrl: String) -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/routing/Routing.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.routing 2 | 3 | import io.ktor.server.application.* 4 | 5 | fun Application.configureRouting() { 6 | configureFrontendSupportRouting() 7 | configureAEServiceRouting() 8 | configureSecuritySupportRouting() 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/routing/SecuritySupportRouting.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.routing 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.algorithms.Algorithm 5 | import icu.takeneko.appwebterminal.support.AENetworkSupport 6 | import icu.takeneko.appwebterminal.support.http.plugins.Principal 7 | import icu.takeneko.appwebterminal.support.http.plugins.jwtAudience 8 | import icu.takeneko.appwebterminal.support.http.plugins.jwtSecret 9 | import io.ktor.server.application.* 10 | import io.ktor.server.auth.* 11 | import io.ktor.server.request.* 12 | import io.ktor.server.response.* 13 | import io.ktor.server.routing.* 14 | import java.time.ZoneOffset 15 | import java.time.format.DateTimeFormatter 16 | import java.util.Date 17 | import java.util.UUID 18 | 19 | fun Application.configureSecuritySupportRouting() { 20 | routing { 21 | post("/login") { 22 | return@post try { 23 | val user = call.receive() 24 | val uuid = UUID.fromString(user.uuid) 25 | if (AENetworkSupport.auth(uuid, user.password)) { 26 | val token = JWT.create() 27 | .withAudience(jwtAudience) 28 | .withIssuer("AppliedWebTerminal") 29 | .withClaim("uuid", user.uuid) 30 | .withClaim("nonce", AENetworkSupport.getNonce(uuid)) 31 | .withExpiresAt(Date(System.currentTimeMillis() + 6000000)) 32 | .sign(Algorithm.HMAC256(jwtSecret)) 33 | call.respond(UserAuthResult(success = true, payload = token)) 34 | } else { 35 | call.respond(UserAuthResult(success = false, payload = "Authentication failed.")) 36 | } 37 | } catch (e: IllegalArgumentException) { 38 | call.respond(UserAuthResult(success = false, payload = e.message)) 39 | } 40 | } 41 | authenticate("jwt") { 42 | get("/validate") { 43 | val principal = call.principal() 44 | return@get call.respond( 45 | if (principal != null) { 46 | val uuid = principal.payload.getClaim("uuid").asString() 47 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX") 48 | val expiresAt = principal.expiresAt?.toInstant()?.atZone(ZoneOffset.UTC)?.format(formatter) 49 | ValidateResult(true, uuid, expiresAt) 50 | } else { 51 | ValidateResult(false, null, null) 52 | } 53 | ) 54 | } 55 | } 56 | 57 | } 58 | } 59 | 60 | @kotlinx.serialization.Serializable 61 | private data class ValidateResult(val success: Boolean, val uuid: String?, val expiresAt: String?) 62 | 63 | @kotlinx.serialization.Serializable 64 | private data class UserCredential(val uuid: String, val password: String) 65 | 66 | @kotlinx.serialization.Serializable 67 | private data class UserAuthResult(val success: Boolean, val payload: String?) -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/websocket/Protocol.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.websocket 2 | 3 | import icu.takeneko.appwebterminal.support.MECpuStatusBundle 4 | import icu.takeneko.appwebterminal.support.MECraftingStatusBundle 5 | 6 | interface Protocol { 7 | fun type(): String 8 | fun accept(session: WebsocketSession) { 9 | } 10 | } 11 | 12 | @kotlinx.serialization.Serializable 13 | data class SetUpdateInterval(val value: Int) : Protocol { 14 | override fun type(): String = "update_interval" 15 | 16 | override fun accept(session: WebsocketSession) { 17 | session.updateInterval = value 18 | session.updateCountdown = 0 19 | } 20 | } 21 | 22 | @kotlinx.serialization.Serializable 23 | data class SelectCpu(val cpuId: Int) : Protocol { 24 | override fun type(): String = "select_cpu" 25 | 26 | override fun accept(session: WebsocketSession) { 27 | session.craftingServiceView.selectCpu(cpuId) 28 | session.updateCountdown = 0 29 | } 30 | } 31 | 32 | @kotlinx.serialization.Serializable 33 | data class CancelJob(val cpuId: Int) : Protocol { 34 | override fun type(): String = "cancel_job" 35 | override fun accept(session: WebsocketSession) { 36 | session.craftingServiceView.getCpu(cpuId)?.cancelJob() 37 | } 38 | } 39 | 40 | @kotlinx.serialization.Serializable 41 | data class MECraftingServiceStatusBundle( 42 | val cpus: List, 43 | var craftingStatus: MECraftingStatusBundle? 44 | ) : Protocol { 45 | override fun type(): String = "status" 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/support/http/websocket/WebsocketSession.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.support.http.websocket 2 | 3 | import appeng.api.networking.IGrid 4 | import icu.takeneko.appwebterminal.support.AENetworkAccess 5 | import icu.takeneko.appwebterminal.support.AENetworkSupport 6 | import icu.takeneko.appwebterminal.support.MECraftingServiceView 7 | import icu.takeneko.appwebterminal.support.http.HttpServerLifecycleSupport 8 | import icu.takeneko.appwebterminal.util.DispatchedSerializer 9 | import io.ktor.websocket.* 10 | import kotlinx.coroutines.CancellationException 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.runBlocking 14 | import kotlinx.serialization.builtins.serializer 15 | import kotlinx.serialization.json.Json 16 | import org.slf4j.LoggerFactory 17 | import java.util.UUID 18 | 19 | class WebsocketSession( 20 | private val session: DefaultWebSocketSession, 21 | private val grid: IGrid, 22 | val owner: AENetworkAccess 23 | ) { 24 | private val Json = Json { 25 | ignoreUnknownKeys = true 26 | prettyPrint = false 27 | } 28 | internal var updateInterval = 20 29 | internal var updateCountdown = 0 30 | internal var craftingServiceView = MECraftingServiceView(grid) 31 | private val logger = LoggerFactory.getLogger("WebsocketSession") 32 | private val serializer = DispatchedSerializer( 33 | "type", 34 | mapOf( 35 | "update_interval" to SetUpdateInterval.serializer(), 36 | "select_cpu" to SelectCpu.serializer(), 37 | "status" to MECraftingServiceStatusBundle.serializer(), 38 | "cancel_job" to CancelJob.serializer(), 39 | ), 40 | String.serializer(), 41 | Protocol::type 42 | ) 43 | 44 | fun tick() { 45 | if (updateCountdown-- <= 0) { 46 | craftingServiceView.tick() 47 | updateCountdown = updateInterval 48 | val message = craftingServiceView.createUpdateMessage() 49 | send(message) 50 | } 51 | } 52 | 53 | suspend fun accept() { 54 | AENetworkSupport.notifySessionStarted(this) 55 | try { 56 | for (frame in session.incoming) { 57 | if (frame is Frame.Text) { 58 | val text = frame.readText() 59 | logger.debug("Received $text.") 60 | try { 61 | val data = Json.decodeFromString(serializer, text) 62 | data.accept(this) 63 | } catch (e: Throwable) { 64 | logger.error("Error handling websocket packet:", e) 65 | } 66 | } 67 | } 68 | } catch (_: CancellationException) { 69 | 70 | } finally { 71 | AENetworkSupport.notifySessionTerminated(this) 72 | } 73 | 74 | } 75 | 76 | private fun send(message: T) where T : Protocol { 77 | val context = HttpServerLifecycleSupport.serverInstance?.server?.application?.coroutineContext 78 | ?: return 79 | CoroutineScope(context).launch { 80 | try { 81 | logger.debug("Sending {}", message) 82 | sendSuspend(message) 83 | } catch (_: CancellationException) { 84 | 85 | } 86 | } 87 | } 88 | 89 | private suspend fun sendSuspend(message: T) where T : Protocol { 90 | session.send(Json.encodeToString(serializer, message)) 91 | } 92 | 93 | fun close() { 94 | val context = HttpServerLifecycleSupport.serverInstance?.server?.application?.coroutineContext 95 | ?: return 96 | CoroutineScope(context).launch { 97 | try { 98 | session.close(CloseReason(CloseReason.Codes.NORMAL, "Connection closed.")) 99 | } catch (_: CancellationException) { 100 | 101 | } 102 | 103 | } 104 | } 105 | } 106 | 107 | fun main() { 108 | 109 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/AEUtil.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import appeng.api.stacks.AEKey 4 | import java.util.Objects 5 | 6 | fun AEKey.myHash(): Int { 7 | return Objects.hash(this.id, this.type.id) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/CachedModFile.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import net.minecraftforge.forgespi.locating.IModFile 4 | import kotlin.io.path.readBytes 5 | 6 | class CachedModFile( 7 | private val modFile: IModFile 8 | ) { 9 | private val cacheMap = mutableMapOf() 10 | 11 | fun getResource(path: String): ByteArray? { 12 | val resourcePath = modFile.findResource(path) ?: return null 13 | return if (path in cacheMap) { 14 | cacheMap[path] 15 | } else { 16 | resourcePath.readBytes().also { cacheMap[path] = it } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/CommandUtils.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.mojang.brigadier.arguments.ArgumentType 4 | import com.mojang.brigadier.arguments.IntegerArgumentType 5 | import com.mojang.brigadier.arguments.StringArgumentType 6 | import com.mojang.brigadier.builder.LiteralArgumentBuilder 7 | import com.mojang.brigadier.builder.RequiredArgumentBuilder 8 | import com.mojang.brigadier.context.CommandContext as Ctx 9 | import net.minecraft.commands.CommandSourceStack 10 | import net.minecraft.network.chat.Component 11 | import kotlin.reflect.KProperty 12 | 13 | typealias S = CommandSourceStack 14 | 15 | fun CommandContext.sendFeedback(component: Component) { 16 | this.delegate.source.sendSystemMessage(component) 17 | } 18 | 19 | fun CommandContext.sendError(component: Component) { 20 | this.delegate.source.sendFailure(component) 21 | } 22 | 23 | class LiteralCommand(root: String) { 24 | val node: LiteralArgumentBuilder = LiteralArgumentBuilder.literal(root) 25 | } 26 | 27 | class ArgumentCommand(name: String, argumentType: ArgumentType) { 28 | val node: RequiredArgumentBuilder = 29 | RequiredArgumentBuilder.argument(name, argumentType) 30 | } 31 | 32 | 33 | fun LiteralCommand(literal: String, function: LiteralCommand.() -> Unit): LiteralCommand { 34 | return LiteralCommand(literal).apply(function) 35 | } 36 | 37 | fun LiteralCommand.literal(literal: String, function: LiteralCommand.() -> Unit) { 38 | this.node.then(LiteralCommand(literal).apply(function).node) 39 | } 40 | 41 | fun LiteralCommand.requires(predicate: (S) -> Boolean, function: LiteralCommand.() -> Unit) { 42 | this.apply { 43 | node.requires(predicate) 44 | function(this) 45 | } 46 | } 47 | 48 | fun ArgumentCommand.requires(predicate: (S) -> Boolean, function: ArgumentCommand.() -> Unit) { 49 | this.apply { 50 | node.requires(predicate) 51 | function(this) 52 | } 53 | } 54 | 55 | fun LiteralCommand.execute(function: CommandContext.() -> Int) { 56 | this.node.executes { 57 | CommandContext(it).function() 58 | } 59 | } 60 | 61 | fun LiteralCommand.requires(function: S.() -> Boolean): LiteralCommand { 62 | this.node.requires(function) 63 | return this 64 | } 65 | 66 | fun LiteralCommand.argument(name: String, argumentType: ArgumentType, function: ArgumentCommand.() -> Unit) { 67 | this.node.then(ArgumentCommand(name, argumentType).apply(function).node) 68 | } 69 | 70 | 71 | fun ArgumentCommand.literal(literal: String, function: LiteralCommand.() -> Unit) { 72 | this.node.then(LiteralCommand(literal).apply(function).node) 73 | } 74 | 75 | fun ArgumentCommand.execute(function: CommandContext.() -> Int) { 76 | this.node.executes { 77 | CommandContext(it).function() 78 | } 79 | } 80 | 81 | fun ArgumentCommand.requires(function: S.() -> Boolean): ArgumentCommand { 82 | this.node.requires(function) 83 | return this 84 | } 85 | 86 | fun ArgumentCommand.argument( 87 | name: String, 88 | argumentType: ArgumentType, 89 | function: ArgumentCommand.() -> Unit 90 | ) { 91 | this.node.then(ArgumentCommand(name, argumentType).apply(function).node) 92 | } 93 | 94 | fun ArgumentCommand.integerArgument( 95 | name: String, 96 | min: Int = Int.MIN_VALUE, 97 | max: Int = Int.MAX_VALUE, 98 | function: ArgumentCommand.() -> Unit 99 | ) { 100 | this.node.then(ArgumentCommand(name, IntegerArgumentType.integer(min, max)).apply(function).node) 101 | } 102 | 103 | fun ArgumentCommand.wordArgument( 104 | name: String, 105 | function: ArgumentCommand.() -> Unit 106 | ) { 107 | this.node.then(ArgumentCommand(name, StringArgumentType.word()).apply(function).node) 108 | } 109 | 110 | fun ArgumentCommand.greedyStringArgument( 111 | name: String, 112 | function: ArgumentCommand.() -> Unit 113 | ) { 114 | this.node.then(ArgumentCommand(name, StringArgumentType.greedyString()).apply(function).node) 115 | } 116 | 117 | 118 | fun LiteralCommand.integerArgument( 119 | name: String, 120 | min: Int = Int.MIN_VALUE, 121 | max: Int = Int.MAX_VALUE, 122 | function: ArgumentCommand.() -> Unit 123 | ) { 124 | this.node.then(ArgumentCommand(name, IntegerArgumentType.integer(min, max)).apply(function).node) 125 | } 126 | 127 | fun LiteralCommand.wordArgument( 128 | name: String, 129 | function: ArgumentCommand.() -> Unit 130 | ) { 131 | this.node.then(ArgumentCommand(name, StringArgumentType.word()).apply(function).node) 132 | } 133 | 134 | fun LiteralCommand.greedyStringArgument( 135 | name: String, 136 | function: ArgumentCommand.() -> Unit 137 | ) { 138 | this.node.then(ArgumentCommand(name, StringArgumentType.greedyString()).apply(function).node) 139 | } 140 | 141 | class CommandContext( 142 | val delegate: Ctx 143 | ) { 144 | inline operator fun getValue(thisRef: Any?, prop: KProperty<*>): T { 145 | if (T::class.java == Ctx::class.java) { 146 | return delegate as T 147 | } 148 | if (T::class.java == S::class.java){ 149 | return delegate.source as T 150 | } 151 | return delegate.getArgument(prop.name, T::class.java) 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/FormattingUtil.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import appeng.shaded.flatbuffers.reflection.Object 4 | import com.google.common.base.CaseFormat 5 | import net.minecraft.network.chat.Component 6 | import net.minecraft.network.chat.ComponentContents 7 | import net.minecraft.network.chat.MutableComponent 8 | import net.minecraft.network.chat.contents.LiteralContents 9 | import net.minecraft.network.chat.contents.TranslatableContents 10 | import java.util.Locale 11 | 12 | fun String.toEnglishName(): String { 13 | return this.lowercase(Locale.ROOT).split("_") 14 | .joinToString(separator = " ") { a -> 15 | a.replaceFirstChar { 16 | if (it.isLowerCase()) { 17 | it.titlecase(Locale.getDefault()) 18 | } else { 19 | it.toString() 20 | } 21 | } 22 | } 23 | } 24 | 25 | fun String.toLowerCaseUnder(): String { 26 | return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, this) 27 | } 28 | 29 | fun Component.toLocalizedString(lang: String): String { 30 | val contents = this.contents 31 | var rt = when (contents) { 32 | is LiteralContents -> { 33 | contents.text() 34 | } 35 | 36 | is TranslatableContents -> { 37 | 38 | val key = contents.key 39 | val args = contents.args.map { 40 | if (it is Component) { 41 | return@map it.toLocalizedString(lang) 42 | } 43 | return@map it.toString() 44 | } 45 | I18nUtil.translate(lang, key, *args.toTypedArray()) 46 | } 47 | 48 | else -> { 49 | "" 50 | } 51 | } 52 | this.siblings.forEach { 53 | rt += it.toLocalizedString(lang) 54 | } 55 | return rt 56 | } 57 | 58 | 59 | fun Component.strip(): MutableComponent { 60 | val rt: MutableComponent 61 | if (neededContents(this.contents)) { 62 | rt = MutableComponent.create(this.contents) 63 | } else { 64 | rt = MutableComponent.create(ComponentContents.EMPTY) 65 | } 66 | rt.style = this.style.withHoverEvent(null).withClickEvent(null).withInsertion(null) 67 | this.siblings.forEach { c -> rt.append(c.strip()) } 68 | return rt 69 | } 70 | 71 | private fun neededContents(contents: ComponentContents): Boolean { 72 | if (contents == ComponentContents.EMPTY) return true 73 | if (contents is LiteralContents) return true 74 | if (contents is TranslatableContents) return true 75 | return false 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/I18nUtil.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import java.util.IllegalFormatException 4 | 5 | object I18nUtil { 6 | val languageProviders = listOf<(String, String) -> String?>( 7 | { lang, key -> KubejsI18nSupport.get(lang, key) }, 8 | { lang, key -> MinecraftI18nSupport.get(lang, key) }, 9 | { lang, key -> ServerI18nSupport.get(lang, key) } 10 | ) 11 | 12 | fun get(language: String, key: String): String { 13 | val content = languageProviders.asSequence() 14 | .map { it(language, key) } 15 | .filterNotNull() 16 | .firstOrNull() 17 | return content ?: key 18 | } 19 | 20 | fun translate(language: String, key: String, vararg args: Any?): String { 21 | val content = get(language, key) 22 | return if (args.isNotEmpty()) { 23 | try { 24 | content.format(*args) 25 | } catch (e: IllegalFormatException) { 26 | key 27 | } 28 | } else { 29 | content 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/KRegistrate.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.tterrag.registrate.Registrate 4 | import net.minecraftforge.eventbus.api.IEventBus 5 | import thedarkcolour.kotlinforforge.forge.MOD_BUS 6 | 7 | class KRegistrate(modid: String) : Registrate(modid) { 8 | 9 | private lateinit var modEventBus: IEventBus 10 | 11 | override fun getModEventBus(): IEventBus { 12 | return modEventBus 13 | } 14 | 15 | override fun registerEventListeners(bus: IEventBus): Registrate { 16 | this.modEventBus = bus; 17 | return super.registerEventListeners(bus) 18 | } 19 | 20 | companion object { 21 | fun create(modid: String): Registrate { 22 | return KRegistrate(modid).apply { 23 | this.registerEventListeners(MOD_BUS) 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/KubejsI18nSupport.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.google.common.collect.HashBasedTable 4 | import com.mojang.logging.LogUtils 5 | import kotlinx.serialization.json.Json 6 | import kotlinx.serialization.json.JsonDecoder 7 | import kotlin.io.path.Path 8 | import kotlin.io.path.div 9 | import kotlin.io.path.exists 10 | import kotlin.io.path.listDirectoryEntries 11 | import kotlin.io.path.name 12 | import kotlin.io.path.nameWithoutExtension 13 | 14 | object KubejsI18nSupport { 15 | private const val DEFAULT_LANGUAGE = "en_us" 16 | private val languages = HashBasedTable.create() 17 | private val logger = LogUtils.getLogger() 18 | 19 | private val Json = Json { 20 | ignoreUnknownKeys = true 21 | } 22 | 23 | fun init() { 24 | val basePath = Path("kubejs", "assets") 25 | if (!basePath.exists()) return@init 26 | basePath.listDirectoryEntries().forEach { 27 | val langDir = it / "lang" 28 | if (!langDir.exists()) return@init 29 | langDir.listDirectoryEntries("*.json").forEach { langFile -> 30 | val langName = langFile.nameWithoutExtension 31 | try { 32 | val map = Json.decodeFromString>(langFile.toFile().readText(Charsets.UTF_8)) 33 | map.forEach { k, v -> 34 | languages.put(langName, k, v) 35 | } 36 | } catch (e: Exception) { 37 | logger.error("Error while reading $langFile", e) 38 | } 39 | } 40 | } 41 | } 42 | 43 | fun get(language: String, key: String): String? { 44 | return languages.get(language, key) 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/LanguageInstance.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.mojang.logging.LogUtils 4 | import kotlinx.serialization.json.Json 5 | import net.minecraftforge.fml.ModList 6 | import kotlin.io.path.exists 7 | import kotlin.io.path.listDirectoryEntries 8 | import kotlin.io.path.readText 9 | 10 | private val Json = Json { 11 | ignoreUnknownKeys = true 12 | } 13 | 14 | private val logger = LogUtils.getLogger() 15 | 16 | class LanguageInstance( 17 | val language: String 18 | ) { 19 | private val translations = mutableMapOf() 20 | 21 | init { 22 | ModList.get().modFiles 23 | .mapNotNull { 24 | it.file.findResource("assets")?.listDirectoryEntries() 25 | }.flatMap { 26 | it.map { it1 -> it1.resolve("lang").resolve("$language.json") }.filter { it1 -> it1.exists() } 27 | }.mapNotNull { 28 | try { 29 | icu.takeneko.appwebterminal.util.Json.decodeFromString>(it.readText()) 30 | } catch (e: Exception) { 31 | logger.error("Error while decoding language", e) 32 | null 33 | } 34 | }.flatMap { 35 | it.entries 36 | }.forEach { (k, v) -> 37 | translations[k] = v 38 | } 39 | } 40 | 41 | operator fun contains(key: String) = key in translations 42 | 43 | fun getOrDefault(key: String, default: String = key) = translations[key] ?: default 44 | 45 | fun get(key: String) = translations[key] 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/MinecraftI18nSupport.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.google.common.collect.HashBasedTable 4 | import icu.takeneko.appwebterminal.resource.CacheProvider 5 | import icu.takeneko.appwebterminal.resource.LanguageFileDownloader 6 | import kotlinx.serialization.json.Json 7 | import kotlin.io.path.exists 8 | import kotlin.io.path.readText 9 | 10 | 11 | object MinecraftI18nSupport { 12 | private const val DEFAULT_LANGUAGE = "en_us" 13 | private val translations = HashBasedTable.create() 14 | private val Json = Json { 15 | ignoreUnknownKeys = true 16 | } 17 | 18 | private fun requestLanguage(language: String) { 19 | if (translations.containsRow(language)) { 20 | return 21 | } 22 | val file = CacheProvider.requireFile( 23 | LanguageFileDownloader.fileNameMapping["minecraft/lang/$language.json"] ?: return 24 | ) 25 | if (file.exists()) { 26 | for ((k, v) in Json.decodeFromString>(file.readText())) { 27 | translations.put(language, k, v) 28 | } 29 | } 30 | } 31 | 32 | fun get(language: String, key: String): String? { 33 | requestLanguage(language) 34 | return translations.get(language, key) 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/QueryJWTAuth.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.auth0.jwt.exceptions.JWTVerificationException 4 | import com.auth0.jwt.impl.JWTParser 5 | import com.auth0.jwt.interfaces.JWTVerifier 6 | import io.ktor.http.* 7 | import io.ktor.server.application.* 8 | import io.ktor.server.auth.* 9 | import io.ktor.server.auth.jwt.* 10 | import io.ktor.server.response.* 11 | import java.util.Base64 12 | 13 | internal val queryJWTKey: Any = "queryJWTAuth" 14 | 15 | fun AuthenticationConfig.queryJwt(name: String, config: QueryParamJWTAuthenticationProvider.Config.() -> Unit) { 16 | val provider = QueryParamJWTAuthenticationProvider.Config(name).apply(config).build() 17 | register(provider) 18 | } 19 | 20 | class QueryParamJWTAuthenticationProvider internal constructor(config: Config) : AuthenticationProvider(config) { 21 | private val verifier: ((String) -> JWTVerifier?) = config.verifier 22 | private val authenticationFunction = config.authenticationFunction 23 | private val challengeFunction: suspend JWTChallengeContext.() -> Unit = config.challenge 24 | 25 | 26 | class Config(name: String) : AuthenticationProvider.Config(name) { 27 | internal var authenticationFunction: AuthenticationFunction = { 28 | throw NotImplementedError("JWT auth validate function is not specified.") 29 | } 30 | 31 | internal var verifier: ((String) -> JWTVerifier?) = { null } 32 | 33 | internal var challenge: suspend JWTChallengeContext.() -> Unit = { 34 | call.respond( 35 | HttpStatusCode.Unauthorized, "token expired" 36 | ) 37 | } 38 | 39 | fun verifier(verifier: JWTVerifier) { 40 | this.verifier = { verifier } 41 | } 42 | 43 | fun verifier(verifier: (String) -> JWTVerifier?) { 44 | this.verifier = verifier 45 | } 46 | 47 | fun validate(validate: suspend ApplicationCall.(JWTCredential) -> Any?) { 48 | authenticationFunction = validate 49 | } 50 | 51 | fun challenge(block: suspend JWTChallengeContext.() -> Unit) { 52 | challenge = block 53 | } 54 | 55 | fun build(): QueryParamJWTAuthenticationProvider { 56 | return QueryParamJWTAuthenticationProvider(this) 57 | } 58 | 59 | } 60 | 61 | override suspend fun onAuthenticate(context: AuthenticationContext) { 62 | val call = context.call 63 | val token = call.request.queryParameters["token"] 64 | 65 | if (token != null) { 66 | try { 67 | val jwtVerifier = verifier(token) 68 | if (jwtVerifier == null) { 69 | context.queryChallenge(AuthenticationFailedCause.InvalidCredentials, challengeFunction) 70 | return 71 | } 72 | val principal = verifyAndValidate(call, jwtVerifier, token, authenticationFunction) 73 | if (principal != null) { 74 | context.principal(name, principal) 75 | return 76 | } 77 | context.queryChallenge(AuthenticationFailedCause.InvalidCredentials, challengeFunction) 78 | return 79 | } catch (cause: Throwable) { 80 | val message = cause.message ?: cause.javaClass.simpleName 81 | context.error(queryJWTKey, AuthenticationFailedCause.Error(message)) 82 | } 83 | } else { 84 | context.queryChallenge(AuthenticationFailedCause.InvalidCredentials, challengeFunction) 85 | } 86 | } 87 | } 88 | 89 | internal fun AuthenticationContext.queryChallenge( 90 | cause: AuthenticationFailedCause, 91 | challengeFunction: suspend JWTChallengeContext.() -> Unit 92 | ) { 93 | this.challenge(queryJWTKey, cause) { challenge, c -> 94 | challengeFunction(JWTChallengeContext(c)) 95 | if (!challenge.completed && call.response.status() != null) { 96 | challenge.complete() 97 | } 98 | } 99 | } 100 | 101 | internal suspend fun verifyAndValidate( 102 | call: ApplicationCall, 103 | jwtVerifier: JWTVerifier?, 104 | token: String, 105 | validate: suspend ApplicationCall.(JWTCredential) -> Any? 106 | ): Any? { 107 | val jwt = try { 108 | token.let { jwtVerifier?.verify(it) } 109 | } catch (cause: JWTVerificationException) { 110 | null 111 | } ?: return null 112 | 113 | val payload = JWTParser().parsePayload(String(Base64.getUrlDecoder().decode(jwt.payload))) 114 | val credentials = JWTCredential(payload) 115 | val principal = validate(call, credentials) 116 | 117 | return principal 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/RenderingUtil.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack 4 | 5 | inline fun PoseStack.use(block: PoseStack.() -> T):T { 6 | this.pushPose() 7 | val ret = this.block() 8 | this.popPose() 9 | return ret 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/ResourceUtil.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import icu.takeneko.appwebterminal.AppWebTerminal.MOD_ID 4 | import io.ktor.http.* 5 | import io.ktor.server.response.* 6 | import io.ktor.server.routing.* 7 | import net.minecraftforge.fml.ModList 8 | import java.io.File 9 | import kotlin.io.path.Path 10 | import kotlin.io.path.extension 11 | 12 | const val pathParameterName = "static-content-path-parameter" 13 | 14 | private val modResource by lazy { 15 | CachedModFile(ModList.get().getModFileById(MOD_ID).file) 16 | } 17 | 18 | fun Routing.staticResourceForModContainer() { 19 | createChild(object : RouteSelector() { 20 | override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = 21 | RouteSelectorEvaluation.Success(quality = RouteSelectorEvaluation.qualityTailcard) 22 | }).apply { 23 | route("/") { 24 | route("{$pathParameterName...}") { 25 | get { 26 | var relativePath = call.parameters.getAll(pathParameterName) 27 | ?.joinToString(File.separator) ?: return@get 28 | if (relativePath.isEmpty()) { 29 | relativePath = "index.html" 30 | } 31 | val requestedPath = "frontend/$relativePath" 32 | val resource = modResource.getResource(requestedPath) 33 | ?: return@get call.respond(HttpStatusCode.NotFound) 34 | val contentType = ContentType.defaultForFileExtension(Path(requestedPath).extension) 35 | return@get call.respondBytes(contentType, HttpStatusCode.OK) { 36 | resource 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/SerializationUtils.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.MissingFieldException 6 | import kotlinx.serialization.descriptors.PrimitiveKind 7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 8 | import kotlinx.serialization.descriptors.SerialDescriptor 9 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor 10 | import kotlinx.serialization.encoding.CompositeDecoder 11 | import kotlinx.serialization.encoding.CompositeEncoder 12 | import kotlinx.serialization.encoding.Decoder 13 | import kotlinx.serialization.encoding.Encoder 14 | import net.minecraft.network.chat.Component 15 | import net.minecraft.resources.ResourceLocation 16 | 17 | class DispatchedSerializer( 18 | private val keyName: String, 19 | private val dispatchMap: Map>, 20 | private val keySerializer: KSerializer, 21 | private val keyGetter: T.() -> K 22 | ) : KSerializer { 23 | private val _descriptor: SerialDescriptor = buildClassSerialDescriptor( 24 | "Dispatched", 25 | typeParameters = (dispatchMap.values + keySerializer).map { it.descriptor }.toTypedArray() 26 | ) { 27 | this.element(keyName, keySerializer.descriptor) 28 | } 29 | 30 | override val descriptor: SerialDescriptor 31 | get() = _descriptor 32 | 33 | @OptIn(ExperimentalSerializationApi::class) 34 | override fun deserialize(decoder: Decoder): T { 35 | val composite = decoder.beginStructure(descriptor) 36 | val keyIdx = composite.decodeElementIndex(descriptor) 37 | if (keyIdx != 0) { 38 | throw MissingFieldException(listOf(keyName), "Expected $keyName.") 39 | } 40 | val value = composite.decodeSerializableElement(keySerializer.descriptor, keyIdx, keySerializer) 41 | val serializer = 42 | dispatchMap[value] ?: throw IllegalArgumentException("No KSerializer found for key type $value") 43 | return serializer.deserialize(WrappedDecoder(decoder, composite)) 44 | } 45 | 46 | @Suppress("UNCHECKED_CAST") 47 | override fun serialize(encoder: Encoder, value: T) { 48 | val key = value.keyGetter() 49 | val compositeEncoder = encoder.beginStructure(descriptor) 50 | compositeEncoder.encodeSerializableElement(descriptor, 0, keySerializer, key) 51 | val serializer = dispatchMap[key] ?: throw IllegalArgumentException("No KSerializer found for key type $value") 52 | (serializer as KSerializer).serialize(WrappedEncoder(encoder, compositeEncoder), value) 53 | } 54 | } 55 | 56 | abstract class StringifySerializer : KSerializer { 57 | override val descriptor: SerialDescriptor 58 | get() = PrimitiveSerialDescriptor("ResourceLocation", PrimitiveKind.STRING) 59 | 60 | override fun deserialize(decoder: Decoder): E { 61 | return fromString(decoder.decodeString()) 62 | } 63 | 64 | override fun serialize(encoder: Encoder, value: E) { 65 | encoder.encodeString(objectToString(value)) 66 | } 67 | 68 | abstract fun objectToString(e: E): String 69 | 70 | abstract fun fromString(s: String): E 71 | } 72 | 73 | class ResourceLocationSerializer : StringifySerializer() { 74 | override fun objectToString(e: ResourceLocation): String = e.toString() 75 | 76 | override fun fromString(s: String): ResourceLocation = ResourceLocation(s) 77 | } 78 | 79 | class ComponentSerializer: StringifySerializer() { 80 | override fun objectToString(e: Component): String = Component.Serializer.toJson(e) 81 | 82 | 83 | override fun fromString(s: String): Component = Component.Serializer.fromJson(s)!! 84 | } 85 | 86 | private class WrappedDecoder(decoder: Decoder, val compositeDecoder: CompositeDecoder) : Decoder by decoder { 87 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { 88 | return compositeDecoder 89 | } 90 | } 91 | 92 | private class WrappedEncoder(encoder: Encoder, val compositeEncoder: CompositeEncoder) : Encoder by encoder { 93 | override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { 94 | return compositeEncoder 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/ServerI18nSupport.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | object ServerI18nSupport { 4 | private const val DEFAULT_LANGUAGE = "en_us" 5 | private val languages = mutableMapOf() 6 | 7 | init { 8 | languages[DEFAULT_LANGUAGE] = LanguageInstance(DEFAULT_LANGUAGE) 9 | } 10 | 11 | private fun getInstance(language: String): LanguageInstance { 12 | if (language in languages) return languages[language]!! 13 | return LanguageInstance(language).also { languages[language] = it } 14 | } 15 | 16 | fun contains(language: String,key: String): Boolean { 17 | return key in getInstance(language) 18 | } 19 | 20 | fun get(language: String, key: String): String? { 21 | return getInstance(language).get(key) 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/icu/takeneko/appwebterminal/util/StateUtils.kt: -------------------------------------------------------------------------------- 1 | package icu.takeneko.appwebterminal.util 2 | 3 | import net.minecraft.world.level.block.state.StateHolder 4 | import net.minecraft.world.level.block.state.properties.Property 5 | 6 | operator fun StateHolder.get(property: Property): T where T : Comparable { 7 | return this.getValue(property) 8 | } 9 | 10 | operator fun StateHolder.set(property: Property, value: T): T where T : Comparable { 11 | this.setValue(property, value) 12 | return this.getValue(property) 13 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/versions/8/DONT_DELETE_ME: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/META-INF/versions/8/DONT_DELETE_ME -------------------------------------------------------------------------------- /src/main/resources/appwebterminal.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "icu.takeneko.appwebterminal.mixins", 4 | "compatibilityLevel": "JAVA_17", 5 | "minVersion": "0.8", 6 | "client": [ 7 | "LevelRendererMixin", 8 | "RenderStateShardMixin" 9 | ], 10 | "refmap": "appwebterminal.refmap.json", 11 | "mixins": [ 12 | ], 13 | "injectors": { 14 | "defaultRequire": 1 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/ae2guide/structures/web_terminal.snbt: -------------------------------------------------------------------------------- 1 | { 2 | DataVersion: 3465, 3 | size: [3, 2, 3], 4 | data: [ 5 | {pos: [0, 0, 1], state: "ae2:drive{facing:north,spin:0}", nbt: {ForgeCaps: {}, id: "ae2:drive", inv: {item0: {}, item1: {}, item2: {}, item3: {}, item4: {}, item5: {}, item6: {}, item7: {}, item8: {}, item9: {}}, priority: 0, proxy: {p: -1}, visual: {online: 1b}}}, 6 | {pos: [0, 0, 2], state: "ae2:cable_bus{light_level:0,waterlogged:false}", nbt: {ForgeCaps: {}, cable: {gn: {p: -1}, id: "ae2:fluix_smart_dense_cable", visual: {channelsEast: 1, channelsNorth: 1, connections: ["north", "east"], missingChannel: 0b, powered: 1b}}, hasRedstone: 2, id: "ae2:cable_bus", visual: {}}}, 7 | {pos: [1, 0, 1], state: "appwebterminal:web_terminal{facing:north,online:true}", nbt: {ForgeCaps: {}, Name: "ME Web Terminal", Password: "AppliedWebTerminal", UUID: [I; 1932285910, 885146403, -2094225687, 1324842882], id: "appwebterminal:web_terminal", proxy: {p: -1}, visual: {}}}, 8 | {pos: [1, 0, 2], state: "ae2:cable_bus{light_level:0,waterlogged:false}", nbt: {ForgeCaps: {}, cable: {gn: {p: -1}, id: "ae2:fluix_smart_dense_cable", visual: {channelsEast: 2, channelsNorth: 1, channelsWest: 1, connections: ["north", "west", "east"], missingChannel: 0b, powered: 1b}}, hasRedstone: 2, id: "ae2:cable_bus", visual: {}}}, 9 | {pos: [2, 0, 2], state: "ae2:controller{state:online,type:block}", nbt: {ForgeCaps: {}, id: "ae2:controller", internalCurrentPower: 0.0d, proxy: {p: -1}, visual: {}}}, 10 | {pos: [2, 1, 2], state: "ae2:creative_energy_cell", nbt: {ForgeCaps: {}, id: "ae2:creative_energy_cell", proxy: {p: -1}, visual: {}}} 11 | ], 12 | entities: [], 13 | palette: [ 14 | "ae2:drive{facing:north,spin:0}", 15 | "ae2:cable_bus{light_level:0,waterlogged:false}", 16 | "appwebterminal:web_terminal{facing:north,online:true}", 17 | "ae2:controller{state:online,type:block}", 18 | "ae2:creative_energy_cell" 19 | ] 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/ae2guide/web_terminal.md: -------------------------------------------------------------------------------- 1 | --- 2 | navigation: 3 | title: Web Terminal 4 | icon: appwebterminal:web_terminal 5 | position: 100 6 | item_ids: 7 | - appwebterminal:web_terminal 8 | --- 9 | 10 | # The ME Web Terminal 11 | 12 | 13 | 14 | While s are the ways an AE2 network interacts with you, the allows you to interact with an AE2 network just like a but via browser. 15 | 16 | Using the functions that an provides does not require you join a minecraft world, but it requires the minecraft server running, not paused, and you should know the password of a . 17 | 18 | Configure the terminal requires you join the world, and interact with the block you placed. 19 | 20 | To make the work, it requires a connection to an AE2 network, it also requires a channel and power for 3 AE/t. 21 | 22 | ## Example Setup 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ## Recipe 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/lang/ja_jp.json: -------------------------------------------------------------------------------- 1 | { 2 | "appwebterminal.button.done": "完了", 3 | "appwebterminal.gui.me_network_offline": "オフライン", 4 | "appwebterminal.gui.me_network_online": "オンライン", 5 | "appwebterminal.hint.name": "名前: ", 6 | "appwebterminal.hint.password": "パスワード: ", 7 | "appwebterminal.message.render_complete": "レンダリング完了。", 8 | "appwebterminal.message.rendering": "レンダラーは現在動作しています。", 9 | "appwebterminal.message.started": "レンダラーを開始しました。", 10 | "appwebterminal.screen.title": "ME Webターミナル", 11 | "block.appwebterminal.web_terminal": "ME Webターミナル", 12 | "config.appwebterminal.option.backendWebsocketEndpoint": "バックエンドWebソケットエンドポイント", 13 | "config.appwebterminal.option.frontendTitle": "フロントエンドタイトル", 14 | "config.appwebterminal.option.httpPort": "http ポート", 15 | "config.appwebterminal.option.needPinInLanguage": "言語にピンが必要", 16 | "config.screen.appwebterminal": "AppliedWebTerminal 設定", 17 | "item.appwebterminal.cable_web_terminal": "ME Webターミナル", 18 | "itemGroup.appwebterminal.applied_web_terminal": "Applied Web Terminal" 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "appwebterminal.button.done": "完成", 3 | "appwebterminal.gui.me_network_offline": "离线", 4 | "appwebterminal.gui.me_network_online": "在线", 5 | "appwebterminal.hint.name": "名称: ", 6 | "appwebterminal.hint.password": "密码: ", 7 | "appwebterminal.message.render_complete": "渲染完成。", 8 | "appwebterminal.message.rendering": "当前正在渲染。", 9 | "appwebterminal.message.started": "已启动渲染器。", 10 | "appwebterminal.screen.title": "ME 网页终端", 11 | "block.appwebterminal.web_terminal": "ME 网页终端", 12 | "config.appwebterminal.option.backendWebsocketEndpoint": "后端 WebSocket 终端访问点", 13 | "config.appwebterminal.option.frontendTitle": "前端标题", 14 | "config.appwebterminal.option.httpPort": "Http 端口", 15 | "config.appwebterminal.option.needPinInLanguage": "启用拼音搜索的语言", 16 | "config.screen.appwebterminal": "网页终端设置", 17 | "item.appwebterminal.cable_web_terminal": "ME 网页终端", 18 | "itemGroup.appwebterminal.applied_web_terminal": "Applied Web Terminal" 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/models/block/web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit": "Made with Blockbench", 3 | "parent": "block/block", 4 | "textures": { 5 | "1": "appwebterminal:block/web_terminal_bottom", 6 | "2": "appwebterminal:block/web_terminal_side", 7 | "3": "appwebterminal:block/web_terminal_top", 8 | "particle": "appwebterminal:block/web_terminal_top" 9 | }, 10 | "elements": [ 11 | { 12 | "from": [0, 0, 0], 13 | "to": [16, 16, 16], 14 | "faces": { 15 | "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, 16 | "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, 17 | "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, 18 | "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, 19 | "up": {"uv": [0, 0, 16, 16], "texture": "#3"}, 20 | "down": {"uv": [0, 0, 16, 16], "texture": "#1"} 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/models/block/web_terminal_offline.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit": "Made with Blockbench", 3 | "parent": "block/block", 4 | "textures": { 5 | "1": "appwebterminal:block/web_terminal_bottom", 6 | "2": "appwebterminal:block/web_terminal_side", 7 | "4": "appwebterminal:block/web_terminal_top_offline", 8 | "particle": "appwebterminal:block/web_terminal_top" 9 | }, 10 | "elements": [ 11 | { 12 | "from": [0, 0, 0], 13 | "to": [16, 16, 16], 14 | "faces": { 15 | "north": {"uv": [0, 0, 16, 16], "texture": "#2"}, 16 | "east": {"uv": [0, 0, 16, 16], "texture": "#2"}, 17 | "south": {"uv": [0, 0, 16, 16], "texture": "#2"}, 18 | "west": {"uv": [0, 0, 16, 16], "texture": "#2"}, 19 | "up": {"uv": [0, 0, 16, 16], "texture": "#4"}, 20 | "down": {"uv": [0, 0, 16, 16], "texture": "#1"} 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/models/item/cable_web_terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "ae2:item/cable_interface", 3 | "textures": { 4 | "sides": "ae2:part/monitor_sides", 5 | "back": "ae2:part/monitor_back", 6 | "front": "appwebterminal:block/web_terminal_top" 7 | } 8 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/models/part/web_terminal_off.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": { 3 | "sides": "ae2:part/monitor_sides", 4 | "sidesStatus": "ae2:part/monitor_sides_status", 5 | "back": "ae2:part/monitor_back", 6 | "front": "appwebterminal:block/web_terminal_top_offline", 7 | "particle": "ae2:part/monitor_back" 8 | }, 9 | "elements": [ 10 | { 11 | "from": [2, 2, 0], 12 | "to": [14, 14, 2], 13 | "faces": { 14 | "down": { 15 | "texture": "#sides" 16 | }, 17 | "up": { 18 | "texture": "#sides" 19 | }, 20 | "south": { 21 | "texture": "#back" 22 | }, 23 | "east": { 24 | "texture": "#sides" 25 | }, 26 | "north": { 27 | "texture": "#front" 28 | }, 29 | "west": { 30 | "texture": "#sides" 31 | } 32 | } 33 | }, 34 | { 35 | "from": [4, 4, 2], 36 | "to": [12, 12, 3], 37 | "faces": { 38 | "down": { 39 | "texture": "#sidesStatus" 40 | }, 41 | "up": { 42 | "texture": "#sidesStatus" 43 | }, 44 | "south": { 45 | "texture": "#back" 46 | }, 47 | "east": { 48 | "texture": "#sidesStatus" 49 | }, 50 | "west": { 51 | "texture": "#sidesStatus" 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/models/part/web_terminal_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": { 3 | "sides": "ae2:part/monitor_sides", 4 | "sidesStatus": "ae2:part/monitor_sides_status", 5 | "back": "ae2:part/monitor_back", 6 | "front": "appwebterminal:block/web_terminal_top", 7 | "particle": "ae2:part/monitor_back" 8 | }, 9 | "elements": [ 10 | { 11 | "from": [2, 2, 0], 12 | "to": [14, 14, 2], 13 | "faces": { 14 | "down": { 15 | "texture": "#sides" 16 | }, 17 | "up": { 18 | "texture": "#sides" 19 | }, 20 | "south": { 21 | "texture": "#back" 22 | }, 23 | "east": { 24 | "texture": "#sides" 25 | }, 26 | "north": { 27 | "texture": "#front" 28 | }, 29 | "west": { 30 | "texture": "#sides" 31 | } 32 | } 33 | }, 34 | { 35 | "from": [4, 4, 2], 36 | "to": [12, 12, 3], 37 | "faces": { 38 | "down": { 39 | "texture": "#sidesStatus" 40 | }, 41 | "up": { 42 | "texture": "#sidesStatus" 43 | }, 44 | "south": { 45 | "texture": "#back" 46 | }, 47 | "east": { 48 | "texture": "#sidesStatus" 49 | }, 50 | "west": { 51 | "texture": "#sidesStatus" 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/shaders/core/blur.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform sampler2D DiffuseSampler; 4 | 5 | uniform vec2 BlurDir; 6 | uniform float weight[7] = float[] (0.207027, 0.1519810, 0.114514, 0.114514, 0.016216, 0.0111, 0.0100); 7 | 8 | in vec2 texCoord; 9 | 10 | out vec4 fragColor; 11 | 12 | float apply(float f) { 13 | return -(f - 1) * (f - 1) + 1; 14 | } 15 | 16 | void main() { 17 | vec2 texOffset = 1.0 / textureSize(DiffuseSampler, 0); 18 | 19 | vec3 result = texture(DiffuseSampler, texCoord).rgb * weight[0]; 20 | for (int i = 1; i < 7; ++i) { 21 | result += texture(DiffuseSampler, texCoord + vec2(BlurDir.x * texOffset.x * i * 1.943, BlurDir.y * texOffset.y * i * 1.943)).rgb * weight[i]; 22 | result += texture(DiffuseSampler, texCoord - vec2(BlurDir.x * texOffset.x * i * 1.943, BlurDir.y * texOffset.y * i * 1.943)).rgb * weight[i]; 23 | } 24 | vec3 color = vec3(apply(result.r), apply(result.g), apply(result.b)); 25 | fragColor = vec4(result.rgb * 0.95, 1.0); 26 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/shaders/core/blur.json: -------------------------------------------------------------------------------- 1 | { 2 | "blend": { 3 | "func": "add", 4 | "srcrgb": "one", 5 | "dstrgb": "zero" 6 | }, 7 | "vertex": "appwebterminal:blur", 8 | "fragment": "appwebterminal:blur", 9 | "attributes": [ "Position" ], 10 | "samplers": [ 11 | { "name": "DiffuseSampler" } 12 | ], 13 | "uniforms": [ 14 | { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, 15 | { "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, 16 | { "name": "BlurDir", "type": "float", "count": 2, "values": [ 1.0, 0.0 ] } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/shaders/core/blur.vsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec4 Position; 4 | 5 | uniform mat4 ProjMat; 6 | uniform vec2 OutSize; 7 | 8 | out vec2 texCoord; 9 | 10 | void main() { 11 | vec4 outPos = ProjMat * vec4(Position.xy, 0.0, 1.0); 12 | gl_Position = vec4(outPos.xy, 0.2, 1.0); 13 | texCoord = Position.xy / OutSize; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/shaders/core/rendertype_textured_round_rect.fsh: -------------------------------------------------------------------------------- 1 | // This file is part of Modern UI. 2 | // Copyright (C) 2024 BloCamLimb. 3 | // Licensed under LGPL-3.0-or-later. 4 | #version 150 5 | 6 | uniform sampler2D Sampler0; 7 | 8 | uniform vec4 u_Rect; 9 | uniform vec2 u_Radii; 10 | uniform vec4 ColorModulator; 11 | 12 | #define u_Center u_Rect.xy 13 | #define u_Size u_Rect.zw 14 | #define u_Radius u_Radii.x 15 | #define u_Thickness u_Radii.y 16 | 17 | in vec2 f_Position; 18 | in vec4 f_Color; 19 | in vec2 texCoord0; 20 | 21 | out vec4 fragColor; 22 | 23 | void main() { 24 | vec2 pos = f_Position - u_Center; 25 | vec2 d = abs(pos) - u_Size + u_Radius; 26 | float dis = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - u_Radius; 27 | dis = mix(dis, abs(dis) - u_Thickness, float(u_Thickness >= 0.0)); 28 | 29 | vec4 color = texture(Sampler0, vec2(texCoord0.x, 1 - texCoord0.y)) * f_Color * ColorModulator; 30 | // minecraft uses non-premultiplied alpha 31 | color.a *= 1.0 - clamp(dis / fwidth(dis) + 0.5, 0.0, 1.0); 32 | if (color.a < 0.002) { 33 | discard; 34 | } 35 | fragColor = color; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/shaders/core/rendertype_textured_round_rect.json: -------------------------------------------------------------------------------- 1 | { 2 | "blend": { 3 | "func": "add", 4 | "srcrgb": "srcalpha", 5 | "dstrgb": "1-srcalpha" 6 | }, 7 | "vertex": "appwebterminal:rendertype_textured_round_rect", 8 | "fragment": "appwebterminal:rendertype_textured_round_rect", 9 | "attributes": [ 10 | "Position", 11 | "Color", 12 | "UV0" 13 | ], 14 | "samplers": [ 15 | { "name": "Sampler0" } 16 | ], 17 | "uniforms": [ 18 | { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, 19 | { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, 20 | { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, 21 | { "name": "u_Rect", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] }, 22 | { "name": "u_Radii", "type": "float", "count": 2, "values": [ 0.0, 0.0 ] } 23 | ] 24 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/shaders/core/rendertype_textured_round_rect.vsh: -------------------------------------------------------------------------------- 1 | // This file is part of Modern UI. 2 | // Copyright (C) 2024 BloCamLimb. 3 | // Licensed under LGPL-3.0-or-later. 4 | #version 150 5 | 6 | uniform mat4 ModelViewMat; 7 | uniform mat4 ProjMat; 8 | 9 | in vec3 Position; 10 | in vec4 Color; 11 | in vec2 UV0; 12 | 13 | out vec2 f_Position; 14 | out vec4 f_Color; 15 | out vec2 texCoord0; 16 | 17 | void main() { 18 | f_Position = Position.xy; 19 | f_Color = Color; 20 | texCoord0 = UV0; 21 | 22 | gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/textures/block/web_terminal_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/assets/appwebterminal/textures/block/web_terminal_bottom.png -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/textures/block/web_terminal_side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/assets/appwebterminal/textures/block/web_terminal_side.png -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/textures/block/web_terminal_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/assets/appwebterminal/textures/block/web_terminal_top.png -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/textures/block/web_terminal_top.png.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "animation": { 3 | "frametime": 5 4 | } 5 | } -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/textures/block/web_terminal_top_offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/assets/appwebterminal/textures/block/web_terminal_top_offline.png -------------------------------------------------------------------------------- /src/main/resources/assets/appwebterminal/textures/gui/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/assets/appwebterminal/textures/gui/blank.png -------------------------------------------------------------------------------- /src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhuRuoLing/AppliedWebTerminal/18efc779afcba38abb8724079408dd227801eb7d/src/main/resources/icon.png -------------------------------------------------------------------------------- /src/main/templates/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="kotlinforforge" #mandatory 2 | loaderVersion="${loader_version_range}" #mandatory 3 | license="${mod_license}" 4 | issueTrackerURL="https://github.com/ZhuRuoLing/AppliedWebTerminal/issues" 5 | 6 | [[mods]] #mandatory 7 | modId="${mod_id}" #mandatory 8 | version="${mod_version}" #mandatory 9 | displayName="${mod_name}" #mandatory 10 | 11 | # A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ 12 | #updateJSONURL="https://change.me.example.invalid/updates.json" #optional 13 | 14 | displayURL="https://github.com/ZhuRuoLing/AppliedWebTerminal" 15 | 16 | # A file name (in the root of the mod JAR) containing a logo for display 17 | logoFile="icon.png" #optional 18 | 19 | # A text field displayed in the mod UI 20 | #credits="" #optional 21 | 22 | authors="${mod_authors}" #optional 23 | 24 | description='''${mod_description}''' 25 | 26 | [[dependencies.${mod_id}]] 27 | modId="forge" #mandatory 28 | mandatory=true #mandatory 29 | # Optional field describing why the dependency is required or why it is incompatible 30 | # reason="..." 31 | versionRange="${forge_version_range}" #mandatory 32 | # An ordering relationship for the dependency. 33 | # BEFORE - This mod is loaded BEFORE the dependency 34 | # AFTER - This mod is loaded AFTER the dependency 35 | ordering="NONE" 36 | # Side this dependency is applied on - BOTH, CLIENT, or SERVER 37 | side="BOTH" 38 | 39 | # Here's another dependency 40 | [[dependencies.${mod_id}]] 41 | modId="minecraft" 42 | mandatory=true 43 | versionRange="${minecraft_version_range}" 44 | ordering="NONE" 45 | side="BOTH" 46 | 47 | [[dependencies.${mod_id}]] 48 | modId="ae2" 49 | mandatory=true 50 | versionRange="[15.3.0,)" 51 | ordering="NONE" 52 | side="BOTH" 53 | [dependencies.${mod_id}.mc-publish] 54 | modrinth="ae2" 55 | curseforge="applied-energistics-2" 56 | 57 | [[dependencies.${mod_id}]] 58 | modId="configuration" 59 | mandatory=true 60 | versionRange="[3.1.0,)" 61 | ordering="NONE" 62 | side="BOTH" 63 | [dependencies.${mod_id}.mc-publish] 64 | modrinth="configuration" 65 | curseforge="configuration" 66 | 67 | [[dependencies.${mod_id}]] 68 | modId="kotlinforforge" 69 | mandatory=true 70 | versionRange="${loader_version_range}" 71 | ordering="NONE" 72 | side="BOTH" 73 | [dependencies.${mod_id}.mc-publish] 74 | modrinth="kotlin-for-forge" 75 | curseforge="kotlin-for-forge" 76 | 77 | # Features are specific properties of the game environment, that you may want to declare you require. This example declares 78 | # that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't 79 | # stop your mod loading on the server for example. 80 | #[features.${mod_id}] 81 | #openGLVersion="[3.2,)" 82 | 83 | [mc-publish] 84 | modrinth="StQbiBuW" 85 | curseforge=1247579 -------------------------------------------------------------------------------- /src/main/templates/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "description": "${mod_name} resources", 4 | "pack_format": 8 5 | } 6 | } 7 | --------------------------------------------------------------------------------