mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-16 14:37:39 +00:00
Compare commits
18 Commits
v1.20.1-1.
...
v1.20.1-1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3ebdf7ef5e | ||
![]() |
905d4cb091 | ||
![]() |
e7ab05d064 | ||
![]() |
6ec34b42e5 | ||
![]() |
ab785a0906 | ||
![]() |
4541decd40 | ||
![]() |
747a5a53b4 | ||
![]() |
c0643fadca | ||
![]() |
0a31de43c2 | ||
![]() |
96b6947ef2 | ||
![]() |
e7a1065bfc | ||
![]() |
663eecff0c | ||
![]() |
e6125bcf60 | ||
![]() |
0d6c6e7ae7 | ||
![]() |
ae71eb3cae | ||
![]() |
3188197447 | ||
![]() |
6c8b391dab | ||
![]() |
b1248e4901 |
@@ -44,7 +44,7 @@ repos:
|
||||
name: Check Java codestyle
|
||||
files: ".*\\.java$"
|
||||
language: system
|
||||
entry: ./gradlew checkstyleMain checkstyleTest
|
||||
entry: ./gradlew checkstyle
|
||||
pass_filenames: false
|
||||
require_serial: true
|
||||
- id: illuaminate
|
||||
|
14
.reuse/dep5
14
.reuse/dep5
@@ -10,8 +10,8 @@ Files:
|
||||
projects/common/src/testMod/resources/data/cctest/structures/*
|
||||
projects/fabric/src/generated/*
|
||||
projects/forge/src/generated/*
|
||||
projects/web/src/export/index.json
|
||||
projects/web/src/export/items/minecraft/*
|
||||
projects/web/src/htmlTransform/export/index.json
|
||||
projects/web/src/htmlTransform/export/items/minecraft/*
|
||||
Comment: Generated/data files are CC0.
|
||||
Copyright: The CC: Tweaked Developers
|
||||
License: CC0-1.0
|
||||
@@ -37,10 +37,10 @@ Files:
|
||||
projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json
|
||||
projects/fabric/src/testMod/resources/fabric.mod.json
|
||||
projects/forge/src/client/resources/computercraft-client.forge.mixins.json
|
||||
projects/web/src/mount/.settings
|
||||
projects/web/src/mount/example.nfp
|
||||
projects/web/src/mount/example.nft
|
||||
projects/web/src/mount/expr_template.lua
|
||||
projects/web/src/frontend/mount/.settings
|
||||
projects/web/src/frontend/mount/example.nfp
|
||||
projects/web/src/frontend/mount/example.nft
|
||||
projects/web/src/frontend/mount/expr_template.lua
|
||||
projects/web/tsconfig.json
|
||||
Comment: Several assets where it's inconvenient to create a .license file.
|
||||
Copyright: The CC: Tweaked Developers
|
||||
@@ -56,7 +56,7 @@ Files:
|
||||
projects/core/src/main/resources/data/computercraft/lua/rom/autorun/.ignoreme
|
||||
projects/core/src/main/resources/data/computercraft/lua/rom/help/*
|
||||
projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/levels/*
|
||||
projects/web/src/export/items/computercraft/*
|
||||
projects/web/src/htmlTransform/export/items/computercraft/*
|
||||
Comment: Bulk-license original assets as CCPL.
|
||||
Copyright: 2011 Daniel Ratcliffe
|
||||
License: LicenseRef-CCPL
|
||||
|
@@ -100,7 +100,6 @@ about how you can build on that until you've covered everything!
|
||||
[new-issue]: https://github.com/cc-tweaked/CC-Tweaked/issues/new/choose "Create a new issue"
|
||||
[community]: README.md#community "Get in touch with the community."
|
||||
[Adoptium]: https://adoptium.net/temurin/releases?version=17 "Download OpenJDK 17"
|
||||
[checkstyle]: https://checkstyle.org/
|
||||
[illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub"
|
||||
[weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance"
|
||||
[docs]: https://tweaked.cc/ "CC: Tweaked documentation"
|
||||
|
@@ -66,6 +66,7 @@ repositories {
|
||||
includeGroup("me.shedaniel.cloth")
|
||||
includeGroup("me.shedaniel")
|
||||
includeGroup("mezz.jei")
|
||||
includeGroup("org.teavm")
|
||||
includeModule("com.terraformersmc", "modmenu")
|
||||
includeModule("me.lucko", "fabric-permissions-api")
|
||||
}
|
||||
@@ -99,7 +100,10 @@ sourceSets.all {
|
||||
check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
|
||||
|
||||
check("NullAway", CheckSeverity.ERROR)
|
||||
option("NullAway:AnnotatedPackages", listOf("dan200.computercraft", "net.fabricmc.fabric.api").joinToString(","))
|
||||
option(
|
||||
"NullAway:AnnotatedPackages",
|
||||
listOf("dan200.computercraft", "cc.tweaked", "net.fabricmc.fabric.api").joinToString(","),
|
||||
)
|
||||
option("NullAway:ExcludedFieldAnnotations", listOf("org.spongepowered.asm.mixin.Shadow").joinToString(","))
|
||||
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
|
||||
option("NullAway:CheckOptionalEmptiness")
|
||||
@@ -174,6 +178,12 @@ project.plugins.withType(CCTweakedPlugin::class.java) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("checkstyle") {
|
||||
description = "Run Checkstyle on all sources"
|
||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
dependsOn(tasks.withType(Checkstyle::class.java))
|
||||
}
|
||||
|
||||
spotless {
|
||||
encoding = StandardCharsets.UTF_8
|
||||
lineEndings = LineEnding.UNIX
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.AbstractExecTask
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
@@ -11,5 +12,5 @@ import java.io.File
|
||||
|
||||
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
|
||||
@get:OutputDirectory
|
||||
abstract val output: Property<File>
|
||||
abstract val output: DirectoryProperty
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler
|
||||
import org.gradle.api.file.FileSystemLocation
|
||||
import org.gradle.api.file.FileSystemLocationProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.tasks.JavaExec
|
||||
@@ -124,3 +126,6 @@ class CloseScope : AutoCloseable {
|
||||
|
||||
/** Proxy method to avoid overload ambiguity. */
|
||||
fun <T> Property<T>.setProvider(provider: Provider<out T>) = set(provider)
|
||||
|
||||
/** Short-cut method to get the absolute path of a [FileSystemLocation] provider. */
|
||||
fun Provider<out FileSystemLocation>.getAbsolutePath(): String = get().asFile.absolutePath
|
||||
|
@@ -16,4 +16,10 @@ SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
<!-- The commands API is documented in Lua. -->
|
||||
<suppress checks="SummaryJavadocCheck" files=".*[\\/]CommandAPI.java" />
|
||||
|
||||
<!-- Allow putting files in other packages if they look like our TeaVM stubs. -->
|
||||
<suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" />
|
||||
|
||||
<!-- Allow underscores in our test classes. -->
|
||||
<suppress checks="MethodName" files=".*Contract.java" />
|
||||
</suppressions>
|
||||
|
@@ -29,7 +29,7 @@ for _, file in ipairs(files.getFiles()) do
|
||||
local size = file.seek("end")
|
||||
file.seek("set", 0)
|
||||
|
||||
print(file.getName() .. " " .. file.getSize())
|
||||
print(file.getName() .. " " .. size)
|
||||
end
|
||||
```
|
||||
|
||||
|
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
||||
|
||||
# Mod properties
|
||||
isUnstable=false
|
||||
modVersion=1.108.1
|
||||
modVersion=1.108.3
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.20.1
|
||||
|
@@ -30,7 +30,7 @@ kotlin = "1.8.10"
|
||||
kotlin-coroutines = "1.6.4"
|
||||
netty = "4.1.82.Final"
|
||||
nightConfig = "3.6.7"
|
||||
slf4j = "1.7.36"
|
||||
slf4j = "2.0.1"
|
||||
|
||||
# Minecraft mods
|
||||
emi = "1.0.8+1.20.1"
|
||||
@@ -60,7 +60,7 @@ fabric-loom = "1.3.7"
|
||||
forgeGradle = "6.0.8"
|
||||
githubRelease = "2.4.1"
|
||||
ideaExt = "1.1.7"
|
||||
illuaminate = "0.1.0-40-g975cbc3"
|
||||
illuaminate = "0.1.0-44-g9ee0055"
|
||||
librarian = "1.+"
|
||||
minotaur = "2.+"
|
||||
mixinGradle = "0.7.+"
|
||||
@@ -69,10 +69,12 @@ spotless = "6.21.0"
|
||||
taskTree = "2.1.1"
|
||||
vanillaGradle = "0.2.1-SNAPSHOT"
|
||||
vineflower = "1.11.0"
|
||||
teavm = "0.9.0-SQUID.1"
|
||||
|
||||
[libraries]
|
||||
# Normal dependencies
|
||||
asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
|
||||
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
|
||||
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
|
||||
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
|
||||
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
|
||||
@@ -138,6 +140,14 @@ librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" }
|
||||
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
|
||||
nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" }
|
||||
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
|
||||
teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" }
|
||||
teavm-jso = { module = "org.teavm:teavm-jso", version.ref = "teavm" }
|
||||
teavm-jso-apis = { module = "org.teavm:teavm-jso-apis", version.ref = "teavm" }
|
||||
teavm-jso-impl = { module = "org.teavm:teavm-jso-impl", version.ref = "teavm" }
|
||||
teavm-metaprogramming-api = { module = "org.teavm:teavm-metaprogramming-api", version.ref = "teavm" }
|
||||
teavm-metaprogramming-impl = { module = "org.teavm:teavm-metaprogramming-impl", version.ref = "teavm" }
|
||||
teavm-platform = { module = "org.teavm:teavm-platform", version.ref = "teavm" }
|
||||
teavm-tooling = { module = "org.teavm:teavm-tooling", version.ref = "teavm" }
|
||||
vanillaGradle = { module = "org.spongepowered:vanillagradle", version.ref = "vanillaGradle" }
|
||||
vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" }
|
||||
|
||||
@@ -151,6 +161,7 @@ mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
|
||||
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
|
||||
|
||||
[bundles]
|
||||
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
|
||||
kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
|
||||
|
||||
# Minecraft
|
||||
@@ -164,3 +175,7 @@ externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
|
||||
# Testing
|
||||
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
|
||||
testRuntime = ["junit-jupiter-engine", "jqwik-engine"]
|
||||
|
||||
# Build tools
|
||||
teavm-api = [ "teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api" ]
|
||||
teavm-tooling = [ "teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl" ]
|
||||
|
@@ -10,7 +10,7 @@
|
||||
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
|
||||
/projects/core/src/main/resources/data/computercraft/lua/rom/
|
||||
/projects/core/src/test/resources/test-rom
|
||||
/projects/web/src/mount)
|
||||
/projects/web/src/frontend/mount)
|
||||
|
||||
(doc
|
||||
; Also defined in projects/web/build.gradle.kts
|
||||
@@ -23,7 +23,7 @@
|
||||
(url https://tweaked.cc/)
|
||||
(source-link https://github.com/cc-tweaked/CC-Tweaked/blob/${commit}/${path}#L${line})
|
||||
|
||||
(styles /projects/web/src/styles.css)
|
||||
(styles /projects/web/build/rollup/index.css)
|
||||
(scripts /projects/web/build/rollup/index.js)
|
||||
(head doc/head.html))
|
||||
|
||||
@@ -115,4 +115,4 @@
|
||||
:max sleep write
|
||||
cct_test describe expect howlci fail it pending stub before_each)))
|
||||
|
||||
(at /projects/web/src/mount/expr_template.lua (lint (globals :max __expr__)))
|
||||
(at /projects/web/src/frontend/mount/expr_template.lua (lint (globals :max __expr__)))
|
||||
|
3957
package-lock.json
generated
3957
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -6,24 +6,24 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@squid-dev/cc-web-term": "^2.0.0",
|
||||
"preact": "^10.5.5",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.1",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@rollup/plugin-url": "^8.0.1",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"glob": "^10.3.4",
|
||||
"react-dom": "^18.1.0",
|
||||
"react": "^18.1.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-react": "^7.1.1",
|
||||
"rehype": "^12.0.0",
|
||||
"requirejs": "^2.3.6",
|
||||
"rollup": "^3.19.1",
|
||||
"ts-node": "^10.8.0",
|
||||
"@swc/core": "^1.3.92",
|
||||
"@types/node": "^20.8.3",
|
||||
"lightningcss": "^1.22.0",
|
||||
"preact-render-to-string": "^6.2.1",
|
||||
"rehype": "^13.0.0",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rehype-react": "^8.0.0",
|
||||
"rollup": "^4.0.0",
|
||||
"tsx": "^3.12.10",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
@@ -46,6 +46,14 @@ public class ComputerCraftTags {
|
||||
public static final TagKey<Block> WIRED_MODEM = make("wired_modem");
|
||||
public static final TagKey<Block> MONITOR = make("monitor");
|
||||
|
||||
/**
|
||||
* Blocks which should be ignored by a {@code peripheral_hub} peripheral.
|
||||
* <p>
|
||||
* This should include blocks which themselves expose a peripheral hub (such as {@linkplain #WIRED_MODEM wired
|
||||
* modems}).
|
||||
*/
|
||||
public static final TagKey<Block> PERIPHERAL_HUB_IGNORE = make("peripheral_hub_ignore");
|
||||
|
||||
/**
|
||||
* Blocks which can be broken by any turtle tool.
|
||||
*/
|
||||
|
@@ -39,4 +39,6 @@ dependencies {
|
||||
testModImplementation(testFixtures(project(":core")))
|
||||
testModImplementation(testFixtures(project(":common")))
|
||||
testModImplementation(libs.bundles.kotlin)
|
||||
|
||||
testFixturesImplementation(testFixtures(project(":core")))
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
|
||||
@@ -25,15 +26,12 @@ import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.PackOutput;
|
||||
import net.minecraft.data.recipes.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.item.DyeItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
|
||||
import net.minecraft.world.level.ItemLike;
|
||||
@@ -41,6 +39,7 @@ import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
|
||||
@@ -443,7 +442,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
.requires(ModRegistry.Items.MONITOR_NORMAL.get())
|
||||
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
|
||||
.withResultTag(playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c")),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_cloudy")
|
||||
);
|
||||
@@ -454,7 +453,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
.requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
|
||||
.withResultTag(playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb")),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_dan200")
|
||||
);
|
||||
@@ -513,17 +512,15 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
}
|
||||
|
||||
private static CompoundTag playerHead(String name, String uuid) {
|
||||
var owner = new CompoundTag();
|
||||
owner.putString("Name", name);
|
||||
owner.putString("Id", uuid);
|
||||
var owner = NbtUtils.writeGameProfile(new CompoundTag(), new GameProfile(UUID.fromString(uuid), name));
|
||||
|
||||
var tag = new CompoundTag();
|
||||
tag.put("SkullOwner", owner);
|
||||
tag.put(PlayerHeadItem.TAG_SKULL_OWNER, owner);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static Consumer<JsonObject> family(ComputerFamily family) {
|
||||
return json -> json.addProperty("family", family.toString());
|
||||
return json -> json.addProperty("family", family.getSerializedName());
|
||||
}
|
||||
|
||||
private static void addSpecial(Consumer<FinishedRecipe> add, SimpleCraftingRecipeSerializer<?> special) {
|
||||
|
@@ -35,6 +35,8 @@ class TagProvider {
|
||||
tags.tag(ComputerCraftTags.Blocks.WIRED_MODEM).add(ModRegistry.Blocks.CABLE.get(), ModRegistry.Blocks.WIRED_MODEM_FULL.get());
|
||||
tags.tag(ComputerCraftTags.Blocks.MONITOR).add(ModRegistry.Blocks.MONITOR_NORMAL.get(), ModRegistry.Blocks.MONITOR_ADVANCED.get());
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE).addTag(ComputerCraftTags.Blocks.WIRED_MODEM);
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE).addTag(BlockTags.LEAVES).add(
|
||||
Blocks.BAMBOO, Blocks.BAMBOO_SAPLING // Bamboo isn't instabreak for some odd reason.
|
||||
);
|
||||
|
@@ -70,6 +70,10 @@ import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.pocket.peripherals.PocketModem;
|
||||
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
|
||||
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
|
||||
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
|
||||
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
|
||||
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
|
||||
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
|
||||
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
@@ -79,8 +83,6 @@ import dan200.computercraft.shared.turtle.recipes.TurtleOverlayRecipe;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
||||
import dan200.computercraft.shared.turtle.upgrades.*;
|
||||
import dan200.computercraft.shared.util.ImpostorRecipe;
|
||||
import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
|
||||
@@ -359,17 +361,21 @@ public final class ModRegistry {
|
||||
return REGISTRY.register(name, () -> new SimpleCraftingRecipeSerializer<>(factory));
|
||||
}
|
||||
|
||||
public static final RegistryEntry<RecipeSerializer<CustomShapedRecipe>> SHAPED = REGISTRY.register("shaped", () -> CustomShapedRecipe.serialiser(CustomShapedRecipe::new));
|
||||
public static final RegistryEntry<RecipeSerializer<CustomShapelessRecipe>> SHAPELESS = REGISTRY.register("shapeless", () -> CustomShapelessRecipe.serialiser(CustomShapelessRecipe::new));
|
||||
|
||||
public static final RegistryEntry<RecipeSerializer<ImpostorShapedRecipe>> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", () -> CustomShapedRecipe.serialiser(ImpostorShapedRecipe::new));
|
||||
public static final RegistryEntry<RecipeSerializer<ImpostorShapelessRecipe>> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", () -> CustomShapelessRecipe.serialiser(ImpostorShapelessRecipe::new));
|
||||
|
||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<ColourableRecipe>> DYEABLE_ITEM = simple("colour", ColourableRecipe::new);
|
||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<ClearColourRecipe>> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new);
|
||||
public static final RegistryEntry<TurtleRecipe.Serializer> TURTLE = REGISTRY.register("turtle", TurtleRecipe.Serializer::new);
|
||||
public static final RegistryEntry<RecipeSerializer<TurtleRecipe>> TURTLE = REGISTRY.register("turtle", () -> TurtleRecipe.validatingSerialiser(TurtleRecipe::of));
|
||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
|
||||
public static final RegistryEntry<TurtleOverlayRecipe.Serializer> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
|
||||
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
|
||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PocketComputerUpgradeRecipe>> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new);
|
||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
|
||||
public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::new);
|
||||
public static final RegistryEntry<ComputerUpgradeRecipe.Serializer> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
|
||||
public static final RegistryEntry<ImpostorRecipe.Serializer> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", ImpostorRecipe.Serializer::new);
|
||||
public static final RegistryEntry<ImpostorShapelessRecipe.Serializer> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new);
|
||||
public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
|
||||
}
|
||||
|
||||
public static class Permissions {
|
||||
|
@@ -4,8 +4,33 @@
|
||||
|
||||
package dan200.computercraft.shared.computer.core;
|
||||
|
||||
public enum ComputerFamily {
|
||||
NORMAL,
|
||||
ADVANCED,
|
||||
COMMAND
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
|
||||
public enum ComputerFamily implements StringRepresentable {
|
||||
NORMAL("normal"),
|
||||
ADVANCED("advanced"),
|
||||
COMMAND("command");
|
||||
|
||||
private final String name;
|
||||
|
||||
ComputerFamily(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static ComputerFamily getFamily(JsonObject json, String name) {
|
||||
var familyName = GsonHelper.getAsString(json, name);
|
||||
for (var family : values()) {
|
||||
if (family.getSerializedName().equalsIgnoreCase(familyName)) return family;
|
||||
}
|
||||
|
||||
throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerializedName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
@@ -29,8 +29,6 @@ import java.util.Map;
|
||||
public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResourceMount.class);
|
||||
|
||||
private static final byte[] TEMP_BUFFER = new byte[8192];
|
||||
|
||||
/**
|
||||
* Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes.
|
||||
*/
|
||||
@@ -60,7 +58,7 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
var hasAny = false;
|
||||
String existingNamespace = null;
|
||||
|
||||
var newRoot = new FileEntry("", new ResourceLocation(namespace, subPath));
|
||||
var newRoot = new FileEntry(new ResourceLocation(namespace, subPath));
|
||||
for (var file : manager.listResources(subPath, s -> true).keySet()) {
|
||||
existingNamespace = file.getNamespace();
|
||||
|
||||
@@ -68,7 +66,11 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
|
||||
|
||||
var localPath = FileSystem.toLocal(file.getPath(), subPath);
|
||||
create(newRoot, localPath);
|
||||
try {
|
||||
getOrCreateChild(newRoot, localPath, this::createEntry);
|
||||
} catch (ResourceLocationException e) {
|
||||
LOG.warn("Cannot create resource location for {} ({})", localPath, e.getMessage());
|
||||
}
|
||||
hasAny = true;
|
||||
}
|
||||
|
||||
@@ -83,65 +85,24 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
}
|
||||
}
|
||||
|
||||
private void create(FileEntry lastEntry, String path) {
|
||||
var lastIndex = 0;
|
||||
while (lastIndex < path.length()) {
|
||||
var nextIndex = path.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = path.length();
|
||||
|
||||
var part = path.substring(lastIndex, nextIndex);
|
||||
if (lastEntry.children == null) lastEntry.children = new HashMap<>();
|
||||
|
||||
var nextEntry = lastEntry.children.get(part);
|
||||
if (nextEntry == null) {
|
||||
ResourceLocation childPath;
|
||||
try {
|
||||
childPath = new ResourceLocation(namespace, subPath + "/" + path);
|
||||
} catch (ResourceLocationException e) {
|
||||
LOG.warn("Cannot create resource location for {} ({})", part, e.getMessage());
|
||||
return;
|
||||
}
|
||||
lastEntry.children.put(part, nextEntry = new FileEntry(path, childPath));
|
||||
}
|
||||
|
||||
lastEntry = nextEntry;
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
private FileEntry createEntry(String path) {
|
||||
return new FileEntry(new ResourceLocation(namespace, subPath + "/" + path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(FileEntry file) {
|
||||
protected byte[] getFileContents(String path, FileEntry file) throws IOException {
|
||||
var resource = manager.getResource(file.identifier).orElse(null);
|
||||
if (resource == null) return 0;
|
||||
|
||||
try (var stream = resource.open()) {
|
||||
int total = 0, read = 0;
|
||||
do {
|
||||
total += read;
|
||||
read = stream.read(TEMP_BUFFER);
|
||||
} while (read > 0);
|
||||
|
||||
return total;
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContents(FileEntry file) throws IOException {
|
||||
var resource = manager.getResource(file.identifier).orElse(null);
|
||||
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
if (resource == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
|
||||
try (var stream = resource.open()) {
|
||||
return stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
final ResourceLocation identifier;
|
||||
|
||||
FileEntry(String path, ResourceLocation identifier) {
|
||||
super(path);
|
||||
FileEntry(ResourceLocation identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,12 @@
|
||||
|
||||
package dan200.computercraft.shared.computer.menu;
|
||||
|
||||
import dan200.computercraft.shared.computer.upload.*;
|
||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFile;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
@@ -18,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The default concrete implementation of {@link ServerInputHandler}.
|
||||
@@ -150,8 +154,14 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
}
|
||||
}
|
||||
|
||||
computer.queueEvent("file_transfer", new Object[]{
|
||||
new TransferredFiles(player, owner, toUpload.stream().map(x -> new TransferredFile(x.getName(), x.getBytes())).collect(Collectors.toList())),
|
||||
computer.queueEvent(TransferredFiles.EVENT, new Object[]{
|
||||
new TransferredFiles(
|
||||
toUpload.stream().map(x -> new TransferredFile(x.getName(), new ByteBufferChannel(x.getBytes()))).toList(),
|
||||
() -> {
|
||||
if (player.isAlive() && player.containerMenu == owner) {
|
||||
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
|
||||
}
|
||||
}),
|
||||
});
|
||||
return UploadResultMessage.queued(owner);
|
||||
}
|
||||
|
@@ -5,31 +5,20 @@
|
||||
package dan200.computercraft.shared.computer.recipe;
|
||||
|
||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
|
||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Represents a recipe which converts a computer from one form into another.
|
||||
* A recipe which converts a computer from one form into another.
|
||||
*/
|
||||
public abstract class ComputerConvertRecipe extends ShapedRecipe {
|
||||
private final String group;
|
||||
private final ItemStack result;
|
||||
|
||||
public ComputerConvertRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
|
||||
super(identifier, group, category, width, height, ingredients, result);
|
||||
this.group = group;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public ItemStack getResultItem() {
|
||||
return result;
|
||||
public abstract class ComputerConvertRecipe extends CustomShapedRecipe {
|
||||
public ComputerConvertRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
|
||||
super(identifier, recipe);
|
||||
}
|
||||
|
||||
protected abstract ItemStack convert(IComputerItem item, ItemStack stack);
|
||||
@@ -55,9 +44,4 @@ public abstract class ComputerConvertRecipe extends ShapedRecipe {
|
||||
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
@@ -1,72 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.computer.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.util.RecipeUtil;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
|
||||
public abstract class ComputerFamilyRecipe extends ComputerConvertRecipe {
|
||||
private final ComputerFamily family;
|
||||
|
||||
public ComputerFamilyRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
||||
super(identifier, group, category, width, height, ingredients, result);
|
||||
this.family = family;
|
||||
}
|
||||
|
||||
public ComputerFamily getFamily() {
|
||||
return family;
|
||||
}
|
||||
|
||||
public abstract static class Serializer<T extends ComputerFamilyRecipe> implements RecipeSerializer<T> {
|
||||
protected abstract T create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family);
|
||||
|
||||
@Override
|
||||
public T fromJson(ResourceLocation identifier, JsonObject json) {
|
||||
var group = GsonHelper.getAsString(json, "group", "");
|
||||
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
|
||||
var family = RecipeUtil.getFamily(json, "family");
|
||||
|
||||
var template = RecipeUtil.getTemplate(json);
|
||||
var result = itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
|
||||
|
||||
return create(identifier, group, category, template.width(), template.height(), template.ingredients(), result, family);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
|
||||
var width = buf.readVarInt();
|
||||
var height = buf.readVarInt();
|
||||
var group = buf.readUtf();
|
||||
var category = buf.readEnum(CraftingBookCategory.class);
|
||||
|
||||
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
|
||||
for (var i = 0; i < ingredients.size(); i++) ingredients.set(i, Ingredient.fromNetwork(buf));
|
||||
|
||||
var result = buf.readItem();
|
||||
var family = buf.readEnum(ComputerFamily.class);
|
||||
return create(identifier, group, category, width, height, ingredients, result, family);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buf, T recipe) {
|
||||
buf.writeVarInt(recipe.getWidth());
|
||||
buf.writeVarInt(recipe.getHeight());
|
||||
buf.writeUtf(recipe.getGroup());
|
||||
buf.writeEnum(recipe.category());
|
||||
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buf);
|
||||
buf.writeItem(recipe.getResultItem());
|
||||
buf.writeEnum(recipe.getFamily());
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,35 +4,57 @@
|
||||
|
||||
package dan200.computercraft.shared.computer.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
|
||||
public final class ComputerUpgradeRecipe extends ComputerFamilyRecipe {
|
||||
private ComputerUpgradeRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
||||
super(identifier, group, category, width, height, ingredients, result, family);
|
||||
/**
|
||||
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
|
||||
* family} to another.
|
||||
*/
|
||||
public final class ComputerUpgradeRecipe extends ComputerConvertRecipe {
|
||||
private final ComputerFamily family;
|
||||
|
||||
private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe, ComputerFamily family) {
|
||||
super(identifier, recipe);
|
||||
this.family = family;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItemStack convert(IComputerItem item, ItemStack stack) {
|
||||
return item.withFamily(stack, getFamily());
|
||||
return item.withFamily(stack, family);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getSerializer() {
|
||||
public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
|
||||
}
|
||||
|
||||
public static class Serializer extends ComputerFamilyRecipe.Serializer<ComputerUpgradeRecipe> {
|
||||
public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
|
||||
@Override
|
||||
protected ComputerUpgradeRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
||||
return new ComputerUpgradeRecipe(identifier, group, category, width, height, ingredients, result, family);
|
||||
public ComputerUpgradeRecipe fromJson(ResourceLocation identifier, JsonObject json) {
|
||||
var recipe = ShapedRecipeSpec.fromJson(json);
|
||||
var family = ComputerFamily.getFamily(json, "family");
|
||||
return new ComputerUpgradeRecipe(identifier, recipe, family);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputerUpgradeRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
|
||||
var recipe = ShapedRecipeSpec.fromNetwork(buf);
|
||||
var family = buf.readEnum(ComputerFamily.class);
|
||||
return new ComputerUpgradeRecipe(identifier, recipe, family);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buf, ComputerUpgradeRecipe recipe) {
|
||||
recipe.toSpec().toNetwork(buf);
|
||||
buf.writeEnum(recipe.family);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ public class ItemDetails {
|
||||
|
||||
/*
|
||||
* Used to hide some data from ItemStack tooltip.
|
||||
* @see https://minecraft.gamepedia.com/Tutorials/Command_NBT_tags
|
||||
* @see https://minecraft.wiki/w/Tutorials/Command_NBT_tags
|
||||
* @see ItemStack#getTooltip
|
||||
*/
|
||||
var hideFlags = tag != null ? tag.getInt("HideFlags") : 0;
|
||||
|
@@ -4,8 +4,8 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem.wired;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.platform.ComponentAccess;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
@@ -124,8 +124,7 @@ public final class WiredModemLocalPeripheral {
|
||||
private IPeripheral getPeripheralFrom(Level world, BlockPos pos, Direction direction) {
|
||||
var offset = pos.relative(direction);
|
||||
|
||||
var block = world.getBlockState(offset).getBlock();
|
||||
if (block == ModRegistry.Blocks.WIRED_MODEM_FULL.get() || block == ModRegistry.Blocks.CABLE.get()) return null;
|
||||
if (world.getBlockState(offset).is(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE)) return null;
|
||||
|
||||
var peripheral = peripherals.get((ServerLevel) world, pos, direction);
|
||||
return peripheral instanceof WiredModemPeripheral ? null : peripheral;
|
||||
|
@@ -349,13 +349,15 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
// Either delete the current monitor or sync a new one.
|
||||
if (needsTerminal) {
|
||||
if (serverMonitor == null) serverMonitor = new ServerMonitor(advanced, this);
|
||||
} else {
|
||||
serverMonitor = null;
|
||||
}
|
||||
|
||||
// Update the terminal's width and height and rebuild it. This ensures the monitor
|
||||
// is consistent when syncing it to other monitors.
|
||||
if (serverMonitor != null) serverMonitor.rebuild();
|
||||
// Update the terminal's width and height and rebuild it. This ensures the monitor
|
||||
// is consistent when syncing it to other monitors.
|
||||
serverMonitor.rebuild();
|
||||
} else {
|
||||
// Remove the terminal from the serverMonitor, but keep it around - this ensures that we sync
|
||||
// the (now blank) monitor to the client.
|
||||
if (serverMonitor != null) serverMonitor.reset();
|
||||
}
|
||||
|
||||
// Update the other monitors, setting coordinates, dimensions and the server terminal
|
||||
var pos = getBlockPos();
|
||||
|
@@ -55,6 +55,12 @@ public class ServerMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void reset() {
|
||||
if (terminal == null) return;
|
||||
terminal = null;
|
||||
markChanged();
|
||||
}
|
||||
|
||||
private void markChanged() {
|
||||
if (!changed.getAndSet(true)) TickScheduler.schedule(origin.tickToken);
|
||||
}
|
||||
|
@@ -188,7 +188,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
|
||||
* {@literal false}.
|
||||
* <p>
|
||||
* ### Valid instruments
|
||||
* The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments).
|
||||
* The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.wiki/w/Note_Block#Instruments).
|
||||
* These are:
|
||||
* <p>
|
||||
* {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, {@code "flute"},
|
||||
@@ -228,7 +228,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
|
||||
/**
|
||||
* Plays a Minecraft sound through the speaker.
|
||||
* <p>
|
||||
* This takes the [name of a Minecraft sound](https://minecraft.fandom.com/wiki/Sounds.json), such as
|
||||
* This takes the [name of a Minecraft sound](https://minecraft.wiki/w/Sounds.json), such as
|
||||
* {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch.
|
||||
* <p>
|
||||
* Only one sound can be played at once. This function will return {@literal false} if another sound was started
|
||||
|
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
|
||||
/**
|
||||
* A custom version of {@link ShapedRecipe}, which can be converted to and from a {@link ShapedRecipeSpec}.
|
||||
* <p>
|
||||
* This recipe may both be used as a normal recipe (behaving mostly the same as {@link ShapedRecipe}, with
|
||||
* {@linkplain RecipeUtil#itemStackFromJson(JsonObject) support for putting nbt on the result}), or subclassed to
|
||||
* customise the crafting behaviour.
|
||||
*/
|
||||
public class CustomShapedRecipe extends ShapedRecipe {
|
||||
private final ItemStack result;
|
||||
|
||||
public CustomShapedRecipe(ResourceLocation id, ShapedRecipeSpec recipe) {
|
||||
super(
|
||||
id,
|
||||
recipe.properties().group(), recipe.properties().category(),
|
||||
recipe.template().width(), recipe.template().height(), recipe.template().ingredients(),
|
||||
recipe.result()
|
||||
);
|
||||
this.result = recipe.result();
|
||||
}
|
||||
|
||||
public final ShapedRecipeSpec toSpec() {
|
||||
return new ShapedRecipeSpec(RecipeProperties.of(this), ShapedTemplate.of(this), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<? extends CustomShapedRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.SHAPED.get();
|
||||
}
|
||||
|
||||
public interface Factory<R> {
|
||||
R create(ResourceLocation id, ShapedRecipeSpec recipe);
|
||||
}
|
||||
|
||||
public static <T extends CustomShapedRecipe> RecipeSerializer<T> serialiser(CustomShapedRecipe.Factory<T> factory) {
|
||||
return new Serialiser<>((id, r) -> DataResult.success(factory.create(id, r)));
|
||||
}
|
||||
|
||||
public static <T extends CustomShapedRecipe> RecipeSerializer<T> validatingSerialiser(CustomShapedRecipe.Factory<DataResult<T>> factory) {
|
||||
return new Serialiser<>(factory);
|
||||
}
|
||||
|
||||
private record Serialiser<T extends CustomShapedRecipe>(
|
||||
Factory<DataResult<T>> factory
|
||||
) implements RecipeSerializer<T> {
|
||||
private Serialiser(Factory<DataResult<T>> factory) {
|
||||
this.factory = (id, r) -> factory.create(id, r).flatMap(x -> {
|
||||
if (x.getSerializer() != this) {
|
||||
return DataResult.error(() -> "Expected serialiser to be " + this + ", but was " + x.getSerializer());
|
||||
}
|
||||
return DataResult.success(x);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromJson(ResourceLocation id, JsonObject json) {
|
||||
return Util.getOrThrow(factory.create(id, ShapedRecipeSpec.fromJson(json)), JsonParseException::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
|
||||
return Util.getOrThrow(factory.create(id, ShapedRecipeSpec.fromNetwork(buffer)), IllegalStateException::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buffer, T recipe) {
|
||||
recipe.toSpec().toNetwork(buffer);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
|
||||
|
||||
/**
|
||||
* A custom version of {@link ShapelessRecipe}, which can be converted to and from a {@link ShapelessRecipeSpec}.
|
||||
* <p>
|
||||
* This recipe may both be used as a normal recipe (behaving mostly the same as {@link ShapelessRecipe}, with
|
||||
* {@linkplain RecipeUtil#itemStackFromJson(JsonObject) support for putting nbt on the result}), or subclassed to
|
||||
* customise the crafting behaviour.
|
||||
*/
|
||||
public class CustomShapelessRecipe extends ShapelessRecipe {
|
||||
private final ItemStack result;
|
||||
|
||||
public CustomShapelessRecipe(ResourceLocation id, ShapelessRecipeSpec recipe) {
|
||||
super(id, recipe.properties().group(), recipe.properties().category(), recipe.result(), recipe.ingredients());
|
||||
this.result = recipe.result();
|
||||
}
|
||||
|
||||
public final ShapelessRecipeSpec toSpec() {
|
||||
return new ShapelessRecipeSpec(RecipeProperties.of(this), getIngredients(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<? extends CustomShapelessRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.SHAPELESS.get();
|
||||
}
|
||||
|
||||
public interface Factory<R> {
|
||||
R create(ResourceLocation id, ShapelessRecipeSpec recipe);
|
||||
}
|
||||
|
||||
public static <T extends CustomShapelessRecipe> RecipeSerializer<T> serialiser(Factory<T> factory) {
|
||||
return new CustomShapelessRecipe.Serialiser<>((id, r) -> DataResult.success(factory.create(id, r)));
|
||||
}
|
||||
|
||||
public static <T extends CustomShapelessRecipe> RecipeSerializer<T> validatingSerialiser(Factory<DataResult<T>> factory) {
|
||||
return new CustomShapelessRecipe.Serialiser<>(factory);
|
||||
}
|
||||
|
||||
private record Serialiser<T extends CustomShapelessRecipe>(
|
||||
Factory<DataResult<T>> factory
|
||||
) implements RecipeSerializer<T> {
|
||||
private Serialiser(Factory<DataResult<T>> factory) {
|
||||
this.factory = (id, r) -> factory.create(id, r).flatMap(x -> {
|
||||
if (x.getSerializer() != this) {
|
||||
return DataResult.error(() -> "Expected serialiser to be " + this + ", but was " + x.getSerializer());
|
||||
}
|
||||
return DataResult.success(x);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromJson(ResourceLocation id, JsonObject json) {
|
||||
return Util.getOrThrow(factory.create(id, ShapelessRecipeSpec.fromJson(json)), JsonParseException::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
|
||||
return Util.getOrThrow(factory.create(id, ShapelessRecipeSpec.fromNetwork(buffer)), IllegalStateException::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buffer, T recipe) {
|
||||
recipe.toSpec().toNetwork(buffer);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CustomRecipe;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* A fake {@link ShapedRecipe}, which appears in the recipe book (and other recipe mods), but cannot be crafted.
|
||||
* <p>
|
||||
* This is used to represent examples for our {@link CustomRecipe}s.
|
||||
*/
|
||||
public final class ImpostorShapedRecipe extends CustomShapedRecipe {
|
||||
public ImpostorShapedRecipe(ResourceLocation id, ShapedRecipeSpec recipe) {
|
||||
super(id, recipe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingContainer inv, Level world) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAccess) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<ImpostorShapedRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get();
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CustomRecipe;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* A fake {@link ShapelessRecipe}, which appears in the recipe book (and other recipe mods), but cannot be crafted.
|
||||
* <p>
|
||||
* This is used to represent examples for our {@link CustomRecipe}s.
|
||||
*/
|
||||
public final class ImpostorShapelessRecipe extends CustomShapelessRecipe {
|
||||
public ImpostorShapelessRecipe(ResourceLocation id, ShapelessRecipeSpec recipe) {
|
||||
super(id, recipe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingContainer inv, Level world) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingContainer inventory, RegistryAccess access) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<ImpostorShapelessRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get();
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.TagParser;
|
||||
|
||||
/**
|
||||
* Additional codecs for working with recipes.
|
||||
*/
|
||||
public class MoreCodecs {
|
||||
/**
|
||||
* A codec for {@link CompoundTag}s, which either accepts a NBT-string or a JSON object.
|
||||
*/
|
||||
public static final Codec<CompoundTag> TAG = Codec.either(Codec.STRING, CompoundTag.CODEC).flatXmap(
|
||||
either -> either.map(MoreCodecs::parseTag, DataResult::success),
|
||||
nbtCompound -> DataResult.success(Either.left(nbtCompound.getAsString()))
|
||||
);
|
||||
|
||||
private static DataResult<CompoundTag> parseTag(String contents) {
|
||||
try {
|
||||
return DataResult.success(TagParser.parseTag(contents));
|
||||
} catch (CommandSyntaxException e) {
|
||||
return DataResult.error(e::getMessage);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.CraftingRecipe;
|
||||
|
||||
/**
|
||||
* Common properties that appear in all {@link CraftingRecipe}s.
|
||||
*
|
||||
* @param group The (optional) group of the recipe, see {@link CraftingRecipe#getGroup()}.
|
||||
* @param category The category the recipe appears in, see {@link CraftingRecipe#category()}.
|
||||
*/
|
||||
public record RecipeProperties(String group, CraftingBookCategory category) {
|
||||
public static RecipeProperties of(CraftingRecipe recipe) {
|
||||
return new RecipeProperties(recipe.getGroup(), recipe.category());
|
||||
}
|
||||
|
||||
public static RecipeProperties fromJson(JsonObject json) {
|
||||
var group = GsonHelper.getAsString(json, "group", "");
|
||||
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
|
||||
return new RecipeProperties(group, category);
|
||||
}
|
||||
|
||||
public static RecipeProperties fromNetwork(FriendlyByteBuf buffer) {
|
||||
var group = buffer.readUtf();
|
||||
var category = buffer.readEnum(CraftingBookCategory.class);
|
||||
return new RecipeProperties(group, category);
|
||||
}
|
||||
|
||||
public void toNetwork(FriendlyByteBuf buffer) {
|
||||
buffer.writeUtf(group());
|
||||
buffer.writeEnum(category());
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
|
||||
public final class RecipeUtil {
|
||||
private RecipeUtil() {
|
||||
}
|
||||
|
||||
public static NonNullList<Ingredient> readIngredients(FriendlyByteBuf buffer) {
|
||||
var count = buffer.readVarInt();
|
||||
var ingredients = NonNullList.withSize(count, Ingredient.EMPTY);
|
||||
for (var i = 0; i < ingredients.size(); i++) ingredients.set(i, Ingredient.fromNetwork(buffer));
|
||||
return ingredients;
|
||||
}
|
||||
|
||||
public static void writeIngredients(FriendlyByteBuf buffer, NonNullList<Ingredient> ingredients) {
|
||||
buffer.writeCollection(ingredients, (a, b) -> b.toNetwork(a));
|
||||
}
|
||||
|
||||
public static NonNullList<Ingredient> readShapelessIngredients(JsonObject json) {
|
||||
NonNullList<Ingredient> ingredients = NonNullList.create();
|
||||
|
||||
var ingredientsList = GsonHelper.getAsJsonArray(json, "ingredients");
|
||||
for (var i = 0; i < ingredientsList.size(); ++i) {
|
||||
var ingredient = Ingredient.fromJson(ingredientsList.get(i));
|
||||
if (!ingredient.isEmpty()) ingredients.add(ingredient);
|
||||
}
|
||||
|
||||
if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
|
||||
if (ingredients.size() > 9) {
|
||||
throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
|
||||
}
|
||||
|
||||
return ingredients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends {@link ShapedRecipe#itemStackFromJson(JsonObject)} with support for the {@code nbt} field.
|
||||
*
|
||||
* @param json The json to extract the item from.
|
||||
* @return The parsed item stack.
|
||||
*/
|
||||
public static ItemStack itemStackFromJson(JsonObject json) {
|
||||
var item = ShapedRecipe.itemFromJson(json);
|
||||
if (json.has("data")) throw new JsonParseException("Disallowed data tag found");
|
||||
|
||||
var count = GsonHelper.getAsInt(json, "count", 1);
|
||||
if (count < 1) throw new JsonSyntaxException("Invalid output count: " + count);
|
||||
|
||||
var stack = new ItemStack(item, count);
|
||||
|
||||
var nbt = json.get("nbt");
|
||||
if (nbt != null) {
|
||||
stack.setTag(Util.getOrThrow(MoreCodecs.TAG.parse(JsonOps.INSTANCE, nbt), JsonParseException::new));
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
|
||||
/**
|
||||
* A description of a {@link ShapedRecipe}.
|
||||
* <p>
|
||||
* This is meant to be used in conjunction with {@link CustomShapedRecipe} for more reusable serialisation and
|
||||
* deserialisation of {@link ShapedRecipe}-like recipes.
|
||||
*
|
||||
* @param properties The common properties of this recipe.
|
||||
* @param template The shaped template of the recipe.
|
||||
* @param result The result of the recipe.
|
||||
*/
|
||||
public record ShapedRecipeSpec(RecipeProperties properties, ShapedTemplate template, ItemStack result) {
|
||||
public static ShapedRecipeSpec fromJson(JsonObject json) {
|
||||
var properties = RecipeProperties.fromJson(json);
|
||||
var template = ShapedTemplate.fromJson(json);
|
||||
var result = RecipeUtil.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
|
||||
return new ShapedRecipeSpec(properties, template, result);
|
||||
}
|
||||
|
||||
public static ShapedRecipeSpec fromNetwork(FriendlyByteBuf buffer) {
|
||||
var properties = RecipeProperties.fromNetwork(buffer);
|
||||
var template = ShapedTemplate.fromNetwork(buffer);
|
||||
var result = buffer.readItem();
|
||||
return new ShapedRecipeSpec(properties, template, result);
|
||||
}
|
||||
|
||||
public void toNetwork(FriendlyByteBuf buffer) {
|
||||
properties().toNetwork(buffer);
|
||||
template().toNetwork(buffer);
|
||||
buffer.writeItem(result());
|
||||
}
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The template for {@linkplain ShapedRecipe shaped recipes}. This largely exists for parsing shaped recipes from JSON.
|
||||
*
|
||||
* @param width The width of the recipe, see {@link ShapedRecipe#getWidth()}.
|
||||
* @param height The height of the recipe, see {@link ShapedRecipe#getHeight()}.
|
||||
* @param ingredients The ingredients in the recipe, see {@link ShapedRecipe#getIngredients()}
|
||||
*/
|
||||
public record ShapedTemplate(int width, int height, NonNullList<Ingredient> ingredients) {
|
||||
public static ShapedTemplate of(ShapedRecipe recipe) {
|
||||
return new ShapedTemplate(recipe.getWidth(), recipe.getHeight(), recipe.getIngredients());
|
||||
}
|
||||
|
||||
public static ShapedTemplate fromJson(JsonObject json) {
|
||||
Map<Character, Ingredient> key = new HashMap<>();
|
||||
for (var entry : GsonHelper.getAsJsonObject(json, "key").entrySet()) {
|
||||
if (entry.getKey().length() != 1) {
|
||||
throw new JsonSyntaxException("Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only).");
|
||||
}
|
||||
if (" ".equals(entry.getKey())) {
|
||||
throw new JsonSyntaxException("Invalid key entry: ' ' is a reserved symbol.");
|
||||
}
|
||||
|
||||
key.put(entry.getKey().charAt(0), Ingredient.fromJson(entry.getValue()));
|
||||
}
|
||||
|
||||
var patternList = GsonHelper.getAsJsonArray(json, "pattern");
|
||||
if (patternList.size() == 0) {
|
||||
throw new JsonSyntaxException("Invalid pattern: empty pattern not allowed");
|
||||
}
|
||||
|
||||
var pattern = new String[patternList.size()];
|
||||
for (var x = 0; x < pattern.length; x++) {
|
||||
var line = GsonHelper.convertToString(patternList.get(x), "pattern[" + x + "]");
|
||||
if (x > 0 && pattern[0].length() != line.length()) {
|
||||
throw new JsonSyntaxException("Invalid pattern: each row must be the same width");
|
||||
}
|
||||
pattern[x] = line;
|
||||
}
|
||||
|
||||
var width = pattern[0].length();
|
||||
var height = pattern.length;
|
||||
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
|
||||
|
||||
Set<Character> missingKeys = new HashSet<>(key.keySet());
|
||||
|
||||
var ingredientIdx = 0;
|
||||
for (var line : pattern) {
|
||||
for (var x = 0; x < line.length(); x++) {
|
||||
var chr = line.charAt(x);
|
||||
var ing = chr == ' ' ? Ingredient.EMPTY : key.get(chr);
|
||||
if (ing == null) {
|
||||
throw new JsonSyntaxException("Pattern references symbol '" + chr + "' but it's not defined in the key");
|
||||
}
|
||||
ingredients.set(ingredientIdx++, ing);
|
||||
missingKeys.remove(chr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingKeys.isEmpty()) {
|
||||
throw new JsonSyntaxException("Key defines symbols that aren't used in pattern: " + missingKeys);
|
||||
}
|
||||
|
||||
return new ShapedTemplate(width, height, ingredients);
|
||||
}
|
||||
|
||||
public static ShapedTemplate fromNetwork(FriendlyByteBuf buffer) {
|
||||
var width = buffer.readVarInt();
|
||||
var height = buffer.readVarInt();
|
||||
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
|
||||
for (var i = 0; i < ingredients.size(); ++i) ingredients.set(i, Ingredient.fromNetwork(buffer));
|
||||
return new ShapedTemplate(width, height, ingredients);
|
||||
}
|
||||
|
||||
public void toNetwork(FriendlyByteBuf buffer) {
|
||||
buffer.writeVarInt(width());
|
||||
buffer.writeVarInt(height());
|
||||
for (var ingredient : ingredients) ingredient.toNetwork(buffer);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
|
||||
|
||||
/**
|
||||
* A description of a {@link ShapelessRecipe}.
|
||||
* <p>
|
||||
* This is meant to be used in conjunction with {@link CustomShapelessRecipe} for more reusable serialisation and
|
||||
* deserialisation of {@link ShapelessRecipe}-like recipes.
|
||||
*
|
||||
* @param properties The common properties of this recipe.
|
||||
* @param ingredients The ingredients of the recipe.
|
||||
* @param result The result of the recipe.
|
||||
*/
|
||||
public record ShapelessRecipeSpec(RecipeProperties properties, NonNullList<Ingredient> ingredients, ItemStack result) {
|
||||
public static ShapelessRecipeSpec fromJson(JsonObject json) {
|
||||
var properties = RecipeProperties.fromJson(json);
|
||||
var ingredients = RecipeUtil.readShapelessIngredients(json);
|
||||
var result = RecipeUtil.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
|
||||
return new ShapelessRecipeSpec(properties, ingredients, result);
|
||||
}
|
||||
|
||||
public static ShapelessRecipeSpec fromNetwork(FriendlyByteBuf buffer) {
|
||||
var properties = RecipeProperties.fromNetwork(buffer);
|
||||
var ingredients = RecipeUtil.readIngredients(buffer);
|
||||
var result = buffer.readItem();
|
||||
|
||||
return new ShapelessRecipeSpec(properties, ingredients, result);
|
||||
}
|
||||
|
||||
public void toNetwork(FriendlyByteBuf buffer) {
|
||||
properties().toNetwork(buffer);
|
||||
RecipeUtil.writeIngredients(buffer, ingredients());
|
||||
buffer.writeItem(result());
|
||||
}
|
||||
}
|
@@ -4,32 +4,30 @@
|
||||
|
||||
package dan200.computercraft.shared.turtle.recipes;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
|
||||
import dan200.computercraft.shared.recipe.ShapelessRecipeSpec;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapelessRecipe;
|
||||
|
||||
/**
|
||||
* A {@link ShapelessRecipe} which sets the {@linkplain TurtleItem#getOverlay(ItemStack)} turtle's overlay} instead.
|
||||
*/
|
||||
public class TurtleOverlayRecipe extends ShapelessRecipe {
|
||||
public class TurtleOverlayRecipe extends CustomShapelessRecipe {
|
||||
private final ResourceLocation overlay;
|
||||
private final ItemStack result;
|
||||
|
||||
public TurtleOverlayRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients, ResourceLocation overlay) {
|
||||
super(id, group, category, result, ingredients);
|
||||
public TurtleOverlayRecipe(ResourceLocation id, ShapelessRecipeSpec spec, ResourceLocation overlay) {
|
||||
super(id, spec);
|
||||
this.overlay = overlay;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
private static ItemStack make(ItemStack stack, ResourceLocation overlay) {
|
||||
@@ -56,63 +54,29 @@ public class TurtleOverlayRecipe extends ShapelessRecipe {
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getSerializer() {
|
||||
public RecipeSerializer<TurtleOverlayRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
|
||||
}
|
||||
|
||||
public static class Serializer implements RecipeSerializer<TurtleOverlayRecipe> {
|
||||
@Override
|
||||
public TurtleOverlayRecipe fromJson(ResourceLocation id, JsonObject json) {
|
||||
var group = GsonHelper.getAsString(json, "group", "");
|
||||
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
|
||||
var ingredients = readIngredients(GsonHelper.getAsJsonArray(json, "ingredients"));
|
||||
|
||||
if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
|
||||
if (ingredients.size() > 9) {
|
||||
throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
|
||||
}
|
||||
|
||||
var recipe = ShapelessRecipeSpec.fromJson(json);
|
||||
var overlay = new ResourceLocation(GsonHelper.getAsString(json, "overlay"));
|
||||
|
||||
// We could derive this from the ingredients, but we want to avoid evaluating the ingredients too early, so
|
||||
// it's easier to do this.
|
||||
var result = make(ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")), overlay);
|
||||
|
||||
return new TurtleOverlayRecipe(id, group, category, result, ingredients, overlay);
|
||||
}
|
||||
|
||||
private NonNullList<Ingredient> readIngredients(JsonArray arrays) {
|
||||
NonNullList<Ingredient> items = NonNullList.create();
|
||||
for (var i = 0; i < arrays.size(); ++i) {
|
||||
var ingredient = Ingredient.fromJson(arrays.get(i));
|
||||
if (!ingredient.isEmpty()) items.add(ingredient);
|
||||
}
|
||||
|
||||
return items;
|
||||
return new TurtleOverlayRecipe(id, recipe, overlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TurtleOverlayRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
|
||||
var group = buffer.readUtf();
|
||||
var category = buffer.readEnum(CraftingBookCategory.class);
|
||||
var count = buffer.readVarInt();
|
||||
var items = NonNullList.withSize(count, Ingredient.EMPTY);
|
||||
|
||||
for (var j = 0; j < items.size(); j++) items.set(j, Ingredient.fromNetwork(buffer));
|
||||
var result = buffer.readItem();
|
||||
var recipe = ShapelessRecipeSpec.fromNetwork(buffer);
|
||||
var overlay = buffer.readResourceLocation();
|
||||
|
||||
return new TurtleOverlayRecipe(id, group, category, result, items, overlay);
|
||||
return new TurtleOverlayRecipe(id, recipe, overlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buffer, TurtleOverlayRecipe recipe) {
|
||||
buffer.writeUtf(recipe.getGroup());
|
||||
buffer.writeEnum(recipe.category());
|
||||
buffer.writeVarInt(recipe.getIngredients().size());
|
||||
|
||||
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
|
||||
buffer.writeItem(recipe.result);
|
||||
recipe.toSpec().toNetwork(buffer);
|
||||
buffer.writeResourceLocation(recipe.overlay);
|
||||
}
|
||||
}
|
||||
|
@@ -4,26 +4,33 @@
|
||||
|
||||
package dan200.computercraft.shared.turtle.recipes;
|
||||
|
||||
import com.mojang.serialization.DataResult;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
||||
import dan200.computercraft.shared.computer.recipe.ComputerFamilyRecipe;
|
||||
import dan200.computercraft.shared.computer.recipe.ComputerConvertRecipe;
|
||||
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
|
||||
public final class TurtleRecipe extends ComputerFamilyRecipe {
|
||||
public TurtleRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
||||
super(identifier, group, category, width, height, ingredients, result, family);
|
||||
/**
|
||||
* The recipe which crafts a turtle from an existing computer item.
|
||||
*/
|
||||
public final class TurtleRecipe extends ComputerConvertRecipe {
|
||||
private final TurtleItem turtle;
|
||||
|
||||
private TurtleRecipe(ResourceLocation id, ShapedRecipeSpec recipe, TurtleItem turtle) {
|
||||
super(id, recipe);
|
||||
this.turtle = turtle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.TURTLE.get();
|
||||
public static DataResult<TurtleRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
|
||||
if (!(recipe.result().getItem() instanceof TurtleItem turtle)) {
|
||||
return DataResult.error(() -> recipe.result().getItem() + " is not a turtle item");
|
||||
}
|
||||
|
||||
return DataResult.success(new TurtleRecipe(id, recipe, turtle));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,13 +38,11 @@ public final class TurtleRecipe extends ComputerFamilyRecipe {
|
||||
var computerID = item.getComputerID(stack);
|
||||
var label = item.getLabel(stack);
|
||||
|
||||
return TurtleItem.create(computerID, label, -1, getFamily(), null, null, 0, null);
|
||||
return turtle.create(computerID, label, -1, null, null, 0, null);
|
||||
}
|
||||
|
||||
public static class Serializer extends ComputerFamilyRecipe.Serializer<TurtleRecipe> {
|
||||
@Override
|
||||
protected TurtleRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
|
||||
return new TurtleRecipe(identifier, group, category, width, height, ingredients, result, family);
|
||||
}
|
||||
@Override
|
||||
public RecipeSerializer<TurtleRecipe> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.TURTLE.get();
|
||||
}
|
||||
}
|
||||
|
@@ -1,88 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class ImpostorRecipe extends ShapedRecipe {
|
||||
private final String group;
|
||||
private final ItemStack result;
|
||||
|
||||
private ImpostorRecipe(ResourceLocation id, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
|
||||
super(id, group, category, width, height, ingredients, result);
|
||||
this.group = group;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
ItemStack getResultItem() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingContainer inv, Level world) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAccess) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get();
|
||||
}
|
||||
|
||||
public static class Serializer implements RecipeSerializer<ImpostorRecipe> {
|
||||
@Override
|
||||
public ImpostorRecipe fromJson(ResourceLocation identifier, JsonObject json) {
|
||||
var group = GsonHelper.getAsString(json, "group", "");
|
||||
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
|
||||
var recipe = RecipeSerializer.SHAPED_RECIPE.fromJson(identifier, json);
|
||||
var result = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
|
||||
return new ImpostorRecipe(identifier, group, category, recipe.getWidth(), recipe.getHeight(), recipe.getIngredients(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImpostorRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
|
||||
var width = buf.readVarInt();
|
||||
var height = buf.readVarInt();
|
||||
var group = buf.readUtf(Short.MAX_VALUE);
|
||||
var category = buf.readEnum(CraftingBookCategory.class);
|
||||
var items = NonNullList.withSize(width * height, Ingredient.EMPTY);
|
||||
for (var k = 0; k < items.size(); k++) items.set(k, Ingredient.fromNetwork(buf));
|
||||
var result = buf.readItem();
|
||||
return new ImpostorRecipe(identifier, group, category, width, height, items, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buf, ImpostorRecipe recipe) {
|
||||
buf.writeVarInt(recipe.getWidth());
|
||||
buf.writeVarInt(recipe.getHeight());
|
||||
buf.writeUtf(recipe.getGroup());
|
||||
buf.writeEnum(recipe.category());
|
||||
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buf);
|
||||
buf.writeItem(recipe.getResultItem());
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class ImpostorShapelessRecipe extends ShapelessRecipe {
|
||||
private final String group;
|
||||
private final ItemStack result;
|
||||
|
||||
private ImpostorShapelessRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients) {
|
||||
super(id, group, category, result, ingredients);
|
||||
this.group = group;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
ItemStack getResultItem() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingContainer inv, Level world) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingContainer inventory, RegistryAccess access) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get();
|
||||
}
|
||||
|
||||
public static final class Serializer implements RecipeSerializer<ImpostorShapelessRecipe> {
|
||||
@Override
|
||||
public ImpostorShapelessRecipe fromJson(ResourceLocation id, JsonObject json) {
|
||||
var group = GsonHelper.getAsString(json, "group", "");
|
||||
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
|
||||
var ingredients = readIngredients(GsonHelper.getAsJsonArray(json, "ingredients"));
|
||||
|
||||
if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
|
||||
if (ingredients.size() > 9) {
|
||||
throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
|
||||
}
|
||||
|
||||
var result = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
|
||||
return new ImpostorShapelessRecipe(id, group, category, result, ingredients);
|
||||
}
|
||||
|
||||
private NonNullList<Ingredient> readIngredients(JsonArray arrays) {
|
||||
NonNullList<Ingredient> items = NonNullList.create();
|
||||
for (var i = 0; i < arrays.size(); ++i) {
|
||||
var ingredient = Ingredient.fromJson(arrays.get(i));
|
||||
if (!ingredient.isEmpty()) items.add(ingredient);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImpostorShapelessRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
|
||||
var group = buffer.readUtf();
|
||||
var category = buffer.readEnum(CraftingBookCategory.class);
|
||||
var count = buffer.readVarInt();
|
||||
var items = NonNullList.withSize(count, Ingredient.EMPTY);
|
||||
|
||||
for (var j = 0; j < items.size(); j++) items.set(j, Ingredient.fromNetwork(buffer));
|
||||
var result = buffer.readItem();
|
||||
|
||||
return new ImpostorShapelessRecipe(id, group, category, result, items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toNetwork(FriendlyByteBuf buffer, ImpostorShapelessRecipe recipe) {
|
||||
buffer.writeUtf(recipe.getGroup());
|
||||
buffer.writeEnum(recipe.category());
|
||||
buffer.writeVarInt(recipe.getIngredients().size());
|
||||
|
||||
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
|
||||
buffer.writeItem(recipe.getResultItem());
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,94 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
// TODO: Replace some things with Forge??
|
||||
|
||||
public final class RecipeUtil {
|
||||
private RecipeUtil() {
|
||||
}
|
||||
|
||||
public record ShapedTemplate(int width, int height, NonNullList<Ingredient> ingredients) {
|
||||
}
|
||||
|
||||
public static ShapedTemplate getTemplate(JsonObject json) {
|
||||
Map<Character, Ingredient> ingMap = Maps.newHashMap();
|
||||
for (var entry : GsonHelper.getAsJsonObject(json, "key").entrySet()) {
|
||||
if (entry.getKey().length() != 1) {
|
||||
throw new JsonSyntaxException("Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only).");
|
||||
}
|
||||
if (" ".equals(entry.getKey())) {
|
||||
throw new JsonSyntaxException("Invalid key entry: ' ' is a reserved symbol.");
|
||||
}
|
||||
|
||||
ingMap.put(entry.getKey().charAt(0), Ingredient.fromJson(entry.getValue()));
|
||||
}
|
||||
|
||||
ingMap.put(' ', Ingredient.EMPTY);
|
||||
|
||||
var patternJ = GsonHelper.getAsJsonArray(json, "pattern");
|
||||
|
||||
if (patternJ.size() == 0) {
|
||||
throw new JsonSyntaxException("Invalid pattern: empty pattern not allowed");
|
||||
}
|
||||
|
||||
var pattern = new String[patternJ.size()];
|
||||
for (var x = 0; x < pattern.length; x++) {
|
||||
var line = GsonHelper.convertToString(patternJ.get(x), "pattern[" + x + "]");
|
||||
if (x > 0 && pattern[0].length() != line.length()) {
|
||||
throw new JsonSyntaxException("Invalid pattern: each row must be the same width");
|
||||
}
|
||||
pattern[x] = line;
|
||||
}
|
||||
|
||||
var width = pattern[0].length();
|
||||
var height = pattern.length;
|
||||
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
|
||||
|
||||
Set<Character> missingKeys = Sets.newHashSet(ingMap.keySet());
|
||||
missingKeys.remove(' ');
|
||||
|
||||
var ingredientIdx = 0;
|
||||
for (var line : pattern) {
|
||||
for (var i = 0; i < line.length(); i++) {
|
||||
var chr = line.charAt(i);
|
||||
|
||||
var ing = ingMap.get(chr);
|
||||
if (ing == null) {
|
||||
throw new JsonSyntaxException("Pattern references symbol '" + chr + "' but it's not defined in the key");
|
||||
}
|
||||
ingredients.set(ingredientIdx++, ing);
|
||||
missingKeys.remove(chr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingKeys.isEmpty()) {
|
||||
throw new JsonSyntaxException("Key defines symbols that aren't used in pattern: " + missingKeys);
|
||||
}
|
||||
|
||||
return new ShapedTemplate(width, height, ingredients);
|
||||
}
|
||||
|
||||
public static ComputerFamily getFamily(JsonObject json, String name) {
|
||||
var familyName = GsonHelper.getAsString(json, name);
|
||||
for (var family : ComputerFamily.values()) {
|
||||
if (family.name().equalsIgnoreCase(familyName)) return family;
|
||||
}
|
||||
|
||||
throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import dan200.computercraft.test.shared.MinecraftArbitraries;
|
||||
import it.unimi.dsi.fastutil.ints.IntIntImmutablePair;
|
||||
import net.jqwik.api.Arbitraries;
|
||||
import net.jqwik.api.Arbitrary;
|
||||
import net.jqwik.api.Combinators;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
|
||||
/**
|
||||
* {@link Arbitrary} implementations for recipes.
|
||||
*/
|
||||
public final class RecipeArbitraries {
|
||||
public static Arbitrary<RecipeProperties> recipeProperties() {
|
||||
return Combinators.combine(
|
||||
Arbitraries.strings().ofMinLength(1).withChars("abcdefghijklmnopqrstuvwxyz_"),
|
||||
Arbitraries.of(CraftingBookCategory.values())
|
||||
).as(RecipeProperties::new);
|
||||
}
|
||||
|
||||
public static Arbitrary<ShapelessRecipeSpec> shapelessRecipeSpec() {
|
||||
return Combinators.combine(
|
||||
recipeProperties(),
|
||||
MinecraftArbitraries.ingredient().array(Ingredient[].class).ofMinSize(1).map(x -> NonNullList.of(Ingredient.EMPTY, x)),
|
||||
MinecraftArbitraries.nonEmptyItemStack()
|
||||
).as(ShapelessRecipeSpec::new);
|
||||
}
|
||||
|
||||
public static Arbitrary<ShapedTemplate> shapedTemplate() {
|
||||
return Combinators.combine(Arbitraries.integers().between(1, 3), Arbitraries.integers().between(1, 3))
|
||||
.as(IntIntImmutablePair::new)
|
||||
.flatMap(x -> MinecraftArbitraries.ingredient().array(Ingredient[].class).ofSize(x.leftInt() * x.rightInt())
|
||||
.map(i -> new ShapedTemplate(x.leftInt(), x.rightInt(), NonNullList.of(Ingredient.EMPTY, i)))
|
||||
);
|
||||
}
|
||||
|
||||
public static Arbitrary<ShapedRecipeSpec> shapedRecipeSpec() {
|
||||
return Combinators.combine(
|
||||
recipeProperties(),
|
||||
shapedTemplate(),
|
||||
MinecraftArbitraries.nonEmptyItemStack()
|
||||
).as(ShapedRecipeSpec::new);
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import dan200.computercraft.test.core.StructuralEquality;
|
||||
import dan200.computercraft.test.shared.MinecraftEqualities;
|
||||
|
||||
/**
|
||||
* {@link StructuralEquality} implementations for recipes.
|
||||
*/
|
||||
public final class RecipeEqualities {
|
||||
private RecipeEqualities() {
|
||||
}
|
||||
|
||||
public static final StructuralEquality<ShapelessRecipeSpec> shapelessRecipeSpec = StructuralEquality.all(
|
||||
StructuralEquality.at("properties", ShapelessRecipeSpec::properties),
|
||||
StructuralEquality.at("ingredients", ShapelessRecipeSpec::ingredients, MinecraftEqualities.ingredient.list()),
|
||||
StructuralEquality.at("result", ShapelessRecipeSpec::result, MinecraftEqualities.itemStack)
|
||||
);
|
||||
|
||||
public static final StructuralEquality<ShapedTemplate> shapedTemplate = StructuralEquality.all(
|
||||
StructuralEquality.at("width", ShapedTemplate::width),
|
||||
StructuralEquality.at("height", ShapedTemplate::height),
|
||||
StructuralEquality.at("ingredients", ShapedTemplate::ingredients, MinecraftEqualities.ingredient.list())
|
||||
);
|
||||
|
||||
public static final StructuralEquality<ShapedRecipeSpec> shapedRecipeSpec = StructuralEquality.all(
|
||||
StructuralEquality.at("properties", ShapedRecipeSpec::properties),
|
||||
StructuralEquality.at("ingredients", ShapedRecipeSpec::template, shapedTemplate),
|
||||
StructuralEquality.at("result", ShapedRecipeSpec::result, MinecraftEqualities.itemStack)
|
||||
);
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import dan200.computercraft.test.shared.NetworkSupport;
|
||||
import dan200.computercraft.test.shared.WithMinecraft;
|
||||
import net.jqwik.api.Arbitrary;
|
||||
import net.jqwik.api.ForAll;
|
||||
import net.jqwik.api.Property;
|
||||
import net.jqwik.api.Provide;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
@WithMinecraft
|
||||
public class ShapedRecipeSpecTest {
|
||||
static {
|
||||
WithMinecraft.Setup.bootstrap(); // @Property doesn't run test lifecycle methods.
|
||||
}
|
||||
|
||||
@Property
|
||||
public void testRoundTrip(@ForAll("recipe") ShapedRecipeSpec spec) {
|
||||
var converted = NetworkSupport.roundTrip(spec, ShapedRecipeSpec::toNetwork, ShapedRecipeSpec::fromNetwork);
|
||||
assertThat("Recipes are equal", converted, RecipeEqualities.shapedRecipeSpec.asMatcher(ShapedRecipeSpec.class, spec));
|
||||
}
|
||||
|
||||
@Provide
|
||||
Arbitrary<ShapedRecipeSpec> recipe() {
|
||||
return RecipeArbitraries.shapedRecipeSpec();
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.recipe;
|
||||
|
||||
import dan200.computercraft.test.shared.NetworkSupport;
|
||||
import dan200.computercraft.test.shared.WithMinecraft;
|
||||
import net.jqwik.api.Arbitrary;
|
||||
import net.jqwik.api.ForAll;
|
||||
import net.jqwik.api.Property;
|
||||
import net.jqwik.api.Provide;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
@WithMinecraft
|
||||
public class ShapelessRecipeSpecTest {
|
||||
static {
|
||||
WithMinecraft.Setup.bootstrap(); // @Property doesn't run test lifecycle methods.
|
||||
}
|
||||
|
||||
@Property
|
||||
public void testRoundTrip(@ForAll("recipe") ShapelessRecipeSpec spec) {
|
||||
var converted = NetworkSupport.roundTrip(spec, ShapelessRecipeSpec::toNetwork, ShapelessRecipeSpec::fromNetwork);
|
||||
assertThat("Recipes are equal", converted, RecipeEqualities.shapelessRecipeSpec.asMatcher(ShapelessRecipeSpec.class, spec));
|
||||
}
|
||||
|
||||
@Provide
|
||||
Arbitrary<ShapelessRecipeSpec> recipe() {
|
||||
return RecipeArbitraries.shapelessRecipeSpec();
|
||||
}
|
||||
}
|
@@ -8,16 +8,13 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleToolDurability;
|
||||
import dan200.computercraft.test.core.StructuralEquality;
|
||||
import dan200.computercraft.test.shared.MinecraftArbitraries;
|
||||
import dan200.computercraft.test.shared.MinecraftEqualities;
|
||||
import dan200.computercraft.test.shared.NetworkSupport;
|
||||
import dan200.computercraft.test.shared.WithMinecraft;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.jqwik.api.*;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.hamcrest.Description;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@WithMinecraft
|
||||
class TurtleToolSerialiserTest {
|
||||
@@ -32,11 +29,9 @@ class TurtleToolSerialiserTest {
|
||||
*/
|
||||
@Property
|
||||
public void testRoundTrip(@ForAll("tool") TurtleTool tool) {
|
||||
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
|
||||
TurtleToolSerialiser.INSTANCE.toNetwork(buffer, tool);
|
||||
|
||||
var converted = TurtleToolSerialiser.INSTANCE.fromNetwork(tool.getUpgradeID(), buffer);
|
||||
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
|
||||
var converted = NetworkSupport.roundTripSerialiser(
|
||||
tool.getUpgradeID(), tool, TurtleToolSerialiser.INSTANCE::toNetwork, TurtleToolSerialiser.INSTANCE::fromNetwork
|
||||
);
|
||||
|
||||
if (!equality.equals(tool, converted)) {
|
||||
System.out.println("Break");
|
||||
@@ -58,22 +53,10 @@ class TurtleToolSerialiserTest {
|
||||
).as(TurtleTool::new);
|
||||
}
|
||||
|
||||
private static final StructuralEquality<ItemStack> stackEquality = new StructuralEquality<>() {
|
||||
@Override
|
||||
public boolean equals(ItemStack left, ItemStack right) {
|
||||
return ItemStack.isSameItemSameTags(left, right) && left.getCount() == right.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describe(Description description, ItemStack object) {
|
||||
description.appendValue(object).appendValue(object.getTag());
|
||||
}
|
||||
};
|
||||
|
||||
private static final StructuralEquality<TurtleTool> equality = StructuralEquality.all(
|
||||
StructuralEquality.at("id", ITurtleUpgrade::getUpgradeID),
|
||||
StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, stackEquality),
|
||||
StructuralEquality.at("tool", x -> x.item, stackEquality),
|
||||
StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, MinecraftEqualities.itemStack),
|
||||
StructuralEquality.at("tool", x -> x.item, MinecraftEqualities.itemStack),
|
||||
StructuralEquality.at("damageMulitiplier", x -> x.damageMulitiplier),
|
||||
StructuralEquality.at("allowEnchantments", x -> x.allowEnchantments),
|
||||
StructuralEquality.at("consumeDurability", x -> x.consumeDurability),
|
||||
|
@@ -17,6 +17,7 @@ import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -44,6 +45,10 @@ public final class MinecraftArbitraries {
|
||||
return Arbitraries.oneOf(List.of(Arbitraries.just(ItemStack.EMPTY), nonEmptyItemStack()));
|
||||
}
|
||||
|
||||
public static Arbitrary<Ingredient> ingredient() {
|
||||
return nonEmptyItemStack().list().ofMinSize(1).map(x -> Ingredient.of(x.stream()));
|
||||
}
|
||||
|
||||
public static Arbitrary<BlockPos> blockPos() {
|
||||
// BlockPos has a maximum range that can be sent over the network - use those.
|
||||
var xz = Arbitraries.integers().between(-3_000_000, -3_000_000);
|
||||
|
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.shared;
|
||||
|
||||
import dan200.computercraft.test.core.StructuralEquality;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import org.hamcrest.Description;
|
||||
|
||||
/**
|
||||
* {@link StructuralEquality} implementations for Minecraft types.
|
||||
*/
|
||||
public class MinecraftEqualities {
|
||||
public static final StructuralEquality<ItemStack> itemStack = new StructuralEquality<>() {
|
||||
@Override
|
||||
public boolean equals(ItemStack left, ItemStack right) {
|
||||
return ItemStack.isSameItemSameTags(left, right) && left.getCount() == right.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describe(Description description, ItemStack object) {
|
||||
description.appendValue(object).appendValue(object.getTag());
|
||||
}
|
||||
};
|
||||
|
||||
public static final StructuralEquality<Ingredient> ingredient = new StructuralEquality<>() {
|
||||
@Override
|
||||
public boolean equals(Ingredient left, Ingredient right) {
|
||||
return left.toJson().equals(right.toJson());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describe(Description description, Ingredient object) {
|
||||
description.appendValue(object.toJson());
|
||||
}
|
||||
};
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.shared;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Support methods for working with Minecraft's networking code.
|
||||
*/
|
||||
public final class NetworkSupport {
|
||||
private NetworkSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to serialise and then deserialise a value.
|
||||
*
|
||||
* @param value The value to serialise.
|
||||
* @param write Serialise this value to a buffer.
|
||||
* @param read Deserialise this value from a buffer.
|
||||
* @param <T> The type of the value to round trip.
|
||||
* @return The converted value, for checking equivalency.
|
||||
*/
|
||||
public static <T> T roundTrip(T value, BiConsumer<T, FriendlyByteBuf> write, Function<FriendlyByteBuf, T> read) {
|
||||
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
|
||||
write.accept(value, buffer);
|
||||
|
||||
var converted = read.apply(buffer);
|
||||
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
|
||||
return converted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to serialise and then deserialise a value from a {@link RecipeSerializer}-like interface.
|
||||
*
|
||||
* @param id The id of this value.
|
||||
* @param value The value to serialise.
|
||||
* @param write Serialise this value to a buffer.
|
||||
* @param read Deserialise this value from a buffer.
|
||||
* @param <T> The type of the value to round trip.
|
||||
* @return The converted value, for checking equivalency.
|
||||
*/
|
||||
public static <T> T roundTripSerialiser(ResourceLocation id, T value, BiConsumer<FriendlyByteBuf, T> write, BiFunction<ResourceLocation, FriendlyByteBuf, T> read) {
|
||||
return roundTrip(value, (x, b) -> write.accept(b, x), b -> read.apply(id, b));
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import com.mojang.authlib.GameProfile
|
||||
import dan200.computercraft.gametest.api.Structures
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
@@ -11,6 +12,7 @@ import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestAssertException
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.nbt.NbtUtils
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu
|
||||
import net.minecraft.world.inventory.MenuType
|
||||
@@ -41,11 +43,10 @@ class Recipe_Test {
|
||||
|
||||
val result = recipe.get().assemble(container, context.level.registryAccess())
|
||||
|
||||
val owner = CompoundTag()
|
||||
owner.putString("Name", "dan200")
|
||||
owner.putString("Id", "f3c8d69b-0776-4512-8434-d1b2165909eb")
|
||||
val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")
|
||||
|
||||
val tag = CompoundTag()
|
||||
tag.put("SkullOwner", owner)
|
||||
tag.put("SkullOwner", NbtUtils.writeGameProfile(CompoundTag(), profile))
|
||||
|
||||
assertEquals(tag, result.tag, "Expected NBT tags to be the same")
|
||||
}
|
||||
|
@@ -18,9 +18,7 @@ val docApi by configurations.registering {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnlyApi(libs.jsr305)
|
||||
compileOnlyApi(libs.checkerFramework)
|
||||
compileOnlyApi(libs.jetbrainsAnnotations)
|
||||
compileOnlyApi(libs.bundles.annotations)
|
||||
|
||||
"docApi"(project(":common-api"))
|
||||
}
|
||||
|
@@ -12,25 +12,30 @@ import java.time.Instant;
|
||||
/**
|
||||
* A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
|
||||
*
|
||||
* @param isDirectory Whether this filesystem entry is a directory.
|
||||
* @param size The size of the file.
|
||||
* @param isDirectory Whether this filesystem entry is a directory.
|
||||
* @param size The size of the file.
|
||||
* @param creationTime The time the file was created.
|
||||
* @param lastModifiedTime The time the file was last modified.
|
||||
*/
|
||||
public record FileAttributes(boolean isDirectory, long size) implements BasicFileAttributes {
|
||||
public record FileAttributes(
|
||||
boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
|
||||
) implements BasicFileAttributes {
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
@Override
|
||||
public FileTime lastModifiedTime() {
|
||||
return EPOCH;
|
||||
/**
|
||||
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
|
||||
* {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch.
|
||||
*
|
||||
* @param isDirectory Whether the filesystem entry is a directory.
|
||||
* @param size The size of the file.
|
||||
*/
|
||||
public FileAttributes(boolean isDirectory, long size) {
|
||||
this(isDirectory, size, EPOCH, EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastAccessTime() {
|
||||
return EPOCH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime creationTime() {
|
||||
return EPOCH;
|
||||
return lastModifiedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -488,9 +487,9 @@ public class FSAPI implements ILuaAPI {
|
||||
try (var ignored = environment.time(Metrics.FS_OPS)) {
|
||||
var attributes = getFileSystem().getAttributes(path);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("modification", getFileTime(attributes.lastModifiedTime()));
|
||||
result.put("modified", getFileTime(attributes.lastModifiedTime()));
|
||||
result.put("created", getFileTime(attributes.creationTime()));
|
||||
result.put("modification", attributes.lastModifiedTime().toMillis());
|
||||
result.put("modified", attributes.lastModifiedTime().toMillis());
|
||||
result.put("created", attributes.creationTime().toMillis());
|
||||
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
||||
result.put("isDir", attributes.isDirectory());
|
||||
result.put("isReadOnly", getFileSystem().isReadOnly(path));
|
||||
@@ -499,8 +498,4 @@ public class FSAPI implements ILuaAPI {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static long getFileTime(@Nullable FileTime time) {
|
||||
return time == null ? 0 : time.toMillis();
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.http.*;
|
||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import dan200.computercraft.core.apis.http.websocket.WebsocketClient;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
@@ -165,7 +166,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
var timeout = getTimeout(timeoutArg);
|
||||
|
||||
try {
|
||||
var uri = Websocket.checkUri(address);
|
||||
var uri = WebsocketClient.parseUri(address);
|
||||
if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
|
||||
throw new LuaException("Too many websockets already open");
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ import java.util.List;
|
||||
* end
|
||||
* }</pre>
|
||||
* <p>
|
||||
* [comparator]: https://minecraft.gamepedia.com/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on
|
||||
* [comparator]: https://minecraft.wiki/w/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on
|
||||
* the Minecraft wiki."
|
||||
* @cc.module redstone
|
||||
*/
|
||||
|
@@ -77,12 +77,10 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
var read = channel.read(buffer);
|
||||
if (read < 0) return null;
|
||||
buffer.flip();
|
||||
|
||||
// If we failed to read "enough" here, let's just abort
|
||||
if (read >= count || read < BUFFER_SIZE) {
|
||||
buffer.flip();
|
||||
return new Object[]{ buffer };
|
||||
}
|
||||
if (read >= count || read < BUFFER_SIZE) return new Object[]{ buffer };
|
||||
|
||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||
// than doubling up the buffer each time.
|
||||
@@ -90,11 +88,13 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||
parts.add(buffer);
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
buffer = ByteBuffer.allocateDirect(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
read = channel.read(buffer);
|
||||
if (read < 0) break;
|
||||
buffer.flip();
|
||||
|
||||
totalRead += read;
|
||||
assert read == buffer.remaining();
|
||||
parts.add(buffer);
|
||||
}
|
||||
|
||||
@@ -102,9 +102,11 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
var bytes = new byte[totalRead];
|
||||
var pos = 0;
|
||||
for (var part : parts) {
|
||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
||||
pos += part.position();
|
||||
int length = part.remaining();
|
||||
part.get(bytes, pos, length);
|
||||
pos += length;
|
||||
}
|
||||
assert pos == totalRead;
|
||||
return new Object[]{ bytes };
|
||||
}
|
||||
} else {
|
||||
|
@@ -134,7 +134,7 @@ public final class NetworkUtils {
|
||||
*/
|
||||
public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
|
||||
var options = AddressRule.apply(CoreConfig.httpRules, host, address);
|
||||
if (options.action == Action.DENY) throw new HTTPRequestException("Domain not permitted");
|
||||
if (options.action() == Action.DENY) throw new HTTPRequestException("Domain not permitted");
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ public final class NetworkUtils {
|
||||
* @throws HTTPRequestException If a proxy is required but not configured correctly.
|
||||
*/
|
||||
public static @Nullable Consumer<SocketChannel> getProxyHandler(Options options, int timeout) throws HTTPRequestException {
|
||||
if (!options.useProxy) return null;
|
||||
if (!options.useProxy()) return null;
|
||||
|
||||
var type = CoreConfig.httpProxyType;
|
||||
var host = CoreConfig.httpProxyHost;
|
||||
|
@@ -6,20 +6,13 @@ package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
|
||||
/**
|
||||
* Options about a specific domain.
|
||||
* Options for a given HTTP request or websocket, which control its resource constraints.
|
||||
*
|
||||
* @param action Whether to {@link Action#ALLOW} or {@link Action#DENY} this request.
|
||||
* @param maxUpload The maximum size of the HTTP request.
|
||||
* @param maxDownload The maximum size of the HTTP response.
|
||||
* @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
|
||||
* @param useProxy Whether to use the configured proxy.
|
||||
*/
|
||||
public final class Options {
|
||||
public final Action action;
|
||||
public final long maxUpload;
|
||||
public final long maxDownload;
|
||||
public final int websocketMessage;
|
||||
public final boolean useProxy;
|
||||
|
||||
Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||
this.action = action;
|
||||
this.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.websocketMessage = websocketMessage;
|
||||
this.useProxy = useProxy;
|
||||
}
|
||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ public final class PartialOptions {
|
||||
this.useProxy = useProxy;
|
||||
}
|
||||
|
||||
Options toOptions() {
|
||||
public Options toOptions() {
|
||||
if (options != null) return options;
|
||||
|
||||
return options = new Options(
|
||||
|
@@ -130,7 +130,7 @@ public class HttpRequest extends Resource<HttpRequest> {
|
||||
if (isClosed()) return;
|
||||
|
||||
var requestBody = getHeaderSize(headers) + postBuffer.capacity();
|
||||
if (options.maxUpload != 0 && requestBody > options.maxUpload) {
|
||||
if (options.maxUpload() != 0 && requestBody > options.maxUpload()) {
|
||||
failure("Request body is too large");
|
||||
return;
|
||||
}
|
||||
|
@@ -136,7 +136,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
var partial = content.content();
|
||||
if (partial.isReadable()) {
|
||||
// If we've read more than we're allowed to handle, abort as soon as possible.
|
||||
if (options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload) {
|
||||
if (options.maxDownload() != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload()) {
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
|
@@ -16,8 +16,8 @@ import java.net.URI;
|
||||
* A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
|
||||
* original HTTP request.
|
||||
*/
|
||||
public class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
|
||||
public NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
||||
class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
|
||||
NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
||||
super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength);
|
||||
}
|
||||
|
||||
|
@@ -12,8 +12,9 @@ import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@@ -23,21 +24,22 @@ import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Provides functionality to verify and connect to a remote websocket.
|
||||
*/
|
||||
public class Websocket extends Resource<Websocket> {
|
||||
public class Websocket extends Resource<Websocket> implements WebsocketClient {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Websocket.class);
|
||||
|
||||
/**
|
||||
@@ -46,14 +48,8 @@ public class Websocket extends Resource<Websocket> {
|
||||
*/
|
||||
public static final int MAX_MESSAGE_SIZE = 1 << 30;
|
||||
|
||||
static final String SUCCESS_EVENT = "websocket_success";
|
||||
static final String FAILURE_EVENT = "websocket_failure";
|
||||
static final String CLOSE_EVENT = "websocket_closed";
|
||||
static final String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
private @Nullable Future<?> executorFuture;
|
||||
private @Nullable ChannelFuture connectFuture;
|
||||
private @Nullable WeakReference<WebsocketHandle> websocketHandle;
|
||||
private @Nullable ChannelFuture channelFuture;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final URI uri;
|
||||
@@ -70,38 +66,6 @@ public class Websocket extends Resource<Websocket> {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public static URI checkUri(String address) throws HTTPRequestException {
|
||||
URI uri = null;
|
||||
try {
|
||||
uri = new URI(address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + uri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
} else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
|
||||
throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
if (isClosed()) return;
|
||||
executorFuture = NetworkUtils.EXECUTOR.submit(this::doConnect);
|
||||
@@ -122,7 +86,7 @@ public class Websocket extends Resource<Websocket> {
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if (isClosed()) return;
|
||||
|
||||
connectFuture = new Bootstrap()
|
||||
channelFuture = new Bootstrap()
|
||||
.group(NetworkUtils.LOOP_GROUP)
|
||||
.channel(NioSocketChannel.class)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@@ -133,7 +97,7 @@ public class Websocket extends Resource<Websocket> {
|
||||
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
||||
var handshaker = new NoOriginWebSocketHandshaker(
|
||||
uri, WebSocketVersion.V13, subprotocol, true, headers,
|
||||
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
|
||||
options.websocketMessage() <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage()
|
||||
);
|
||||
|
||||
var p = ch.pipeline();
|
||||
@@ -162,12 +126,12 @@ public class Websocket extends Resource<Websocket> {
|
||||
}
|
||||
}
|
||||
|
||||
void success(Channel channel, Options options) {
|
||||
void success(Options options) {
|
||||
if (isClosed()) return;
|
||||
|
||||
var handle = new WebsocketHandle(this, options, channel);
|
||||
var handle = new WebsocketHandle(environment, address, this, options);
|
||||
environment().queueEvent(SUCCESS_EVENT, address, handle);
|
||||
websocketHandle = createOwnerReference(handle);
|
||||
createOwnerReference(handle);
|
||||
|
||||
checkClosed();
|
||||
}
|
||||
@@ -189,19 +153,35 @@ public class Websocket extends Resource<Websocket> {
|
||||
super.dispose();
|
||||
|
||||
executorFuture = closeFuture(executorFuture);
|
||||
connectFuture = closeChannel(connectFuture);
|
||||
|
||||
var websocketHandleRef = websocketHandle;
|
||||
var websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
||||
IoUtil.closeQuietly(websocketHandle);
|
||||
this.websocketHandle = null;
|
||||
channelFuture = closeChannel(channelFuture);
|
||||
}
|
||||
|
||||
public IAPIEnvironment environment() {
|
||||
IAPIEnvironment environment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
public String address() {
|
||||
String address() {
|
||||
return address;
|
||||
}
|
||||
|
||||
private @Nullable Channel channel() {
|
||||
var channel = channelFuture;
|
||||
return channel == null ? null : channel.channel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendText(String message) {
|
||||
environment.observe(Metrics.WEBSOCKET_OUTGOING, message.length());
|
||||
|
||||
var channel = channel();
|
||||
if (channel != null) channel.writeAndFlush(new TextWebSocketFrame(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBinary(ByteBuffer message) {
|
||||
environment.observe(Metrics.WEBSOCKET_OUTGOING, message.remaining());
|
||||
|
||||
var channel = channel();
|
||||
if (channel != null) channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)));
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A client-side websocket, which can be used to send messages to a remote server.
|
||||
* <p>
|
||||
* {@link WebsocketHandle} wraps this into a Lua-compatible interface.
|
||||
*/
|
||||
public interface WebsocketClient extends Closeable {
|
||||
String SUCCESS_EVENT = "websocket_success";
|
||||
String FAILURE_EVENT = "websocket_failure";
|
||||
String CLOSE_EVENT = "websocket_closed";
|
||||
String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
/**
|
||||
* Determine whether this websocket is closed.
|
||||
*
|
||||
* @return Whether this websocket is closed.
|
||||
*/
|
||||
boolean isClosed();
|
||||
|
||||
/**
|
||||
* Close this websocket.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Send a text websocket frame.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void sendText(String message);
|
||||
|
||||
/**
|
||||
* Send a binary websocket frame.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void sendBinary(ByteBuffer message);
|
||||
|
||||
/**
|
||||
* Parse an address, ensuring it is a valid websocket URI.
|
||||
*
|
||||
* @param address The address to parse.
|
||||
* @return The parsed URI.
|
||||
* @throws HTTPRequestException If the address is not valid.
|
||||
*/
|
||||
static URI parseUri(String address) throws HTTPRequestException {
|
||||
URI uri = null;
|
||||
try {
|
||||
uri = new URI(address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
// Fall through to the case below
|
||||
}
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + uri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
} else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
|
||||
throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
}
|
@@ -6,40 +6,34 @@ package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.CLOSE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
|
||||
|
||||
/**
|
||||
* A websocket, which can be used to send an receive messages with a web server.
|
||||
* A websocket, which can be used to send and receive messages with a web server.
|
||||
*
|
||||
* @cc.module http.Websocket
|
||||
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
|
||||
*/
|
||||
public class WebsocketHandle implements Closeable {
|
||||
private final Websocket websocket;
|
||||
public class WebsocketHandle {
|
||||
private final IAPIEnvironment environment;
|
||||
private final String address;
|
||||
private final WebsocketClient websocket;
|
||||
private final Options options;
|
||||
private boolean closed = false;
|
||||
|
||||
private @Nullable Channel channel;
|
||||
|
||||
public WebsocketHandle(Websocket websocket, Options options, Channel channel) {
|
||||
public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Options options) {
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +52,7 @@ public class WebsocketHandle implements Closeable {
|
||||
public final MethodResult receive(Optional<Double> timeout) throws LuaException {
|
||||
checkOpen();
|
||||
var timeoutId = timeout.isPresent()
|
||||
? websocket.environment().startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
||||
? environment.startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
||||
: -1;
|
||||
|
||||
return new ReceiveCallback(timeoutId).pull;
|
||||
@@ -78,17 +72,14 @@ public class WebsocketHandle implements Closeable {
|
||||
checkOpen();
|
||||
|
||||
var text = message.value();
|
||||
if (options.websocketMessage != 0 && text.length() > options.websocketMessage) {
|
||||
if (options.websocketMessage() != 0 && text.length() > options.websocketMessage()) {
|
||||
throw new LuaException("Message is too large");
|
||||
}
|
||||
|
||||
websocket.environment().observe(Metrics.WEBSOCKET_OUTGOING, text.length());
|
||||
|
||||
var channel = this.channel;
|
||||
if (channel != null) {
|
||||
channel.writeAndFlush(binary.orElse(false)
|
||||
? new BinaryWebSocketFrame(Unpooled.wrappedBuffer(LuaValues.encode(text)))
|
||||
: new TextWebSocketFrame(text));
|
||||
if (binary.orElse(false)) {
|
||||
websocket.sendBinary(LuaValues.encode(text));
|
||||
} else {
|
||||
websocket.sendText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,25 +87,13 @@ public class WebsocketHandle implements Closeable {
|
||||
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
|
||||
* along it.
|
||||
*/
|
||||
@LuaFunction("close")
|
||||
public final void doClose() {
|
||||
close();
|
||||
@LuaFunction
|
||||
public final void close() {
|
||||
websocket.close();
|
||||
}
|
||||
|
||||
private void checkOpen() throws LuaException {
|
||||
if (closed) throw new LuaException("attempt to use a closed file");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
|
||||
var channel = this.channel;
|
||||
if (channel != null) {
|
||||
channel.close();
|
||||
this.channel = null;
|
||||
}
|
||||
if (websocket.isClosed()) throw new LuaException("attempt to use a closed file");
|
||||
}
|
||||
|
||||
private final class ReceiveCallback implements ILuaCallback {
|
||||
@@ -127,9 +106,9 @@ public class WebsocketHandle implements Closeable {
|
||||
|
||||
@Override
|
||||
public MethodResult resume(Object[] event) {
|
||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], websocket.address())) {
|
||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], address)) {
|
||||
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
|
||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], websocket.address()) && closed) {
|
||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], address) && websocket.isClosed()) {
|
||||
// If the socket is closed abort.
|
||||
return MethodResult.of();
|
||||
} else if (event.length >= 2 && timeoutId != -1 && Objects.equal(event[0], TIMER_EVENT)
|
||||
|
@@ -13,14 +13,14 @@ import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.websocketx.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
|
||||
|
||||
public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
private final Websocket websocket;
|
||||
private final Options options;
|
||||
private boolean handshakeComplete = false;
|
||||
|
||||
public WebsocketHandler(Websocket websocket, Options options) {
|
||||
WebsocketHandler(Websocket websocket, Options options) {
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
}
|
||||
@@ -32,9 +32,9 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
||||
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
|
||||
websocket.success(ctx.channel(), options);
|
||||
websocket.success(options);
|
||||
handshakeComplete = true;
|
||||
} else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
|
||||
websocket.failure("Timed out");
|
||||
|
@@ -2,14 +2,13 @@
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.computer.upload;
|
||||
package dan200.computercraft.core.apis.transfer;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -26,9 +25,9 @@ public class TransferredFile implements ObjectSource {
|
||||
private final String name;
|
||||
private final BinaryReadableHandle handle;
|
||||
|
||||
public TransferredFile(String name, ByteBuffer contents) {
|
||||
public TransferredFile(String name, SeekableByteChannel contents) {
|
||||
this.name = name;
|
||||
handle = BinaryReadableHandle.of(new ByteBufferChannel(contents));
|
||||
handle = BinaryReadableHandle.of(contents);
|
||||
}
|
||||
|
||||
/**
|
@@ -2,13 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.computer.upload;
|
||||
package dan200.computercraft.core.apis.transfer;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -19,16 +15,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
* @cc.module [kind=event] file_transfer.TransferredFiles
|
||||
*/
|
||||
public class TransferredFiles {
|
||||
private final ServerPlayer player;
|
||||
private final AbstractContainerMenu container;
|
||||
public static final String EVENT = "file_transfer";
|
||||
|
||||
private final AtomicBoolean consumed = new AtomicBoolean(false);
|
||||
private final Runnable onConsumed;
|
||||
|
||||
private final List<TransferredFile> files;
|
||||
|
||||
public TransferredFiles(ServerPlayer player, AbstractContainerMenu container, List<TransferredFile> files) {
|
||||
this.player = player;
|
||||
this.container = container;
|
||||
public TransferredFiles(List<TransferredFile> files, Runnable onConsumed) {
|
||||
this.files = files;
|
||||
this.onConsumed = onConsumed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,9 +40,6 @@ public class TransferredFiles {
|
||||
|
||||
private void consumed() {
|
||||
if (consumed.getAndSet(true)) return;
|
||||
|
||||
if (player.isAlive() && player.containerMenu == container) {
|
||||
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(container), player);
|
||||
}
|
||||
onConsumed.run();
|
||||
}
|
||||
}
|
@@ -20,7 +20,7 @@ import java.util.Objects;
|
||||
* This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide
|
||||
* method supplier}. It should not be used directly.
|
||||
*/
|
||||
public class PeripheralMethodSupplier {
|
||||
public final class PeripheralMethodSupplier {
|
||||
private static final Generator<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class),
|
||||
m -> (target, context, computer, args) -> {
|
||||
var escArgs = args.escapes();
|
||||
@@ -31,6 +31,9 @@ public class PeripheralMethodSupplier {
|
||||
method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args)
|
||||
);
|
||||
|
||||
private PeripheralMethodSupplier() {
|
||||
}
|
||||
|
||||
public static MethodSupplier<PeripheralMethod> create(List<GenericMethod> genericMethods) {
|
||||
return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic
|
||||
? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null")
|
||||
|
@@ -0,0 +1,146 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* An abstract mount which stores its file tree in memory.
|
||||
*
|
||||
* @param <T> The type of file.
|
||||
*/
|
||||
public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.FileEntry<T>> implements Mount {
|
||||
protected static final String NO_SUCH_FILE = "No such file";
|
||||
|
||||
@Nullable
|
||||
protected T root;
|
||||
|
||||
protected final @Nullable T get(String path) {
|
||||
var lastEntry = root;
|
||||
var lastIndex = 0;
|
||||
|
||||
while (lastEntry != null && lastIndex < path.length()) {
|
||||
var nextIndex = path.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = path.length();
|
||||
|
||||
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
|
||||
return lastEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean exists(String path) {
|
||||
return get(path) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isDirectory(String path) {
|
||||
var file = get(path);
|
||||
return file != null && file.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void list(String path, List<String> contents) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || file.children == null) throw new FileOperationException(path, "Not a directory");
|
||||
|
||||
contents.addAll(file.children.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getSize(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return getSize(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to get the size of.
|
||||
* @return The size of the file. This should be 0 for directories, and equal to {@code openForRead(_).size()} for files.
|
||||
* @throws IOException If the size could not be read.
|
||||
*/
|
||||
protected abstract long getSize(String path, T file) throws IOException;
|
||||
|
||||
@Override
|
||||
public final SeekableByteChannel openForRead(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return openForRead(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file for reading.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to read. This will not be a directory.
|
||||
* @return The channel for this file.
|
||||
*/
|
||||
protected abstract SeekableByteChannel openForRead(String path, T file) throws IOException;
|
||||
|
||||
@Override
|
||||
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return getAttributes(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes of the file.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to compute attributes for.
|
||||
* @return The file's attributes.
|
||||
* @throws IOException If the attributes could not be read.
|
||||
*/
|
||||
protected BasicFileAttributes getAttributes(String path, T file) throws IOException {
|
||||
return new FileAttributes(file.isDirectory(), getSize(path, file));
|
||||
}
|
||||
|
||||
protected T getOrCreateChild(T lastEntry, String localPath, Function<String, T> factory) {
|
||||
var lastIndex = 0;
|
||||
while (lastIndex < localPath.length()) {
|
||||
var nextIndex = localPath.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = localPath.length();
|
||||
|
||||
var part = localPath.substring(lastIndex, nextIndex);
|
||||
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
|
||||
|
||||
var nextEntry = lastEntry.children.get(part);
|
||||
if (nextEntry == null || !nextEntry.isDirectory()) {
|
||||
lastEntry.children.put(part, nextEntry = factory.apply(localPath.substring(0, nextIndex)));
|
||||
}
|
||||
|
||||
lastEntry = nextEntry;
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
|
||||
return lastEntry;
|
||||
}
|
||||
|
||||
protected static class FileEntry<T extends FileEntry<T>> {
|
||||
@Nullable
|
||||
public Map<String, T> children;
|
||||
|
||||
public boolean isDirectory() {
|
||||
return children != null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,25 +6,21 @@ package dan200.computercraft.core.filesystem;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* An abstract mount based on some archive of files, such as a Zip or Minecraft's resources.
|
||||
* <p>
|
||||
* We assume that we cannot create {@link SeekableByteChannel}s directly from the archive, and so maintain a (shared)
|
||||
* cache of recently read files and their contents.
|
||||
*
|
||||
* @param <T> The type of file.
|
||||
*/
|
||||
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implements Mount {
|
||||
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends AbstractInMemoryMount<T> {
|
||||
protected static final String NO_SUCH_FILE = "No such file";
|
||||
|
||||
/**
|
||||
@@ -44,128 +40,51 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implemen
|
||||
.<FileEntry<?>, byte[]>weigher((k, v) -> v.length)
|
||||
.build();
|
||||
|
||||
@Nullable
|
||||
protected T root;
|
||||
|
||||
private @Nullable T get(String path) {
|
||||
var lastEntry = root;
|
||||
var lastIndex = 0;
|
||||
|
||||
while (lastEntry != null && lastIndex < path.length()) {
|
||||
var nextIndex = path.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = path.length();
|
||||
|
||||
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
|
||||
return lastEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean exists(String path) {
|
||||
return get(path) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isDirectory(String path) {
|
||||
var file = get(path);
|
||||
return file != null && file.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void list(String path, List<String> contents) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
|
||||
|
||||
file.list(contents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getSize(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return getCachedSize(file);
|
||||
}
|
||||
|
||||
private long getCachedSize(T file) throws IOException {
|
||||
protected final long getSize(String path, T file) throws IOException {
|
||||
if (file.size != -1) return file.size;
|
||||
if (file.isDirectory()) return file.size = 0;
|
||||
|
||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
||||
if (contents != null) return file.size = contents.length;
|
||||
|
||||
return file.size = getSize(file);
|
||||
return file.size = contents != null ? contents.length : getFileSize(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file.
|
||||
* <p>
|
||||
* This should only be called once per file, as the result is cached in {@link #getSize(String)}.
|
||||
* Get the size of the file by reading it (or its metadata) from disk.
|
||||
*
|
||||
* @param file The file to compute the size of. This will not be a directory.
|
||||
* @return The size of the file.
|
||||
* @throws IOException If the size could not be read.
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to get the size of.
|
||||
* @return The file's size.
|
||||
* @throws IOException If the size could not be computed.
|
||||
*/
|
||||
protected abstract long getSize(T file) throws IOException;
|
||||
protected long getFileSize(String path, T file) throws IOException {
|
||||
return getContents(path, file).length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForRead(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
protected final SeekableByteChannel openForRead(String path, T file) throws IOException {
|
||||
return new ArrayByteChannel(getContents(path, file));
|
||||
}
|
||||
|
||||
private byte[] getContents(String path, T file) throws IOException {
|
||||
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
||||
if (cachedContents != null) return new ArrayByteChannel(cachedContents);
|
||||
if (cachedContents != null) return cachedContents;
|
||||
|
||||
var contents = getContents(file);
|
||||
var contents = getFileContents(path, file);
|
||||
CONTENTS_CACHE.put(file, contents);
|
||||
return new ArrayByteChannel(contents);
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the entirety of a file into memory.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to read into memory. This will not be a directory.
|
||||
* @return The contents of the file.
|
||||
*/
|
||||
protected abstract byte[] getContents(T file) throws IOException;
|
||||
|
||||
@Override
|
||||
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
|
||||
return getAttributes(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes of the file.
|
||||
*
|
||||
* @param file The file to compute attributes for. This will not be a directory.
|
||||
* @return The file's attributes.
|
||||
* @throws IOException If the attributes could not be read.
|
||||
*/
|
||||
protected BasicFileAttributes getAttributes(T file) throws IOException {
|
||||
return new FileAttributes(file.isDirectory(), getCachedSize(file));
|
||||
}
|
||||
|
||||
protected static class FileEntry<T extends ArchiveMount.FileEntry<T>> {
|
||||
public final String path;
|
||||
@Nullable
|
||||
public Map<String, T> children;
|
||||
protected abstract byte[] getFileContents(String path, T file) throws IOException;
|
||||
|
||||
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
|
||||
long size = -1;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
protected boolean isDirectory() {
|
||||
return children != null;
|
||||
}
|
||||
|
||||
protected void list(List<String> contents) {
|
||||
if (children != null) contents.addAll(children.keySet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
@@ -22,7 +22,7 @@ import java.util.zip.ZipFile;
|
||||
/**
|
||||
* A mount which reads zip/jar files.
|
||||
*/
|
||||
public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closeable {
|
||||
public final class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closeable {
|
||||
private final ZipFile zip;
|
||||
|
||||
public JarMount(File jarFile, String subPath) throws IOException {
|
||||
@@ -42,7 +42,7 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
}
|
||||
|
||||
// Read in all the entries
|
||||
root = new FileEntry("");
|
||||
var root = this.root = new FileEntry();
|
||||
var zipEntries = zip.entries();
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
var entry = zipEntries.nextElement();
|
||||
@@ -51,55 +51,33 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
if (!entryPath.startsWith(subPath)) continue;
|
||||
|
||||
var localPath = FileSystem.toLocal(entryPath, subPath);
|
||||
create(entry, localPath);
|
||||
getOrCreateChild(root, localPath, x -> new FileEntry()).setup(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void create(ZipEntry entry, String localPath) {
|
||||
var lastEntry = Nullability.assertNonNull(root);
|
||||
|
||||
var lastIndex = 0;
|
||||
while (lastIndex < localPath.length()) {
|
||||
var nextIndex = localPath.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = localPath.length();
|
||||
|
||||
var part = localPath.substring(lastIndex, nextIndex);
|
||||
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
|
||||
|
||||
var nextEntry = lastEntry.children.get(part);
|
||||
if (nextEntry == null || !nextEntry.isDirectory()) {
|
||||
lastEntry.children.put(part, nextEntry = new FileEntry(localPath.substring(0, nextIndex)));
|
||||
}
|
||||
|
||||
lastEntry = nextEntry;
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
|
||||
lastEntry.setup(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSize(FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
protected long getFileSize(String path, FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return file.zipEntry.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] getContents(FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
protected byte[] getFileContents(String path, FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
|
||||
try (var stream = zip.getInputStream(file.zipEntry)) {
|
||||
return stream.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
// Mask other IO exceptions as a non-existent file.
|
||||
throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
return new ZipEntryAttributes(file.zipEntry);
|
||||
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||
return file.zipEntry == null ? super.getAttributes(path, file) : new FileAttributes(
|
||||
file.isDirectory(), getSize(path, file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,79 +85,19 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
zip.close();
|
||||
}
|
||||
|
||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
@Nullable
|
||||
ZipEntry zipEntry;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
super(path);
|
||||
}
|
||||
|
||||
void setup(ZipEntry entry) {
|
||||
zipEntry = entry;
|
||||
size = entry.getSize();
|
||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ZipEntryAttributes implements BasicFileAttributes {
|
||||
private final ZipEntry entry;
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
ZipEntryAttributes(ZipEntry entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastModifiedTime() {
|
||||
return orEpoch(entry.getLastModifiedTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastAccessTime() {
|
||||
return orEpoch(entry.getLastAccessTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime creationTime() {
|
||||
var time = entry.getCreationTime();
|
||||
return time == null ? lastModifiedTime() : time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegularFile() {
|
||||
return !entry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return entry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSymbolicLink() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOther() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return entry.getSize();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object fileKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
private static FileTime orEpoch(@Nullable FileTime time) {
|
||||
return time == null ? EPOCH : time;
|
||||
}
|
||||
private static FileTime orEpoch(@Nullable FileTime time) {
|
||||
return time == null ? EPOCH : time;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,334 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.WritableFileMount.MINIMUM_FILE_SIZE;
|
||||
|
||||
/**
|
||||
* A basic {@link Mount} which stores files and directories in-memory.
|
||||
*/
|
||||
public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> implements WritableMount {
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
private final long capacity;
|
||||
|
||||
/**
|
||||
* Create a memory mount with a 1GB capacity.
|
||||
*/
|
||||
public MemoryMount() {
|
||||
this(1000_000_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a memory mount with a custom capacity. Note, this is only used in calculations for {@link #getCapacity()}
|
||||
* and {@link #getRemainingSpace()}, it is not checked when creating or writing files.
|
||||
*
|
||||
* @param capacity The capacity of this mount.
|
||||
*/
|
||||
public MemoryMount(long capacity) {
|
||||
this.capacity = capacity;
|
||||
root = new FileEntry();
|
||||
root.children = new HashMap<>();
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, byte[] contents, FileTime created, FileTime modified) {
|
||||
var entry = getOrCreateChild(Nullability.assertNonNull(root), file, x -> new FileEntry());
|
||||
entry.contents = contents;
|
||||
entry.length = contents.length;
|
||||
entry.created = created;
|
||||
entry.modified = modified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, String contents, FileTime created, FileTime modified) {
|
||||
return addFile(file, contents.getBytes(StandardCharsets.UTF_8), created, modified);
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, byte[] contents) {
|
||||
return addFile(file, contents, EPOCH, EPOCH);
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, String contents) {
|
||||
return addFile(file, contents, EPOCH, EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSize(String path, FileEntry file) {
|
||||
return file.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SeekableByteChannel openForRead(String path, FileEntry file) throws IOException {
|
||||
if (file.contents == null) throw new FileOperationException(path, "File is a directory");
|
||||
return new EntryChannel(file, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||
return new FileAttributes(file.isDirectory(), file.length, file.created, file.modified);
|
||||
}
|
||||
|
||||
private @Nullable ParentAndName getParentAndName(String path) {
|
||||
if (path.isEmpty()) throw new IllegalArgumentException("Path is empty");
|
||||
var index = path.lastIndexOf('/');
|
||||
if (index == -1) {
|
||||
return new ParentAndName(Nullability.assertNonNull(Nullability.assertNonNull(root).children), path);
|
||||
}
|
||||
|
||||
var entry = get(path.substring(0, index));
|
||||
return entry == null || entry.children == null
|
||||
? null
|
||||
: new ParentAndName(entry.children, path.substring(index + 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeDirectory(String path) throws IOException {
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
var lastEntry = Nullability.assertNonNull(root);
|
||||
var lastIndex = 0;
|
||||
while (lastIndex < path.length()) {
|
||||
if (lastEntry.children == null) throw new NullPointerException("children is null");
|
||||
|
||||
var nextIndex = path.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = path.length();
|
||||
|
||||
var part = path.substring(lastIndex, nextIndex);
|
||||
var nextEntry = lastEntry.children.get(part);
|
||||
if (nextEntry == null) {
|
||||
lastEntry.children.put(part, nextEntry = FileEntry.newDir());
|
||||
} else if (nextEntry.children == null) {
|
||||
throw new FileOperationException(path, "File exists");
|
||||
}
|
||||
|
||||
lastEntry = nextEntry;
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws IOException {
|
||||
if (path.isEmpty()) throw new AccessDeniedException("Access denied");
|
||||
var node = getParentAndName(path);
|
||||
if (node != null) node.parent().remove(node.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rename(String source, String dest) throws IOException {
|
||||
if (dest.startsWith(source)) throw new FileOperationException(source, "Cannot move a directory inside itself");
|
||||
|
||||
var sourceParent = getParentAndName(source);
|
||||
if (sourceParent == null || !sourceParent.exists()) throw new FileOperationException(source, "No such file");
|
||||
|
||||
var destParent = getParentAndName(dest);
|
||||
if (destParent == null) throw new FileOperationException(dest, "Parent directory does not exist");
|
||||
if (destParent.exists()) throw new FileOperationException(dest, "File exists");
|
||||
|
||||
destParent.put(sourceParent.parent().remove(sourceParent.name()));
|
||||
}
|
||||
|
||||
private FileEntry getForWrite(String path) throws FileOperationException {
|
||||
if (path.isEmpty()) throw new FileOperationException(path, "Cannot write to directory");
|
||||
|
||||
var parent = getParentAndName(path);
|
||||
if (parent == null) throw new FileOperationException(path, "Parent directory does not exist");
|
||||
|
||||
var file = parent.get();
|
||||
if (file != null && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
|
||||
if (file == null) parent.put(file = FileEntry.newFile());
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForWrite(String path) throws IOException {
|
||||
var file = getForWrite(path);
|
||||
|
||||
// Truncate the file.
|
||||
file.contents = EMPTY;
|
||||
file.length = 0;
|
||||
return new EntryChannel(file, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForAppend(String path) throws IOException {
|
||||
var file = getForWrite(path);
|
||||
return new EntryChannel(file, file.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingSpace() {
|
||||
return capacity - computeUsedSpace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCapacity() {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
private long computeUsedSpace() {
|
||||
Queue<FileEntry> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
|
||||
long size = 0;
|
||||
|
||||
FileEntry entry;
|
||||
while ((entry = queue.poll()) != null) {
|
||||
if (entry.children == null) {
|
||||
size += Math.max(MINIMUM_FILE_SIZE, Nullability.assertNonNull(entry.contents).length);
|
||||
} else {
|
||||
size += MINIMUM_FILE_SIZE;
|
||||
queue.addAll(entry.children.values());
|
||||
}
|
||||
}
|
||||
|
||||
return size - MINIMUM_FILE_SIZE; // Subtract one file for the root.
|
||||
}
|
||||
|
||||
protected static final class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
|
||||
FileTime created = EPOCH;
|
||||
FileTime modified = EPOCH;
|
||||
@Nullable
|
||||
byte[] contents;
|
||||
|
||||
int length;
|
||||
|
||||
static FileEntry newFile() {
|
||||
var entry = new FileEntry();
|
||||
entry.contents = EMPTY;
|
||||
entry.created = entry.modified = FileTime.from(Instant.now());
|
||||
return entry;
|
||||
}
|
||||
|
||||
static FileEntry newDir() {
|
||||
var entry = new FileEntry();
|
||||
entry.children = new HashMap<>();
|
||||
entry.created = entry.modified = FileTime.from(Instant.now());
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
private record ParentAndName(Map<String, FileEntry> parent, String name) {
|
||||
boolean exists() {
|
||||
return parent.containsKey(name);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
FileEntry get() {
|
||||
return parent.get(name);
|
||||
}
|
||||
|
||||
void put(FileEntry entry) {
|
||||
assert !parent.containsKey(name);
|
||||
parent.put(name, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class EntryChannel implements SeekableByteChannel {
|
||||
private final FileEntry entry;
|
||||
private long position;
|
||||
private boolean isOpen = true;
|
||||
|
||||
private void checkClosed() throws ClosedChannelException {
|
||||
if (!isOpen()) throw new ClosedChannelException();
|
||||
}
|
||||
|
||||
private EntryChannel(FileEntry entry, int position) {
|
||||
this.entry = entry;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer destination) throws IOException {
|
||||
checkClosed();
|
||||
|
||||
var backing = Nullability.assertNonNull(entry.contents);
|
||||
if (position >= backing.length) return -1;
|
||||
|
||||
var remaining = Math.min(backing.length - (int) position, destination.remaining());
|
||||
destination.put(backing, (int) position, remaining);
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
private byte[] ensureCapacity(int capacity) {
|
||||
var contents = Nullability.assertNonNull(entry.contents);
|
||||
if (capacity >= entry.length) {
|
||||
var newCapacity = Math.max(capacity, contents.length << 1);
|
||||
contents = entry.contents = Arrays.copyOf(contents, newCapacity);
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws IOException {
|
||||
var toWrite = src.remaining();
|
||||
var endPosition = position + toWrite;
|
||||
if (endPosition > 1 << 30) throw new IOException("File is too large");
|
||||
|
||||
var contents = ensureCapacity((int) endPosition);
|
||||
src.get(contents, (int) position, toWrite);
|
||||
position = endPosition;
|
||||
if (endPosition > entry.length) entry.length = (int) endPosition;
|
||||
return toWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws IOException {
|
||||
checkClosed();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||
checkClosed();
|
||||
if (newPosition < 0) throw new IllegalArgumentException("Position out of bounds");
|
||||
this.position = newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
checkClosed();
|
||||
return entry.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return isOpen && entry.contents != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
checkClosed();
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -27,7 +27,7 @@ import java.util.Set;
|
||||
public class WritableFileMount extends FileMount implements WritableMount {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
|
||||
|
||||
private static final long MINIMUM_FILE_SIZE = 500;
|
||||
static final long MINIMUM_FILE_SIZE = 500;
|
||||
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
|
||||
|
@@ -687,7 +687,7 @@ settings.define("paint.default_extension", {
|
||||
|
||||
settings.define("list.show_hidden", {
|
||||
default = false,
|
||||
description = [[Show hidden files (those starting with "." in the Lua REPL).]],
|
||||
description = [[Whether the list program show hidden files (those starting with ".").]],
|
||||
type = "boolean",
|
||||
})
|
||||
|
||||
|
@@ -17,7 +17,7 @@ instance, `commands.say("Hi!")` is equivalent to `commands.exec("say Hi!")`.
|
||||
commands. `commands.async.say("Hi!")` is equivalent to
|
||||
`commands.execAsync("say Hi!")`.
|
||||
|
||||
[mc]: https://minecraft.gamepedia.com/Commands
|
||||
[mc]: https://minecraft.wiki/w/Commands
|
||||
|
||||
@module commands
|
||||
@usage Set the block above this computer to stone:
|
||||
|
@@ -96,7 +96,7 @@ end
|
||||
--
|
||||
-- If this returns true, you will can [play][`disk.playAudio`] the record.
|
||||
--
|
||||
-- [disk]: https://minecraft.gamepedia.com/Music_Disc
|
||||
-- [disk]: https://minecraft.wiki/w/Music_Disc
|
||||
--
|
||||
-- @tparam string name The name of the disk drive.
|
||||
-- @treturn boolean If the disk is present and has audio saved on it.
|
||||
|
@@ -722,7 +722,7 @@ do
|
||||
- `parse_empty_array`: When false, empty arrays will be parsed as a new table.
|
||||
By default (or when this value is true), they are parsed as [`empty_json_array`].
|
||||
|
||||
[nbt]: https://minecraft.gamepedia.com/NBT_format
|
||||
[nbt]: https://minecraft.wiki/w/NBT_format
|
||||
@return[1] The deserialised object
|
||||
@treturn[2] nil If the object could not be deserialised.
|
||||
@treturn string A message describing why the JSON string is invalid.
|
||||
|
@@ -1,3 +1,17 @@
|
||||
# New features in CC: Tweaked 1.108.3
|
||||
|
||||
Several bug fixes:
|
||||
* Fix disconnect when joining a dedicated server.
|
||||
|
||||
# New features in CC: Tweaked 1.108.2
|
||||
|
||||
* Add a tag for which blocks wired modems should ignore.
|
||||
|
||||
Several bug fixes:
|
||||
* Fix monitors sometimes being warped after resizing.
|
||||
* Fix the skull recipes using the wrong UUID format.
|
||||
* Fix paint canvas not always being redrawn after a term resize.
|
||||
|
||||
# New features in CC: Tweaked 1.108.1
|
||||
|
||||
Several bug fixes:
|
||||
|
@@ -12,4 +12,4 @@ commands.give( "dan200", "minecraft:diamond", 64 )
|
||||
This works with any command. Use "commands.async" instead of "commands" to execute asynchronously.
|
||||
|
||||
The commands API is only available on Command Computers.
|
||||
Visit http://minecraft.gamepedia.com/Commands for documentation on all commands.
|
||||
Visit https://minecraft.wiki/w/Commands for documentation on all commands.
|
||||
|
@@ -6,4 +6,4 @@ if sEvent == "key" and nKey == keys.enter then
|
||||
-- Do something
|
||||
end
|
||||
|
||||
See http://www.minecraftwiki.net/wiki/Key_codes, or the source code, for a complete reference.
|
||||
See https://www.minecraft.wiki/w/Key_codes, or the source code, for a complete reference.
|
||||
|
@@ -1,7 +1,6 @@
|
||||
New features in CC: Tweaked 1.108.1
|
||||
New features in CC: Tweaked 1.108.3
|
||||
|
||||
Several bug fixes:
|
||||
* Prevent no-opped players breaking or placing command computers.
|
||||
* Allow using `@LuaFunction`-annotated methods on classes defined in child classloaders.
|
||||
* Fix disconnect when joining a dedicated server.
|
||||
|
||||
Type "help changelog" to see the full version history.
|
||||
|
@@ -275,6 +275,12 @@ local function drawCanvas()
|
||||
end
|
||||
end
|
||||
|
||||
local function termResize()
|
||||
w, h = term.getSize()
|
||||
drawCanvas()
|
||||
drawInterface()
|
||||
end
|
||||
|
||||
local menu_choices = {
|
||||
Save = function()
|
||||
if bReadOnly then
|
||||
@@ -376,6 +382,8 @@ local function accessMenu()
|
||||
nMenuPosEnd = nMenuPosEnd + 1
|
||||
nMenuPosStart = nMenuPosEnd
|
||||
end
|
||||
elseif id == "term_resize" then
|
||||
termResize()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -434,9 +442,7 @@ local function handleEvents()
|
||||
drawInterface()
|
||||
end
|
||||
elseif id == "term_resize" then
|
||||
w, h = term.getSize()
|
||||
drawCanvas()
|
||||
drawInterface()
|
||||
termResize()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@@ -23,8 +23,8 @@ public class AddressRuleTest {
|
||||
Action.ALLOW.toPartial()
|
||||
));
|
||||
|
||||
assertEquals(apply(rules, "localhost", 8080).action, Action.ALLOW);
|
||||
assertEquals(apply(rules, "localhost", 8081).action, Action.DENY);
|
||||
assertEquals(apply(rules, "localhost", 8080).action(), Action.ALLOW);
|
||||
assertEquals(apply(rules, "localhost", 8081).action(), Action.DENY);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -43,7 +43,7 @@ public class AddressRuleTest {
|
||||
"169.254.169.254", // AWS, Digital Ocean, GCP, etc..
|
||||
})
|
||||
public void blocksLocalDomains(String domain) {
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.DENY);
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action(), Action.DENY);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -52,7 +52,7 @@ public class AddressRuleTest {
|
||||
"100.63.255.255", "100.128.0.0"
|
||||
})
|
||||
public void allowsNonLocalDomains(String domain) {
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.ALLOW);
|
||||
assertEquals(apply(CoreConfig.httpRules, domain, 80).action(), Action.ALLOW);
|
||||
}
|
||||
|
||||
private Options apply(Iterable<AddressRule> rules, String host, int port) {
|
||||
|
@@ -12,10 +12,9 @@ import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
|
||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
||||
import dan200.computercraft.test.core.filesystem.ReadOnlyWritableMount;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -37,7 +36,7 @@ public class ComputerBootstrap {
|
||||
.addFile("test.lua", program)
|
||||
.addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
|
||||
|
||||
run(new ReadOnlyWritableMount(mount), setup, maxTimes);
|
||||
run(mount, setup, maxTimes);
|
||||
}
|
||||
|
||||
public static void run(String program, int maxTimes) {
|
||||
|
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.test.core.filesystem.MountContract;
|
||||
import dan200.computercraft.test.core.filesystem.WritableMountContract;
|
||||
import org.opentest4j.TestAbortedException;
|
||||
|
||||
public class MemoryMountTest implements MountContract, WritableMountContract {
|
||||
@Override
|
||||
public Mount createSkeleton() {
|
||||
var mount = new MemoryMount();
|
||||
mount.addFile("f.lua", "");
|
||||
mount.addFile("dir/file.lua", "print('testing')", EPOCH, MODIFY_TIME);
|
||||
return mount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountAccess createMount(long capacity) {
|
||||
var mount = new MemoryMount(capacity);
|
||||
return new MountAccess() {
|
||||
@Override
|
||||
public WritableMount mount() {
|
||||
return mount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeReadOnly(String path) {
|
||||
throw new TestAbortedException("Not supported for MemoryMount");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensuresExist() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long computeRemainingSpace() {
|
||||
return mount.getRemainingSpace();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -27,7 +27,7 @@ object HttpServer {
|
||||
const val URL: String = "http://127.0.0.1:$PORT"
|
||||
const val WS_URL: String = "ws://127.0.0.1:$PORT/ws"
|
||||
|
||||
fun runServer(run: () -> Unit) {
|
||||
fun runServer(run: (stop: () -> Unit) -> Unit) {
|
||||
val workerGroup: EventLoopGroup = NioEventLoopGroup(2)
|
||||
try {
|
||||
val ch = ServerBootstrap()
|
||||
@@ -48,7 +48,7 @@ object HttpServer {
|
||||
},
|
||||
).bind(PORT).sync().channel()
|
||||
try {
|
||||
run()
|
||||
run { workerGroup.shutdownGracefully() }
|
||||
} finally {
|
||||
ch.close().sync()
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.core.apis.http
|
||||
|
||||
import dan200.computercraft.api.lua.Coerced
|
||||
import dan200.computercraft.api.lua.LuaException
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.CoreConfig
|
||||
import dan200.computercraft.core.apis.HTTPAPI
|
||||
@@ -22,7 +23,9 @@ import org.hamcrest.Matchers.*
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.util.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class TestHttpApi {
|
||||
companion object {
|
||||
@@ -79,12 +82,36 @@ class TestHttpApi {
|
||||
|
||||
websocket.close()
|
||||
|
||||
val closeEvent = pullEventOrTimeout(500.milliseconds, "websocket_closed")
|
||||
assertThat("No event was queued", closeEvent, equalTo(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Queues an event when the socket is externally closed`() {
|
||||
runServer { stop ->
|
||||
LuaTaskRunner.runTest {
|
||||
val httpApi = addApi(HTTPAPI(environment))
|
||||
assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true)))
|
||||
|
||||
val connectEvent = pullEvent()
|
||||
assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java)))
|
||||
|
||||
val websocket = connectEvent[2] as WebsocketHandle
|
||||
|
||||
stop()
|
||||
|
||||
val closeEvent = pullEvent("websocket_closed")
|
||||
assertThat(
|
||||
"Websocket was closed",
|
||||
closeEvent,
|
||||
array(equalTo("websocket_closed"), equalTo(WS_URL), equalTo("Connection closed"), equalTo(null)),
|
||||
)
|
||||
|
||||
assertThrows<LuaException>("Throws an exception when sending") {
|
||||
websocket.send(Coerced("hello"), Optional.of(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,10 +10,10 @@ import java.util.Deque;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* An {@link AutoCloseable} implementation which can be used to combine other [AutoCloseable] instances.
|
||||
* An {@link AutoCloseable} implementation which can be used to combine other {@link AutoCloseable} instances.
|
||||
* <p>
|
||||
* Values which implement {@link AutoCloseable} can be dynamically registered with [CloseScope.add]. When the scope is
|
||||
* closed, each value is closed in the opposite order.
|
||||
* Values which implement {@link AutoCloseable} can be dynamically registered with {@link CloseScope#add(AutoCloseable)}.
|
||||
* When the scope is closed, each value is closed in the opposite order.
|
||||
* <p>
|
||||
* This is largely intended for cases where it's not appropriate to nest try-with-resources blocks, for instance when
|
||||
* nested would be too deep or when objects are dynamically created.
|
||||
|
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core;
|
||||
|
||||
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||
import org.junit.jupiter.api.DisplayNameGenerator;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* A {@link DisplayNameGenerator} which replaces underscores with spaces. This is equivalent to
|
||||
* {@link DisplayNameGenerator.ReplaceUnderscores}, but excludes the parameter types.
|
||||
*
|
||||
* @see DisplayNameGeneration
|
||||
*/
|
||||
public class ReplaceUnderscoresDisplayNameGenerator extends DisplayNameGenerator.ReplaceUnderscores {
|
||||
@Override
|
||||
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
|
||||
return testMethod.getName().replace('_', ' ');
|
||||
}
|
||||
}
|
@@ -100,6 +100,29 @@ final class StructuralEqualities {
|
||||
}
|
||||
}
|
||||
|
||||
record ListEquality<T>(StructuralEquality<T> equality) implements StructuralEquality<List<T>> {
|
||||
@Override
|
||||
public boolean equals(List<T> left, List<T> right) {
|
||||
if (left.size() != right.size()) return false;
|
||||
for (var i = 0; i < left.size(); i++) {
|
||||
if (!equality.equals(left.get(i), right.get(i))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describe(Description description, List<T> object) {
|
||||
description.appendText("[");
|
||||
var separator = false;
|
||||
for (var value : object) {
|
||||
if (separator) description.appendText(", ");
|
||||
separator = true;
|
||||
equality.describe(description, value);
|
||||
}
|
||||
description.appendText("]");
|
||||
}
|
||||
}
|
||||
|
||||
static final class EqualityMatcher<T> extends TypeSafeMatcher<T> {
|
||||
private final StructuralEquality<T> equality;
|
||||
private final T equalTo;
|
||||
|
@@ -36,6 +36,15 @@ public interface StructuralEquality<T> {
|
||||
*/
|
||||
void describe(Description description, T object);
|
||||
|
||||
/**
|
||||
* Lift this equality to a list of values.
|
||||
*
|
||||
* @return A equality for a list of values.
|
||||
*/
|
||||
default StructuralEquality<List<T>> list() {
|
||||
return new StructuralEqualities.ListEquality<>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this equality instance to a {@link Matcher}.
|
||||
*
|
||||
|
@@ -11,10 +11,9 @@ import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
import dan200.computercraft.core.filesystem.FileMount;
|
||||
import dan200.computercraft.core.filesystem.JarMount;
|
||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||
import dan200.computercraft.core.metrics.Metric;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
||||
import dan200.computercraft.test.core.filesystem.ReadOnlyWritableMount;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -32,7 +31,7 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment,
|
||||
private final WritableMount mount;
|
||||
|
||||
public BasicEnvironment() {
|
||||
this(new ReadOnlyWritableMount(new MemoryMount()));
|
||||
this(new MemoryMount());
|
||||
}
|
||||
|
||||
public BasicEnvironment(WritableMount mount) {
|
||||
|
@@ -1,59 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
|
||||
*/
|
||||
public class MemoryMount implements Mount {
|
||||
private final Map<String, byte[]> files = new HashMap<>();
|
||||
private final Set<String> directories = new HashSet<>();
|
||||
|
||||
public MemoryMount() {
|
||||
directories.add("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String path) {
|
||||
return files.containsKey(path) || directories.contains(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(String path) {
|
||||
return directories.contains(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(String path, List<String> files) {
|
||||
for (var file : this.files.keySet()) {
|
||||
if (file.startsWith(path)) files.add(file.substring(path.length() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(String path) {
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForRead(String path) throws FileOperationException {
|
||||
var file = files.get(path);
|
||||
if (file == null) throw new FileOperationException(path, "File not found");
|
||||
return new ArrayByteChannel(file);
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, String contents) {
|
||||
files.put(file, contents.getBytes(StandardCharsets.UTF_8));
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -34,7 +34,10 @@ public interface MountContract {
|
||||
* Create a skeleton mount. This should contain the following files:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code dir/file.lua}, containing {@code print('testing')}.</li>
|
||||
* <li>
|
||||
* {@code dir/file.lua}, containing {@code print('testing')}. If {@linkplain #hasFileTimes() file times are
|
||||
* supported}, it should have a modification time of {@link #MODIFY_TIME}.
|
||||
* </li>
|
||||
* <li>{@code f.lua}, containing nothing.</li>
|
||||
* </ul>
|
||||
*
|
||||
|
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Utility functions for working with mounts.
|
||||
*/
|
||||
public final class Mounts {
|
||||
private Mounts() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file to this mount.
|
||||
*
|
||||
* @param mount The mount to modify.
|
||||
* @param path The path to write to.
|
||||
* @param contents The contents of this path.
|
||||
* @throws IOException If writing fails.
|
||||
*/
|
||||
public static void writeFile(WritableMount mount, String path, String contents) throws IOException {
|
||||
try (var handle = Channels.newWriter(mount.openForWrite(path), StandardCharsets.UTF_8)) {
|
||||
handle.write(contents);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,91 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wraps a {@link Mount} into a read-only {@link WritableMount}.
|
||||
*
|
||||
* @param mount The original read-only mount we're wrapping.
|
||||
*/
|
||||
public record ReadOnlyWritableMount(Mount mount) implements WritableMount {
|
||||
@Override
|
||||
public boolean exists(String path) throws IOException {
|
||||
return mount.exists(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(String path) throws IOException {
|
||||
return mount.isDirectory(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(String path, List<String> contents) throws IOException {
|
||||
mount.list(path, contents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(String path) throws IOException {
|
||||
return mount.getSize(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForRead(String path) throws IOException {
|
||||
return mount.openForRead(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(String path) throws IOException {
|
||||
return mount.getAttributes(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeDirectory(String path) throws IOException {
|
||||
throw new FileOperationException(path, "Access denied");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws IOException {
|
||||
throw new FileOperationException(path, "Access denied");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rename(String source, String dest) throws IOException {
|
||||
throw new FileOperationException(source, "Access denied");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForWrite(String path) throws IOException {
|
||||
throw new FileOperationException(path, "Access denied");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForAppend(String path) throws IOException {
|
||||
throw new FileOperationException(path, "Access denied");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingSpace() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCapacity() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly(String path) {
|
||||
return true;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user