diff --git a/buildSrc/src/main/kotlin/cc-tweaked.fabric.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.fabric.gradle.kts index 110af43cb..630affb87 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.fabric.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.fabric.gradle.kts @@ -4,7 +4,10 @@ /** Default configuration for Fabric projects. */ -import cc.tweaked.gradle.* +import cc.tweaked.gradle.CCTweakedExtension +import cc.tweaked.gradle.CCTweakedPlugin +import cc.tweaked.gradle.DependencyCheck +import cc.tweaked.gradle.MinecraftConfigurations plugins { `java-library` @@ -44,11 +47,12 @@ dependencies { loom.layered { officialMojangMappings() parchment( - project.dependencies.create( - group = "org.parchmentmc.data", - name = "parchment-${libs.findVersion("parchmentMc").get()}", - version = libs.findVersion("parchment").get().toString(), - ext = "zip", + dependencyFactory.create( + "org.parchmentmc.data", + "parchment-${libs.findVersion("parchmentMc").get()}", + libs.findVersion("parchment").get().toString(), + null, + "zip", ), ) }, @@ -60,9 +64,3 @@ dependencies { // Depend on error prone annotations to silence a lot of compile warnings. compileOnlyApi(libs.findLibrary("errorProne.annotations").get()) } - -tasks.named("checkDependencyConsistency", DependencyCheck::class.java) { - val libs = project.extensions.getByType().named("libs") - // Minecraft depends on asm, but Fabric forces it to a more recent version - override(libs.findLibrary("asm").get(), "9.9") -} diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt index 0035ab118..ece1b1f30 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt @@ -77,13 +77,8 @@ class IlluaminatePlugin : Plugin { else -> error("Unsupported architecture '$osArch' for illuaminate") } - return project.dependencies.create( - mapOf( - "group" to "cc.squiddev", - "name" to "illuaminate", - "version" to version, - "ext" to "$os-$arch$suffix", - ), + return project.dependencyFactory.create( + "cc.squiddev", "illuaminate", version, null, "$os-$arch$suffix", ) } } diff --git a/doc/guides/gps_setup.md b/doc/guides/gps_setup.md index 38ef2443f..54014d01d 100644 --- a/doc/guides/gps_setup.md +++ b/doc/guides/gps_setup.md @@ -9,85 +9,68 @@ SPDX-License-Identifier: MPL-2.0 --> # Setting up GPS -The [`gps`] API allows computers and turtles to find their current position using wireless modems. +The [`gps`] API allows a computer to find its current position using a [wireless modem][`modem`]. This works by +communicating with other computers (called *GPS hosts*) that already know their position, finding the distance to those +computers (with [`modem_message`]), and using that to derive its position from theirs (with a process known as +[trilateration](https://en.wikipedia.org/wiki/Trilateration). -In order to use GPS, you'll need to set up multiple *GPS hosts*. These are computers running the special `gps host` -program, which tell other computers the host's position. Several hosts running together are known as a *GPS -constellation*. +In order for this to work, we need our GPS hosts set up in a specific pattern, each one differing in position on at +least one axis. This guide takes you through the process of setting up a *constellation* of GPS hosts, and using them to +determine a computer's position. -In order to give the best results, a GPS constellation needs at least four computers. More than four GPS hosts per -constellation is redundant, but it does not cause problems. +## Prerequisites +You will need: -## Building a GPS constellation -An example GPS constellation. + - Four computers. + - Four Ender Modems. Normal Wireless Modems maybe be used, but the range of the GPS constellation will be severely + limited. -We are going to build our GPS constellation as shown in the image above. You will need 4 computers and either 4 wireless -modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your -requesting computers are out of range. +Additionally, you will need another computer and a wireless modem, in order to test GPS works! -> [Ender modems vs wireless modems][!TIP] -> Ender modems have a very large range, which makes them very useful for setting up GPS hosts. If you do this then you -> will likely only need one GPS constellation for the whole dimension (such as the Overworld or Nether). -> -> If you do use wireless modems then you may find that you need multiple GPS constellations to cover your needs. -> -> A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as -> it to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance -> parameter of [modem messages][`modem_message`] and some maths. +## Picking an area +First, choose a place to build your GPS constellation. This should be a 10x10x10 cube, though you can make this smaller +if needed. The larger a constellation is, the more accurate it is over large distances, but even a 5x5x5 constellation +should serve a several thousand block radius. -Locate where you want to place your GPS constellation. You will need an area at least 6 blocks high, 6 blocks wide, and -6 blocks deep (6x6x6). If you are using wireless modems then you may want to build your constellation as high as you can -because high altitude boosts modem message range and thus the radius that your constellation covers. +Every computer must be loaded in order for other computers to use GPS, so it is recommended to build your GPS +constellation in a single chunk that will always be loaded. You may want to choose an area in an already chunk-loaded +part of your base, or in the [spawn chunks][spawn chunks]. You can use F3+G to view the chunk boundaries if +needed. -The GPS constellation will only work when it is in a loaded chunk. If you want your constellation to always be -accessible, you may want to permanently load the chunk using a vanilla or modded chunk loader. Make sure that your 6x6x6 -area fits in a single chunk to reduce the number of chunks that need to be kept loaded. +[spawn chunks]: https://minecraft.wiki/w/Spawn_chunk "Spawn Chunk — Minecraft Wiki" -Let's get started building the constellation! Place your first computer in one of the corners of your 6x6x6. Remember -which computer this is as other computers need to be placed relative to it. Place the second computer 4 blocks above the -first. Go back to your first computer and place your third computer 5 blocks in front of your first computer, leaving 4 -blocks of air between them. Finally for the fourth computer, go back to your first computer and place it 5 blocks right -of your first computer, leaving 4 blocks of air between them. +This is the example area we will be building our constellation in: -With all four computers placed within the 6x6x6, place one modem on top of each computer. You should have 4 modems and 4 -computers all within your 6x6x6 where each modem is attached to a computer and each computer has a modem. +An empty 10x10x10 area, with the axis marked with smooth stone. -Currently your GPS constellation will not work, that's because each host is not aware that it's a GPS host. We will fix -this in the next section. +## Building the constellation + + 1. Place down your first computer in a corner of your area, and put a modem on top. + 2. Head to the two adjacent corners of your area, place down another two computers and put a modem on top of each. + 3. Pillar up above the first computer to the top of your cube, and place the final computer. Place a modem on the + computer. + +You should now have something like this: + +The same area as before, but with a computer in each corner. ## Configuring the constellation Now that the structure of your constellation is built, we need to configure each host in it. -Go back to the first computer that you placed and create a startup file, by running `edit startup`. + 1. Press F3 to open Minecraft's debug screen. + 2. Go back to the first computer and look at it. On the right of the screen about halfway down you should see an entry + labelled `Targeted Block`, the numbers correspond to the position of the block that you are looking at. Write these + numbers down. + 3. Open the computer's UI, and run `edit startup.lua`. + 4. Type the following code into the file, replacing `x`, `y`, and `z` with the coordinates you just wrote down. -Type the following code into the file: -```lua -shell.run("gps", "host", x, y, z) -``` + ```lua + shell.run("gps", "host", x, y, z) + ``` + 5. Save the file, and then reboot the computer (hold Ctrl+R or run the `reboot` program) to run the startup + program. -Escape from the computer GUI and then press F3 to open Minecraft's debug screen and then look at the computer -(without opening the GUI). On the right of the screen about halfway down you should see an entry labeled `Targeted -Block`, the numbers correspond to the position of the block that you are looking at. Replace `x` with the first number, -`y` with the second number, and `z` with the third number. - -For example, if I had a computer at x = 59, y = 5, z = -150, then my F3 debug screen entry would be `Target -Block: 59, 5, -150` and I would change my startup file to this `shell.run("gps", "host", 59, 5, -150)`. - -To hide Minecraft's debug screen, press F3 again. - -Create similar startup files for the other computers in your constellation, making sure to input the each computer's own -coordinates. - -> [Modem messages come from the computer's position, not the modem's][!WARNING] -> Wireless modems transmit from the block that they are attached to *not* the block space that they occupy, the -> coordinates that you input into your GPS host should be the position of the computer and not the position of the modem. +Repeat this process for the other three computers. Congratulations, your constellation is now fully set up! You can test it by placing another computer close by, placing a wireless modem on it, and running the `gps locate` program (or calling the [`gps.locate`] function). - -> [Why use Minecraft's coordinates?][!INFO] -> CC doesn't care if you use Minecraft's coordinate system, so long as all of the GPS hosts with overlapping ranges use -> the same reference point (requesting computers will get confused if hosts have different reference points). However, -> using MC's coordinate system does provide a nice standard to adopt server-wide. It also is consistent with how command -> computers get their location, they use MC's command system to get their block which returns that in MC's coordinate -> system. diff --git a/doc/images/gps-constellation-area.png b/doc/images/gps-constellation-area.png new file mode 100644 index 000000000..842fb13f4 Binary files /dev/null and b/doc/images/gps-constellation-area.png differ diff --git a/doc/images/gps-constellation-built.png b/doc/images/gps-constellation-built.png new file mode 100644 index 000000000..08e53c708 Binary files /dev/null and b/doc/images/gps-constellation-built.png differ diff --git a/doc/images/gps-constellation-example.png b/doc/images/gps-constellation-example.png deleted file mode 100644 index 4fb8ffe7d..000000000 Binary files a/doc/images/gps-constellation-example.png and /dev/null differ diff --git a/doc/reference/block_details.md b/doc/reference/block_details.md new file mode 100644 index 000000000..b0bc14bb6 --- /dev/null +++ b/doc/reference/block_details.md @@ -0,0 +1,99 @@ +--- +module: [kind=reference] block_details +since: 1.64 +changed: 1.76 Added block state. +changed: 1.117.0 Added map colour. +--- + + + +# Block details +Several functions in CC: Tweaked, such as [`turtle.inspect`] and [`commands.getBlockInfo`] provide a way to get +information about a block in the world. This page details information about blocks that CC: Tweaked may return. + +## Basic information +Block information will *always* contain: + - `name: string`: The namespaced ID for this block, e.g. `minecraft:dirt`. See [the Minecraft wiki][block ids] for a + list of vanilla block IDs. + - `state: { [string] = any}`: A table containing the block state of the block. + +### Example +A fully hydrated block of farmland: + +```lua {data-no-run=1} +{ + name = "minecraft:farmland", + state = { + moisture = 7 + } +} +``` + +An extended piston, facing upwards: + +```lua {data-no-run=1} +{ + name = "minecraft:piston", + state = { + facing = "up", + extended = true + } +} +``` + +## Block tags +The [tags][block tags] a block has. + + - `tags: { [string] = boolean }`: The set of tags for this block. This is a mapping of tag name to `true`. + +While the representation of tags is a little more complicated then a single list, this makes it very easy to check if a +block has a certain tag: + +```lua +--- Check if the block in front of the turtle is a log. +local function is_log() + local ok, block = turtle.inspect() + return ok and block.tags["minecraft:logs"] +end +``` + +### Example +A fully hydrated block of farmland: + +```lua {data-no-run=1} +{ + name = "minecraft:farmland", + state = { ... }, + tags = { + ["minecraft:mineable/shovel"] = true, + } +} +``` + +## Map colour +The colour the block will appear on the map, if specified. + + - `mapColour?: number`: The colour of the block, as an RGB hex value. + - `mapColor?: number`: The color of the block, as an RGB hex value. + +The map colour is just returned as a plain number (e.g. `9923917` for farmland). It can either be displayed in hex with +[`string.format`], or converted to individual RGB values with [`colors.unpackRGB`]. + +### Example +A fully hydrated block of farmland: + +```lua {data-no-run=1} +{ + name = "minecraft:farmland", + state = { ... }, + mapColour = 9923917, + mapColor = 9923917, +} +``` + +[block ids]: https://minecraft.wiki/w/Java_Edition_data_values#Blocks "Java Edition data values on the Minecraft Wiki" +[block tags]:https://minecraft.wiki/w/Block_tag_%28Java_Edition%29 "Block tags on the Minecraft Wiki" diff --git a/doc/reference/item_details.md b/doc/reference/item_details.md new file mode 100644 index 000000000..a140e1056 --- /dev/null +++ b/doc/reference/item_details.md @@ -0,0 +1,267 @@ +--- +module: [kind=reference] item_details +since: 1.64 +changed: 1.94.0 Add NBT hash, item tags, lore, enchantment and unbreakable flag. +changed: 1.100.9 Add item groups. +changed: 1.117.0 Added map colour and potion effects. +--- + + + +# Item details +Several functions in CC: Tweaked, such as [`turtle.getItemDetail`] and [`inventory.getItemDetail`] provide a way to get +information about an item stack. This page details information about items that CC: Tweaked may return. + +Some methods (such as [`inventory.list`] and [`turtle.getItemDetail`] without the `detailed` flag), will only return +the "Basic information" about the item. + +## Basic information +Item information will *always* contain: + - `name: string`: The namespaced ID for this item, e.g. `minecraft:dirt`. See [the Minecraft wiki][item ids] for a + list of vanilla item IDs. + - `count: number`: The number of items in the stack. + - `nbt?: string`: A hash of the NBT in the stack. While this does not expose any information about the item's NBT, it + can be used as a way to compare items. If two items have the same `name` and `nbt`, then all other properties + (e.g. durability, enchantment) will be the same. + +### Example +A stack of 32 Stripped Acacia Logs: + +```lua {data-no-run=1} +{ + name = "minecraft:stripped_acacia_log", + count = 32, +} +``` + +A turtle with an upgrade attached: + +```lua {data-no-run=1} +{ + name = "computercraft:turtle_normal", + count = 1, + nbt = "a33095c2eb17c10e12f2b970c4e1b2bb", +} +``` + +## Display information +Common information shown in the item's tooltip: + + - `displayName: string`: The translated display name of the item. This uses the *server's* language. This will + typically be English on multi-player servers, and your current language on single player. + - `lore: { string... }`: Additional lore about this item, as a list of strings. + +### Example +A stack of Stripped Acacia Logs: + +```lua {data-no-run=1} +{ + name = "minecraft:stripped_acacia_log", + count = 32, + displayName = "Stripped Acacia Log", +} +``` + +## Max count +The maximum number of items this item can stack to: + + - `maxCount: number`: The max possible size of the item stack. + +### Example +A stack of Stripped Acacia Logs: + +```lua {data-no-run=1} +{ + name = "minecraft:stripped_acacia_log", + count = 32, + maxCount = 64, +} +``` + +## Item tags +The [tags][item tags] an item has. + + - `tags: { [string] = boolean }`: The set of tags for this item. This is a mapping of tag name to `true`. + +While the representation of tags is a little more complicated then a single list, this makes it very easy to check if an +item has a certain tag: + +```lua +--- Check if the item in the turtle's inventory is a log. +local function is_log(slot) + local ok, block = turtle.getItemDetail(slot, true) + return ok and block.tags["minecraft:logs"] +end +``` + +### Example +A stack of Stripped Acacia Logs: + +```lua {data-no-run=1} +{ + name = "minecraft:stripped_acacia_log", + count = 32, + tags = { + ["minecraft:acacia_logs"] = true, + ["minecraft:logs"] = true, + ["minecraft:logs_that_burn"] = true, + } +} +``` + +## Item groups +The creative tabs this item appears on: + + - `itemGroups: { { id = string, displayName = string }... }`: The item groups this item appears on. Each item group is + stored as a table, containing its id and display name. + +> [Version differences][!INFO] +> This information is not available on Minecraft 1.19.3 to 1.20.3. This field is present, but empty on those versions. + +### Example +A stack of Stripped Acacia Logs: + +```lua {data-no-run=1} +{ + name = "minecraft:stripped_acacia_log", + count = 32, + itemGroups = { + { + id = "minecraft:building_blocks", + displayName = "Building Blocks" + } + } +} +``` + +## Damage and durability +If this item can be damaged (e.g. a pickaxe), then its damage and durability will be available: + - `damage: number`: The amount of damage this item has taken. + - `maxDamage: number`: The maximum amount of damage this item has taken. + - `durability?: number`: If this item is damaged (i.e. the durability bar is visible), the percentage left on the + durability bar, between 0 and 1 (inclusive). + - `unbreakable?: boolean`: `true`, if the item is nubreakable + +### Example +An unused diamond pickaxe: + +```lua {data-no-run=1} +{ + name = "minecraft:diamond_pickaxe", + count = 1, + damage = 0, + maxDamage = 1561, +} +``` + +A half-used wooden pickaxe: + +```lua {data-no-run=1} +{ + name = "minecraft:wooden_pickaxe", + count = 1, + damage = 21, + maxDamage = 59, + durability = 0.615, +} +``` + +## Enchantments +The enchantments this item has. This includes both tools and enchanted books. + + - `enchantments: { table... }`: The enchantments this item has. Each enchantment is a table containing several + properties: + - `name: string`: The namespaced ID for this enchantment, e.g. `minecraft:efficiency`. See [the Minecraft + wiki][enchantment ids] for a list of vanilla enchantment IDs. + - `displayName: string`: The translated display name for this enchantment. + - `level: number`: The level for this enchantment. + +### Example +A diamond pickaxe with Efficiency V: +```lua {data-no-run=1} +{ + name = "minecraft:diamond_pickaxe", + count = 1, + enchantments = { + { + name = "minecraft:efficiency", + level = 5, + displayName = "Efficiency V", + } + } +} +``` + +## Potion effects +The effects this potion (or potion-embued item, such as a tipped arrow) has: + + - `potionEffects: { table... }`: The effects this item has. Each potion effect is a table containing several + properties: + - `name: string`: The namespaced ID for this effect, e.g. `minecraft:regeneration`. See [the Minecraft wiki][effect + ids] for a list of vanilla effect IDs. + - `displayName: string`: The translated display name for this potion effect. + - `duration?: number`: The duration this effect will last for (if not instant), in seconds. + - `potency?: number`: The potency of this potion. + +### Example +A basic Potion of Healing: + +```lua {data-no-run=1} +{ + name = "minecraft:potion", + displayName = "Potion of Healing", + potionEffects = { + { + name = "minecraft:instant_health", + displayName = "Instant Health", + }, + }, +} +``` + +An upgraded Potion of Regeneration: + +```lua {data-no-run=1} +{ + name = "minecraft:potion", + displayName = "Potion of Regeneration", + potionEffects = { + { + name = "minecraft:regeneration", + displayName = "Regeneration II", + duration = 22.5, + potency = 2, + }, + }, +} +``` + +## Map colour +The colour the item's block form will appear on the map, if specified. + + - `mapColour?: number`: The colour of the block, as an RGB hex value. + - `mapColor?: number`: The color of the block, as an RGB hex value. + +The map colour is just returned as a plain number (e.g. `9923917` for dirt). It can either be displayed in hex with +[`string.format`], or converted to individual RGB values with [`colors.unpackRGB`]. + +### Example +A stack of Stripped Acacia Logs: + +```lua {data-no-run=1} +{ + name = "minecraft:stripped_acacia_log", + count = 32, + mapColour = 14188339, + mapColor = 14188339, +} +``` + +[item ids]: https://minecraft.wiki/w/Java_Edition_data_values#Items "Java Edition data values on the Minecraft Wiki" +[item tags]:https://minecraft.wiki/w/Item_tag_%28Java_Edition%29 "Item tags on the Minecraft Wiki" +[effect ids]: https://minecraft.wiki/w/Java_Edition_data_values#Effects "Java Edition data values on the Minecraft Wiki" +[enchantment ids]: https://minecraft.wiki/w/Java_Edition_data_values#Enchantments "Java Edition data values on the Minecraft Wiki" diff --git a/gradle.properties b/gradle.properties index cfb39c8c4..30c020eb0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false # Mod properties isUnstable=true -modVersion=1.116.2 +modVersion=1.117.0 # Minecraft properties: We want to configure this here so we can read it in settings.gradle mcVersion=1.21.11 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 650af498a..ae129908d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ jmh = "1.37" # Build tools cctJavadoc = "1.8.5" checkstyle = "12.1.1" -errorProne-core = "2.43.0" +errorProne-core = "2.45.0" errorProne-plugin = "4.3.0" fabric-loom = "1.14.5" githubRelease = "2.5.2" @@ -67,10 +67,10 @@ ideaExt = "1.3" illuaminate = "0.1.0-83-g1131f68" lwjgl = "3.3.6" minotaur = "2.8.7" -modDevGradle = "2.0.122" +modDevGradle = "2.0.124" nullAway = "0.12.14" spotless = "8.0.0" -teavm = "0.13.0-SQUID.2" +teavm = "0.14.0-SQUID.1" vanillaExtract = "0.3.1" versionCatalogUpdate = "1.0.1" diff --git a/package-lock.json b/package-lock.json index 496e265ad..fb0d1d704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,15 @@ "@squid-dev/cc-web-term": "^2.0.0", "preact": "^10.5.5", "setimmediate": "^1.0.5", - "tslib": "^2.0.3" + "tslib": "^2.0.3", + "wasm-feature-detect": "^1.8.0" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-typescript": "^12.0.0", "@rollup/plugin-url": "^8.0.1", "@swc/core": "^1.3.92", - "@types/node": "^24.0.0", + "@types/node": "^25.0.0", "lightningcss": "^1.22.0", "preact-render-to-string": "^6.2.1", "rehype": "^13.0.0", @@ -31,9 +32,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -48,9 +49,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ "arm" ], @@ -65,9 +66,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ "arm64" ], @@ -82,9 +83,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -99,9 +100,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -116,9 +117,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -133,9 +134,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -150,9 +151,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -167,9 +168,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ "arm" ], @@ -184,9 +185,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ "arm64" ], @@ -201,9 +202,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ "ia32" ], @@ -218,9 +219,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ "loong64" ], @@ -235,9 +236,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ "mips64el" ], @@ -252,9 +253,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", "cpu": [ "ppc64" ], @@ -269,9 +270,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", "cpu": [ "riscv64" ], @@ -286,9 +287,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", "cpu": [ "s390x" ], @@ -303,9 +304,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", "cpu": [ "x64" ], @@ -320,9 +321,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ "arm64" ], @@ -337,9 +338,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ "x64" ], @@ -354,9 +355,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -371,9 +372,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -388,9 +389,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", "cpu": [ "arm64" ], @@ -405,9 +406,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -422,9 +423,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ "arm64" ], @@ -439,9 +440,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ "ia32" ], @@ -456,9 +457,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ "x64" ], @@ -571,9 +572,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.4.tgz", + "integrity": "sha512-PWU3Y92H4DD0bOqorEPp1Y0tbzwAurFmIYpjcObv5axGVOtcTlB0b2UKMd2echo08MgN7jO8WQZSSysvfisFSQ==", "cpu": [ "arm" ], @@ -585,9 +586,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.4.tgz", + "integrity": "sha512-Gw0/DuVm3rGsqhMGYkSOXXIx20cC3kTlivZeuaGt4gEgILivykNyBWxeUV5Cf2tDA2nPLah26vq3emlRrWVbng==", "cpu": [ "arm64" ], @@ -599,9 +600,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.4.tgz", + "integrity": "sha512-+w06QvXsgzKwdVg5qRLZpTHh1bigHZIqoIUPtiqh05ZiJVUQ6ymOxaPkXTvRPRLH88575ZCRSRM3PwIoNma01Q==", "cpu": [ "arm64" ], @@ -613,9 +614,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.4.tgz", + "integrity": "sha512-EB4Na9G2GsrRNRNFPuxfwvDRDUwQEzJPpiK1vo2zMVhEeufZ1k7J1bKnT0JYDfnPC7RNZ2H5YNQhW6/p2QKATw==", "cpu": [ "x64" ], @@ -627,9 +628,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.4.tgz", + "integrity": "sha512-bldA8XEqPcs6OYdknoTMaGhjytnwQ0NClSPpWpmufOuGPN5dDmvIa32FygC2gneKK4A1oSx86V1l55hyUWUYFQ==", "cpu": [ "arm64" ], @@ -641,9 +642,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.4.tgz", + "integrity": "sha512-3T8GPjH6mixCd0YPn0bXtcuSXi1Lj+15Ujw2CEb7dd24j9thcKscCf88IV7n76WaAdorOzAgSSbuVRg4C8V8Qw==", "cpu": [ "x64" ], @@ -655,9 +656,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.4.tgz", + "integrity": "sha512-UPMMNeC4LXW7ZSHxeP3Edv09aLsFUMaD1TSVW6n1CWMECnUIJMFFB7+XC2lZTdPtvB36tYC0cJWc86mzSsaviw==", "cpu": [ "arm" ], @@ -669,9 +670,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.4.tgz", + "integrity": "sha512-H8uwlV0otHs5Q7WAMSoyvjV9DJPiy5nJ/xnHolY0QptLPjaSsuX7tw+SPIfiYH6cnVx3fe4EWFafo6gH6ekZKA==", "cpu": [ "arm" ], @@ -683,9 +684,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.4.tgz", + "integrity": "sha512-BLRwSRwICXz0TXkbIbqJ1ibK+/dSBpTJqDClF61GWIrxTXZWQE78ROeIhgl5MjVs4B4gSLPCFeD4xML9vbzvCQ==", "cpu": [ "arm64" ], @@ -697,9 +698,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.4.tgz", + "integrity": "sha512-6bySEjOTbmVcPJAywjpGLckK793A0TJWSbIa0sVwtVGfe/Nz6gOWHOwkshUIAp9j7wg2WKcA4Snu7Y1nUZyQew==", "cpu": [ "arm64" ], @@ -711,9 +712,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.4.tgz", + "integrity": "sha512-U0ow3bXYJZ5MIbchVusxEycBw7bO6C2u5UvD31i5IMTrnt2p4Fh4ZbHSdc/31TScIJQYHwxbj05BpevB3201ug==", "cpu": [ "loong64" ], @@ -725,9 +726,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.4.tgz", + "integrity": "sha512-iujDk07ZNwGLVn0YIWM80SFN039bHZHCdCCuX9nyx3Jsa2d9V/0Y32F+YadzwbvDxhSeVo9zefkoPnXEImnM5w==", "cpu": [ "ppc64" ], @@ -739,9 +740,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.4.tgz", + "integrity": "sha512-MUtAktiOUSu+AXBpx1fkuG/Bi5rhlorGs3lw5QeJ2X3ziEGAq7vFNdWVde6XGaVqi0LGSvugwjoxSNJfHFTC0g==", "cpu": [ "riscv64" ], @@ -753,9 +754,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.4.tgz", + "integrity": "sha512-btm35eAbDfPtcFEgaXCI5l3c2WXyzwiE8pArhd66SDtoLWmgK5/M7CUxmUglkwtniPzwvWioBKKl6IXLbPf2sQ==", "cpu": [ "riscv64" ], @@ -767,9 +768,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.4.tgz", + "integrity": "sha512-uJlhKE9ccUTCUlK+HUz/80cVtx2RayadC5ldDrrDUFaJK0SNb8/cCmC9RhBhIWuZ71Nqj4Uoa9+xljKWRogdhA==", "cpu": [ "s390x" ], @@ -781,9 +782,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.4.tgz", + "integrity": "sha512-jjEMkzvASQBbzzlzf4os7nzSBd/cvPrpqXCUOqoeCh1dQ4BP3RZCJk8XBeik4MUln3m+8LeTJcY54C/u8wb3DQ==", "cpu": [ "x64" ], @@ -795,9 +796,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.4.tgz", + "integrity": "sha512-lu90KG06NNH19shC5rBPkrh6mrTpq5kviFylPBXQVpdEu0yzb0mDgyxLr6XdcGdBIQTH/UAhDJnL+APZTBu1aQ==", "cpu": [ "x64" ], @@ -809,9 +810,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.4.tgz", + "integrity": "sha512-dFDcmLwsUzhAm/dn0+dMOQZoONVYBtgik0VuY/d5IJUUb787L3Ko/ibvTvddqhb3RaB7vFEozYevHN4ox22R/w==", "cpu": [ "arm64" ], @@ -823,9 +824,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.4.tgz", + "integrity": "sha512-WvUpUAWmUxZKtRnQWpRKnLW2DEO8HB/l8z6oFFMNuHndMzFTJEXzaYJ5ZAmzNw0L21QQJZsUQFt2oPf3ykAD/w==", "cpu": [ "arm64" ], @@ -837,9 +838,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.4.tgz", + "integrity": "sha512-JGbeF2/FDU0x2OLySw/jgvkwWUo05BSiJK0dtuI4LyuXbz3wKiC1xHhLB1Tqm5VU6ZZDmAorj45r/IgWNWku5g==", "cpu": [ "ia32" ], @@ -851,9 +852,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.4.tgz", + "integrity": "sha512-zuuC7AyxLWLubP+mlUwEyR8M1ixW1ERNPHJfXm8x7eQNP4Pzkd7hS3qBuKBR70VRiQ04Kw8FNfRMF5TNxuZq2g==", "cpu": [ "x64" ], @@ -865,9 +866,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.4.tgz", + "integrity": "sha512-Sbx45u/Lbb5RyptSbX7/3deP+/lzEmZ0BTSHxwxN/IMOZDZf8S0AGo0hJD5n/LQssxb5Z3B4og4P2X6Dd8acCA==", "cpu": [ "x64" ], @@ -889,9 +890,9 @@ } }, "node_modules/@swc/core": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz", - "integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.5.tgz", + "integrity": "sha512-VRy+AEO0zqUkwV9uOgqXtdI5tNj3y3BZI+9u28fHNjNVTtWYVNIq3uYhoGgdBOv7gdzXlqfHKuxH5a9IFAvopQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -907,16 +908,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.14.0", - "@swc/core-darwin-x64": "1.14.0", - "@swc/core-linux-arm-gnueabihf": "1.14.0", - "@swc/core-linux-arm64-gnu": "1.14.0", - "@swc/core-linux-arm64-musl": "1.14.0", - "@swc/core-linux-x64-gnu": "1.14.0", - "@swc/core-linux-x64-musl": "1.14.0", - "@swc/core-win32-arm64-msvc": "1.14.0", - "@swc/core-win32-ia32-msvc": "1.14.0", - "@swc/core-win32-x64-msvc": "1.14.0" + "@swc/core-darwin-arm64": "1.15.5", + "@swc/core-darwin-x64": "1.15.5", + "@swc/core-linux-arm-gnueabihf": "1.15.5", + "@swc/core-linux-arm64-gnu": "1.15.5", + "@swc/core-linux-arm64-musl": "1.15.5", + "@swc/core-linux-x64-gnu": "1.15.5", + "@swc/core-linux-x64-musl": "1.15.5", + "@swc/core-win32-arm64-msvc": "1.15.5", + "@swc/core-win32-ia32-msvc": "1.15.5", + "@swc/core-win32-x64-msvc": "1.15.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -928,9 +929,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.14.0.tgz", - "integrity": "sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.5.tgz", + "integrity": "sha512-RvdpUcXrIz12yONzOdQrJbEnq23cOc2IHOU1eB8kPxPNNInlm4YTzZEA3zf3PusNpZZLxwArPVLCg0QsFQoTYw==", "cpu": [ "arm64" ], @@ -945,9 +946,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.14.0.tgz", - "integrity": "sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.5.tgz", + "integrity": "sha512-ufJnz3UAff/8G5OfqZZc5cTQfGtXyXVLTB8TGT0xjkvEbfFg8jZUMDBnZT/Cn0k214JhMjiLCNl0A8aY/OKsYQ==", "cpu": [ "x64" ], @@ -962,9 +963,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.14.0.tgz", - "integrity": "sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.5.tgz", + "integrity": "sha512-Yqu92wIT0FZKLDWes+69kBykX97hc8KmnyFwNZGXJlbKUGIE0hAIhbuBbcY64FGSwey4aDWsZ7Ojk89KUu9Kzw==", "cpu": [ "arm" ], @@ -979,9 +980,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.14.0.tgz", - "integrity": "sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.5.tgz", + "integrity": "sha512-3gR3b5V1abe/K1GpD0vVyZgqgV+ykuB5QNecDYzVroX4QuN+amCzQaNSsVM8Aj6DbShQCBTh3hGHd2f3vZ8gCw==", "cpu": [ "arm64" ], @@ -996,9 +997,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.14.0.tgz", - "integrity": "sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.5.tgz", + "integrity": "sha512-Of+wmVh5h47tTpN9ghHVjfL0CJrgn99XmaJjmzWFW7agPdVY6gTDgkk6zQ6q4hcDQ7hXb0BGw6YFpuanBzNPow==", "cpu": [ "arm64" ], @@ -1013,9 +1014,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.14.0.tgz", - "integrity": "sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.5.tgz", + "integrity": "sha512-98kuPS0lZVgjmc/2uTm39r1/OfwKM0PM13ZllOAWi5avJVjRd/j1xA9rKeUzHDWt+ocH9mTCQsAT1jjKSq45bg==", "cpu": [ "x64" ], @@ -1030,9 +1031,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.14.0.tgz", - "integrity": "sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.5.tgz", + "integrity": "sha512-Rk+OtNQP3W/dZExL74LlaakXAQn6/vbrgatmjFqJPO4RZkq+nLo5g7eDUVjyojuERh7R2yhqNvZ/ZZQe8JQqqA==", "cpu": [ "x64" ], @@ -1047,9 +1048,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.14.0.tgz", - "integrity": "sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.5.tgz", + "integrity": "sha512-e3RTdJ769+PrN25iCAlxmsljEVu6iIWS7sE21zmlSiipftBQvSAOWuCDv2A8cH9lm5pSbZtwk8AUpIYCNsj2oQ==", "cpu": [ "arm64" ], @@ -1064,9 +1065,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.14.0.tgz", - "integrity": "sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.5.tgz", + "integrity": "sha512-NmOdl6kyAw6zMz36zCdopTgaK2tcLA53NhUsTRopBc/796Fp87XdsslRHglybQ1HyXIGOQOKv2Y14IUbeci4BA==", "cpu": [ "ia32" ], @@ -1081,9 +1082,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.14.0.tgz", - "integrity": "sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.5.tgz", + "integrity": "sha512-EPXJRf0A8eOi8woXf/qgVIWRl9yeSl0oN1ykGZNCGI7oElsfxUobJFmpJFJoVqKFfd1l0c+GPmWsN2xavTFkNw==", "cpu": [ "x64" ], @@ -1169,9 +1170,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", - "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", + "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", "dev": true, "license": "MIT", "dependencies": { @@ -1366,9 +1367,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1379,32 +1380,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, "node_modules/estree-util-is-identifier-name": { @@ -1680,9 +1681,9 @@ } }, "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "dev": true, "license": "MIT" }, @@ -2178,9 +2179,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dev": true, "license": "MIT", "dependencies": { @@ -2779,20 +2780,19 @@ } }, "node_modules/preact": { - "version": "10.27.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", - "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.0.tgz", + "integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, "node_modules/preact-render-to-string": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.3.tgz", - "integrity": "sha512-7oHG7jzjriqsFPkSPiPnzrQ0GcxFm6wOkYWNdStK5Ks9YlWSQQXKGBRAX4nKDdqX7HAQuRvI4pZNZMycK4WwDw==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.4.tgz", + "integrity": "sha512-Bn6eQZ5SQ5loVEcC/mZmKT7HzO5Z/+vYzxfE/W2N468oSoNMJVdFGApF0GyXq0lDthuyXKTmtZ8k20NpYjr6Rw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2925,12 +2925,11 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.53.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.4.tgz", + "integrity": "sha512-YpXaaArg0MvrnJpvduEDYIp7uGOqKXbH9NsHGQ6SxKCOsNAjZF018MmxefFUulVP2KLtiGw1UvZbr+/ekjvlDg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -2942,28 +2941,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.53.4", + "@rollup/rollup-android-arm64": "4.53.4", + "@rollup/rollup-darwin-arm64": "4.53.4", + "@rollup/rollup-darwin-x64": "4.53.4", + "@rollup/rollup-freebsd-arm64": "4.53.4", + "@rollup/rollup-freebsd-x64": "4.53.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.4", + "@rollup/rollup-linux-arm-musleabihf": "4.53.4", + "@rollup/rollup-linux-arm64-gnu": "4.53.4", + "@rollup/rollup-linux-arm64-musl": "4.53.4", + "@rollup/rollup-linux-loong64-gnu": "4.53.4", + "@rollup/rollup-linux-ppc64-gnu": "4.53.4", + "@rollup/rollup-linux-riscv64-gnu": "4.53.4", + "@rollup/rollup-linux-riscv64-musl": "4.53.4", + "@rollup/rollup-linux-s390x-gnu": "4.53.4", + "@rollup/rollup-linux-x64-gnu": "4.53.4", + "@rollup/rollup-linux-x64-musl": "4.53.4", + "@rollup/rollup-openharmony-arm64": "4.53.4", + "@rollup/rollup-win32-arm64-msvc": "4.53.4", + "@rollup/rollup-win32-ia32-msvc": "4.53.4", + "@rollup/rollup-win32-x64-gnu": "4.53.4", + "@rollup/rollup-win32-x64-msvc": "4.53.4", "fsevents": "~2.3.2" } }, @@ -3010,23 +3009,23 @@ } }, "node_modules/style-to-js": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz", - "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "dev": true, "license": "MIT", "dependencies": { - "style-to-object": "1.0.11" + "style-to-object": "1.0.14" } }, "node_modules/style-to-object": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz", - "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "dev": true, "license": "MIT", "dependencies": { - "inline-style-parser": "0.2.4" + "inline-style-parser": "0.2.7" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -3068,17 +3067,16 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -3097,7 +3095,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3266,6 +3263,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index db331c4fa..640ce0613 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,15 @@ "@squid-dev/cc-web-term": "^2.0.0", "preact": "^10.5.5", "setimmediate": "^1.0.5", - "tslib": "^2.0.3" + "tslib": "^2.0.3", + "wasm-feature-detect": "^1.8.0" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-typescript": "^12.0.0", "@rollup/plugin-url": "^8.0.1", "@swc/core": "^1.3.92", - "@types/node": "^24.0.0", + "@types/node": "^25.0.0", "lightningcss": "^1.22.0", "preact-render-to-string": "^6.2.1", "rehype": "^13.0.0", diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java index 92d77440d..ef3831734 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -98,6 +98,7 @@ public final class ClientRegistry { public static void registerMenuScreens(RegisterMenuScreen register) { register.>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new); register.>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new); + register.register(ModRegistry.Menus.POCKET_COMPUTER_LECTERN.get(), PocketComputerLecternScreen::new); register.register(ModRegistry.Menus.TURTLE.get(), TurtleScreen::new); register.register(ModRegistry.Menus.PRINTER.get(), PrinterScreen::new); diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java index a451b9c97..e0f654ae2 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java @@ -8,9 +8,9 @@ import dan200.computercraft.client.gui.widgets.ComputerSidebar; import dan200.computercraft.client.gui.widgets.DynamicImageButton; import dan200.computercraft.client.gui.widgets.TerminalWidget; import dan200.computercraft.client.network.ClientNetworking; +import dan200.computercraft.core.input.UserComputerInput; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.computer.core.ComputerFamily; -import dan200.computercraft.shared.computer.core.InputHandler; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.upload.FileUpload; import dan200.computercraft.shared.computer.upload.UploadResult; @@ -58,7 +58,8 @@ public abstract class AbstractComputerScreen ext protected @Nullable TerminalWidget terminal; protected Terminal terminalData; protected final ComputerFamily family; - protected final InputHandler input; + protected final UserComputerInput computerInput; + protected final ClientComputerActions computerActions; protected final int sidebarYOffset; @@ -72,7 +73,8 @@ public abstract class AbstractComputerScreen ext family = container.getFamily(); displayStack = container.getDisplayStack(); uploadMaxSize = container.getUploadMaxSize(); - input = new ClientInputHandler(menu); + computerInput = new UserComputerInput(new ClientComputerInput(menu), menu.getTerminal()); + computerActions = new ClientComputerActions(menu); this.sidebarYOffset = sidebarYOffset; } @@ -88,7 +90,7 @@ public abstract class AbstractComputerScreen ext super.init(); terminal = addRenderableWidget(createTerminal()); - ComputerSidebar.addButtons(menu::isOn, input, this::addRenderableWidget, leftPos, topPos + sidebarYOffset); + ComputerSidebar.addButtons(menu::isOn, computerActions, this::addRenderableWidget, leftPos, topPos + sidebarYOffset); setFocused(terminal); } diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/ClientComputerActions.java b/projects/common/src/client/java/dan200/computercraft/client/gui/ClientComputerActions.java new file mode 100644 index 000000000..1b7f36edb --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/ClientComputerActions.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.gui; + +import dan200.computercraft.client.network.ClientNetworking; +import dan200.computercraft.shared.network.server.ComputerActionServerMessage; +import net.minecraft.world.inventory.AbstractContainerMenu; + +/** + * Actions that can be applied to a computer. + * + * @see ComputerActionServerMessage + */ +public final class ClientComputerActions { + private final AbstractContainerMenu menu; + + public ClientComputerActions(AbstractContainerMenu menu) { + this.menu = menu; + } + + public void terminate() { + ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE)); + } + + public void turnOn() { + ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON)); + } + + public void shutdown() { + ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN)); + } + + public void reboot() { + ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT)); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java b/projects/common/src/client/java/dan200/computercraft/client/gui/ClientComputerInput.java similarity index 67% rename from projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java rename to projects/common/src/client/java/dan200/computercraft/client/gui/ClientComputerInput.java index 7fd121a44..15956286e 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/ClientInputHandler.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/ClientComputerInput.java @@ -5,9 +5,8 @@ package dan200.computercraft.client.gui; import dan200.computercraft.client.network.ClientNetworking; -import dan200.computercraft.shared.computer.core.InputHandler; +import dan200.computercraft.core.input.ComputerInput; import dan200.computercraft.shared.computer.menu.ComputerMenu; -import dan200.computercraft.shared.network.server.ComputerActionServerMessage; import dan200.computercraft.shared.network.server.KeyEventServerMessage; import dan200.computercraft.shared.network.server.MouseEventServerMessage; import dan200.computercraft.shared.network.server.PasteEventComputerMessage; @@ -16,37 +15,17 @@ import net.minecraft.world.inventory.AbstractContainerMenu; import java.nio.ByteBuffer; /** - * An {@link InputHandler} for use on the client. + * An {@link ComputerInput} for use on the client. *

- * This queues events on the remote player's open {@link ComputerMenu}. + * This queues events on the player's open {@link ComputerMenu}. */ -public final class ClientInputHandler implements InputHandler { +public final class ClientComputerInput implements ComputerInput { private final AbstractContainerMenu menu; - public ClientInputHandler(AbstractContainerMenu menu) { + public ClientComputerInput(AbstractContainerMenu menu) { this.menu = menu; } - @Override - public void terminate() { - ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE)); - } - - @Override - public void turnOn() { - ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON)); - } - - @Override - public void shutdown() { - ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN)); - } - - @Override - public void reboot() { - ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT)); - } - @Override public void keyDown(int key, boolean repeat) { ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key)); diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java index a6c846a3f..d7278fcd6 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java @@ -32,7 +32,10 @@ public final class ComputerScreen extends Abstra @Override protected TerminalWidget createTerminal() { - return new TerminalWidget(terminalData, input, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH + BORDER, topPos + BORDER); + return new TerminalWidget( + terminalData, computerInput, computerActions, + leftPos + AbstractComputerMenu.SIDEBAR_WIDTH + BORDER, topPos + BORDER + ); } @Override diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java index b876fcbe2..c7306dae8 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java @@ -5,7 +5,7 @@ package dan200.computercraft.client.gui; import dan200.computercraft.client.gui.widgets.TerminalWidget; -import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.input.UserComputerInput; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import net.minecraft.client.KeyMapping; import net.minecraft.client.ScrollWheelHandler; @@ -29,7 +29,8 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull; */ public class NoTermComputerScreen extends Screen implements MenuAccess { private final T menu; - private final Terminal terminalData; + protected final UserComputerInput computerInput; + protected final ClientComputerActions computerActions; private @Nullable TerminalWidget terminal; private final ScrollWheelHandler scrollHandler = new ScrollWheelHandler(); @@ -37,7 +38,8 @@ public class NoTermComputerScreen extends Screen public NoTermComputerScreen(T menu, Inventory player, Component title) { super(title); this.menu = menu; - terminalData = menu.getTerminal(); + computerInput = new UserComputerInput(new ClientComputerInput(menu), menu.getTerminal()); + computerActions = new ClientComputerActions(menu); } @Override @@ -55,7 +57,7 @@ public class NoTermComputerScreen extends Screen super.init(); - terminal = addWidget(new TerminalWidget(terminalData, new ClientInputHandler(menu), 0, 0)); + terminal = addWidget(new TerminalWidget(menu.getTerminal(), computerInput, computerActions, 0, 0)); terminal.visible = false; terminal.active = false; setFocused(terminal); diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/PocketComputerLecternScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/PocketComputerLecternScreen.java new file mode 100644 index 000000000..756f0f3c1 --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/PocketComputerLecternScreen.java @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import dan200.computercraft.client.render.CustomLecternRenderer; +import dan200.computercraft.shared.lectern.PocketComputerLecternMenu; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import org.joml.Matrix4f; +import org.joml.Vector2i; +import org.joml.Vector2ic; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT; +import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH; + +/** + * The screen for computers on lecterns. + *

+ * This extends {@link NoTermComputerScreen}, but with support for interacting with the lectern's pocket computer. + */ +public final class PocketComputerLecternScreen extends NoTermComputerScreen { + public PocketComputerLecternScreen(PocketComputerLecternMenu menu, Inventory player, Component title) { + super(menu, player, title); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + var position = getMousePosition(); + if (position != null) { + computerInput.mouseClick(event.button() + 1, position.x() + 1, position.y() + 1); + return true; + } + + return super.mouseClicked(event, doubleClick); + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) { + var position = getMousePosition(); + if (position != null) { + computerInput.mouseDrag(event.button() + 1, position.x() + 1, position.y() + 1); + return true; + } + + return super.mouseDragged(event, dragX, dragY); + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + var position = getMousePosition(); + if (position != null) { + computerInput.mouseUp(event.button() + 1, position.x() + 1, position.y() + 1); + return true; + } + + return super.mouseReleased(event); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double deltaX, double deltaY) { + var position = getMousePosition(); + if (position != null && deltaY != 0) { + computerInput.mouseScroll(deltaY < 0 ? 1 : -1, position.x(), position.y()); + return true; + } + + return super.mouseScrolled(mouseX, mouseY, deltaX, deltaY); + } + + /** + * Get the position of the mouse on the pocket terminal computer. + * + * @return The cursor position, or {@code null} if the mouse is out-of-bounds. + */ + private @Nullable Vector2ic getMousePosition() { + var minecraft = Objects.requireNonNull(this.minecraft); + if (minecraft.level == null || minecraft.player == null || minecraft.hitResult == null) return null; + + var lecternPos = getMenu().lectern(); + var terminal = getMenu().getTerminal(); + + // First ensure we're looking at the lectern block. + if (minecraft.hitResult.getType() != HitResult.Type.BLOCK || !((BlockHitResult) minecraft.hitResult).getBlockPos().equals(lecternPos)) { + return null; + } + + // Build the same pose stack that we use for rendering pocket computers. + var poseStack = new PoseStack(); + CustomLecternRenderer.applyLecternTransform(poseStack, minecraft.level.getBlockState(lecternPos)); + CustomLecternRenderer.applyPocketComputerTerminalTransform(poseStack); + CustomLecternRenderer.applyScaledPocketComputerTerminalTransform(poseStack, terminal); + + // Then take the inverse matrix, and use it to map the player's position and look vector to terminal space. + var inverseTransform = poseStack.last().pose().invert(new Matrix4f()); + var startPosition = minecraft.player.getEyePosition(); + var transformedStartPosition = inverseTransform.transformPosition( + (float) (startPosition.x() - lecternPos.getX()), + (float) (startPosition.y() - lecternPos.getY()), + (float) (startPosition.z() - lecternPos.getZ()), + new Vector3f() + ); + var transformedLookVector = inverseTransform.transformDirection(minecraft.player.getLookAngle().toVector3f()); + + // Compute the intersection of our plane with the look vector. This is trivial, as the terminal is at + // (0, 0, 0), with a normal of (0, 0, 1). + if (transformedLookVector.z() == 0) return null; + var intersection = transformedStartPosition.add( + transformedLookVector.mul(-transformedStartPosition.z() / transformedLookVector.z()) + ); + + // Then map back to actual terminal coordinates, and check we're still in bounds. + var positionX = (int) (intersection.x() / FONT_WIDTH); + var positionY = (int) (intersection.y() / FONT_HEIGHT); + return positionX >= 0 && positionX < terminal.getWidth() && positionY >= 0 && positionY < terminal.getHeight() + ? new Vector2i(positionX, positionY) + : null; + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java index a31f3b863..1dbb604d3 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java @@ -43,7 +43,10 @@ public class TurtleScreen extends AbstractComputerScreen { @Override protected TerminalWidget createTerminal() { - return new TerminalWidget(terminalData, input, leftPos + BORDER + AbstractComputerMenu.SIDEBAR_WIDTH, topPos + BORDER); + return new TerminalWidget( + terminalData, computerInput, computerActions, + leftPos + BORDER + AbstractComputerMenu.SIDEBAR_WIDTH, topPos + BORDER + ); } @Override diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java index 863159256..f5a908043 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java @@ -4,9 +4,9 @@ package dan200.computercraft.client.gui.widgets; +import dan200.computercraft.client.gui.ClientComputerActions; import dan200.computercraft.client.gui.GuiSprites; import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage; -import dan200.computercraft.shared.computer.core.InputHandler; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.network.chat.Component; @@ -29,7 +29,7 @@ public final class ComputerSidebar { private ComputerSidebar() { } - public static void addButtons(BooleanSupplier isOn, InputHandler input, Consumer add, int x, int y) { + public static void addButtons(BooleanSupplier isOn, ClientComputerActions actions, Consumer add, int x, int y) { x += CORNERS_BORDER + 1; y += CORNERS_BORDER + ICON_MARGIN; @@ -41,7 +41,7 @@ public final class ComputerSidebar { add.accept(new DynamicImageButton( x, y, ICON_WIDTH, ICON_HEIGHT, h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h), - b -> toggleComputer(isOn, input), + b -> toggleComputer(isOn, actions), () -> isOn.getAsBoolean() ? turnOff : turnOn )); @@ -50,7 +50,7 @@ public final class ComputerSidebar { add.accept(new DynamicImageButton( x, y, ICON_WIDTH, ICON_HEIGHT, GuiSprites.TERMINATE::get, - b -> input.terminate(), + b -> actions.terminate(), new HintedMessage( Component.translatable("gui.computercraft.tooltip.terminate"), Component.translatable("gui.computercraft.tooltip.terminate.key") @@ -58,11 +58,11 @@ public final class ComputerSidebar { )); } - private static void toggleComputer(BooleanSupplier isOn, InputHandler input) { + private static void toggleComputer(BooleanSupplier isOn, ClientComputerActions actions) { if (isOn.getAsBoolean()) { - input.shutdown(); + actions.shutdown(); } else { - input.turnOn(); + actions.turnOn(); } } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java index 8b856bd3c..0e3920875 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java @@ -8,11 +8,12 @@ import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.vertex.VertexConsumer; +import dan200.computercraft.client.gui.ClientComputerActions; +import dan200.computercraft.client.gui.ClientComputerInput; import dan200.computercraft.client.gui.KeyConverter; import dan200.computercraft.client.render.text.FixedWidthFontRenderer; +import dan200.computercraft.core.input.UserComputerInput; import dan200.computercraft.core.terminal.Terminal; -import dan200.computercraft.core.util.StringUtil; -import dan200.computercraft.shared.computer.core.InputHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; @@ -32,8 +33,6 @@ import org.joml.Matrix4f; import org.jspecify.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import java.util.BitSet; - import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN; import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT; import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH; @@ -42,7 +41,7 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON * A widget which renders a computer terminal and handles input events (keyboard, mouse, clipboard) and computer * shortcuts (terminate/shutdown/reboot). * - * @see dan200.computercraft.client.gui.ClientInputHandler The input handler typically used with this class. + * @see ClientComputerInput The input handler typically used with this class. */ public class TerminalWidget extends AbstractWidget { private static final Component DESCRIPTION = Component.translatable("gui.computercraft.terminal"); @@ -51,7 +50,8 @@ public class TerminalWidget extends AbstractWidget { private static final float KEY_SUPPRESS_DELAY = 0.2f; private final Terminal terminal; - private final InputHandler computer; + private final UserComputerInput computerInput; + private final ClientComputerActions computerActions; // The positions of the actual terminal private final int innerX; @@ -63,17 +63,12 @@ public class TerminalWidget extends AbstractWidget { private float rebootTimer = -1; private float shutdownTimer = -1; - private int lastMouseButton = -1; - private int lastMouseX = -1; - private int lastMouseY = -1; - - private final BitSet keysDown = new BitSet(256); - - public TerminalWidget(Terminal terminal, InputHandler computer, int x, int y) { + public TerminalWidget(Terminal terminal, UserComputerInput computerInput, ClientComputerActions computerActions, int x, int y) { super(x, y, terminal.getWidth() * FONT_WIDTH + MARGIN * 2, terminal.getHeight() * FONT_HEIGHT + MARGIN * 2, DESCRIPTION); this.terminal = terminal; - this.computer = computer; + this.computerInput = computerInput; + this.computerActions = computerActions; innerX = x + MARGIN; innerY = y + MARGIN; @@ -83,8 +78,7 @@ public class TerminalWidget extends AbstractWidget { @Override public boolean charTyped(CharacterEvent event) { - var terminalChar = StringUtil.unicodeToTerminal(event.codepoint()); - if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar); + computerInput.codepointTyped(event.codepoint()); return true; } @@ -111,27 +105,19 @@ public class TerminalWidget extends AbstractWidget { } if (event.key() >= 0 && terminateTimer < KEY_SUPPRESS_DELAY && rebootTimer < KEY_SUPPRESS_DELAY && shutdownTimer < KEY_SUPPRESS_DELAY) { - // Queue the "key" event and add to the down set - var repeat = keysDown.get(event.key()); - keysDown.set(event.key()); - computer.keyDown(event.key(), repeat); + computerInput.keyDown(event.key()); } return true; } private void paste() { - var clipboard = StringUtil.getClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard()); - if (clipboard.remaining() > 0) computer.paste(clipboard); + computerInput.paste(Minecraft.getInstance().keyboardHandler.getClipboard()); } @Override public boolean keyReleased(KeyEvent event) { - // Queue the "key_up" event and remove from the down set - if (event.key() >= 0 && keysDown.get(event.key())) { - keysDown.set(event.key(), false); - computer.keyUp(event.key()); - } + computerInput.keyUp(event.key()); switch (KeyConverter.physicalToActual(event.key(), event.scancode())) { case GLFW.GLFW_KEY_T -> terminateTimer = -1; @@ -147,18 +133,10 @@ public class TerminalWidget extends AbstractWidget { @Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { if (!inTermRegion(event.x(), event.y())) return false; - if (!hasMouseSupport() || event.button() < 0 || event.button() > 2) return false; var charX = (int) ((event.x() - innerX) / FONT_WIDTH); var charY = (int) ((event.y() - innerY) / FONT_HEIGHT); - charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1); - charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1); - - computer.mouseClick(event.button() + 1, charX + 1, charY + 1); - - lastMouseButton = event.button(); - lastMouseX = charX; - lastMouseY = charY; + computerInput.mouseClick(event.button() + 1, charX + 1, charY + 1); return true; } @@ -166,20 +144,10 @@ public class TerminalWidget extends AbstractWidget { @Override public boolean mouseReleased(MouseButtonEvent event) { if (!inTermRegion(event.x(), event.y())) return false; - if (!hasMouseSupport() || event.button() < 0 || event.button() > 2) return false; var charX = (int) ((event.x() - innerX) / FONT_WIDTH); var charY = (int) ((event.y() - innerY) / FONT_HEIGHT); - charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1); - charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1); - - if (lastMouseButton == event.button()) { - computer.mouseUp(lastMouseButton + 1, charX + 1, charY + 1); - lastMouseButton = -1; - } - - lastMouseX = charX; - lastMouseY = charY; + computerInput.mouseUp(event.button() + 1, charX + 1, charY + 1); return true; } @@ -187,36 +155,21 @@ public class TerminalWidget extends AbstractWidget { @Override public boolean mouseDragged(MouseButtonEvent event, double v2, double v3) { if (!inTermRegion(event.x(), event.y())) return false; - if (!hasMouseSupport() || event.button() < 0 || event.button() > 2) return false; var charX = (int) ((event.x() - innerX) / FONT_WIDTH); var charY = (int) ((event.y() - innerY) / FONT_HEIGHT); - charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1); - charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1); - - if (event.button() == lastMouseButton && (charX != lastMouseX || charY != lastMouseY)) { - computer.mouseDrag(event.button() + 1, charX + 1, charY + 1); - lastMouseX = charX; - lastMouseY = charY; - } - + computerInput.mouseDrag(event.button() + 1, charX + 1, charY + 1); return true; } @Override public boolean mouseScrolled(double mouseX, double mouseY, double deltaX, double deltaY) { if (!inTermRegion(mouseX, mouseY)) return false; - if (!hasMouseSupport() || deltaY == 0) return false; + if (deltaY == 0) return false; var charX = (int) ((mouseX - innerX) / FONT_WIDTH); var charY = (int) ((mouseY - innerY) / FONT_HEIGHT); - charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1); - charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1); - - computer.mouseScroll(deltaY < 0 ? 1 : -1, charX + 1, charY + 1); - - lastMouseX = charX; - lastMouseY = charY; + computerInput.mouseScroll(deltaY < 0 ? 1 : -1, charX + 1, charY + 1); return true; } @@ -225,21 +178,17 @@ public class TerminalWidget extends AbstractWidget { return active && visible && mouseX >= innerX && mouseY >= innerY && mouseX < innerX + innerWidth && mouseY < innerY + innerHeight; } - private boolean hasMouseSupport() { - return terminal.isColour(); - } - public void update() { if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) { - computer.terminate(); + computerActions.terminate(); } if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) { - computer.shutdown(); + computerActions.shutdown(); } if (rebootTimer >= 0 && rebootTimer < TERMINATE_TIME && (rebootTimer += 0.05f) > TERMINATE_TIME) { - computer.reboot(); + computerActions.reboot(); } } @@ -248,18 +197,7 @@ public class TerminalWidget extends AbstractWidget { super.setFocused(focused); if (!focused) { - // When blurring, we should make all keys go up - for (var key = 0; key < keysDown.size(); key++) { - if (keysDown.get(key)) computer.keyUp(key); - } - keysDown.clear(); - - // When blurring, we should make the last mouse button go up - if (lastMouseButton >= 0) { - computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1); - lastMouseButton = -1; - } - + computerInput.releaseInputs(); shutdownTimer = terminateTimer = rebootTimer = -1; } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java index 47a81962e..b58b8d872 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java @@ -33,7 +33,9 @@ import net.minecraft.util.ARGB; import net.minecraft.util.Unit; import net.minecraft.world.item.component.DyedItemColor; import net.minecraft.world.level.block.LecternBlock; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; +import org.joml.Vector2f; import org.jspecify.annotations.Nullable; import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN; @@ -65,6 +67,19 @@ public class CustomLecternRenderer implements BlockEntityRenderer - FixedWidthFontRenderer.drawTerminal(pose.pose(), buffer, marginX, marginY, terminal, marginY, marginY, marginX, marginX)); + FixedWidthFontRenderer.drawTerminal(pose.pose(), buffer, 0, 0, terminal, margin.y(), margin.y(), margin.x(), margin.x())); } private enum Type { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index 9dae386d6..3d49e725b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -46,6 +46,7 @@ import dan200.computercraft.shared.details.ItemDetails; import dan200.computercraft.shared.integration.PermissionRegistry; import dan200.computercraft.shared.lectern.CustomLecternBlock; import dan200.computercraft.shared.lectern.CustomLecternBlockEntity; +import dan200.computercraft.shared.lectern.PocketComputerLecternMenu; import dan200.computercraft.shared.media.MountMedia; import dan200.computercraft.shared.media.PrintoutMenu; import dan200.computercraft.shared.media.items.*; @@ -500,6 +501,9 @@ public final class ModRegistry { public static final RegistryEntry> POCKET_COMPUTER_NO_TERM = REGISTRY.register("pocket_computer_no_term", () -> ContainerData.toType(ComputerContainerData.STREAM_CODEC, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.POCKET_COMPUTER_NO_TERM.get(), id, inv, data))); + public static final RegistryEntry> POCKET_COMPUTER_LECTERN = REGISTRY.register("pocket_computer_lectern", + () -> ContainerData.toType(PocketComputerLecternMenu.Data.STREAM_CODEC, PocketComputerLecternMenu::new)); + public static final RegistryEntry> TURTLE = REGISTRY.register("turtle", () -> ContainerData.toType(ComputerContainerData.STREAM_CODEC, TurtleMenu::ofMenuData)); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java index b83795b56..aca52c0fc 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -255,9 +255,8 @@ public class CommandAPI implements ILuaAPI { /** * Get some basic information about a block. *

- * The returned table contains the current name, metadata and block state (as - * with [`turtle.inspect`]). If there is a block entity for that block, its NBT - * will also be returned. + * The returned table contains the the same information as listed in [`block_details`]. If there is a block entity + * for that block, its NBT will also be returned. * * @param x The x position of the block to query. * @param y The y position of the block to query. diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java index 6a44cfb5b..3e8c2b91c 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlock.java @@ -4,6 +4,7 @@ package dan200.computercraft.shared.computer.blocks; +import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.shared.common.IBundledRedstoneBlock; import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.platform.PlatformHelper; @@ -128,6 +129,12 @@ public abstract class AbstractComputerBlock + * This is only required for MoreRed, which does not fire block updates when bundled redstone changes, see + * #2316 + * + * @param neighbour The position of the neighbour block. + */ + public void neighborBlockEntityChanged(BlockPos neighbour) { + var computer = getServerComputer(); + if (computer == null) return; + + for (var dir : DirectionUtil.FACINGS) { + var offset = getBlockPos().relative(dir); + if (offset.equals(neighbour)) { + updateRedstoneInput(computer, dir, offset); + return; + } + } + } + /** * Called when a neighbour block's shape changes. *

diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java deleted file mode 100644 index dbdd23ce5..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/InputHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.computer.core; - -import dan200.computercraft.shared.computer.menu.ServerInputHandler; - -import java.nio.ByteBuffer; - -/** - * Handles user-provided input, forwarding it to a computer. This describes the "shape" of both the client-and - * server-side input handlers. - * - * @see ServerInputHandler - * @see ServerComputer - */ -public interface InputHandler { - void keyDown(int key, boolean repeat); - - void keyUp(int key); - - void charTyped(byte chr); - - void paste(ByteBuffer contents); - - void mouseClick(int button, int x, int y); - - void mouseUp(int button, int x, int y); - - void mouseDrag(int button, int x, int y); - - void mouseScroll(int direction, int x, int y); - - void terminate(); - - void shutdown(); - - void turnOn(); - - void reboot(); -} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 7ecc07ce7..8c8598c7f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -13,8 +13,9 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.ComputerEnvironment; -import dan200.computercraft.core.computer.ComputerEvents; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.input.EventComputerInput; +import dan200.computercraft.core.input.UserComputerInput; import dan200.computercraft.core.metrics.MetricsObserver; import dan200.computercraft.impl.ApiFactories; import dan200.computercraft.shared.computer.menu.ComputerMenu; @@ -38,7 +39,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver { +public class ServerComputer implements ComputerEnvironment { public static final ComputerComponent METRICS = ComputerComponent.create("computercraft", "metrics"); private final UUID instanceUUID = UUID.randomUUID(); @@ -212,7 +213,6 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei computer.reboot(); } - @Override public final void queueEvent(String event, @Nullable Object @Nullable [] arguments) { computer.queueEvent(event, arguments); } @@ -221,6 +221,10 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei queueEvent(event, null); } + public final UserComputerInput createComputerInput() { + return new UserComputerInput(new EventComputerInput(computer), terminal); + } + public final int getRedstoneOutput(ComputerSide side) { return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/AbstractComputerMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/AbstractComputerMenu.java index 0567e67e1..aef6e7ed0 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/AbstractComputerMenu.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/inventory/AbstractComputerMenu.java @@ -34,7 +34,7 @@ public abstract class AbstractComputerMenu extends AbstractContainerMenu impleme private final ContainerData data; private final @Nullable ServerComputer computer; - private final @Nullable ServerInputState input; + private final @Nullable ServerInputState input; private final @Nullable NetworkedTerminal terminal; @@ -51,7 +51,7 @@ public abstract class AbstractComputerMenu extends AbstractContainerMenu impleme addDataSlots(data); this.computer = computer; - input = computer == null ? null : new ServerInputState<>(this); + input = computer == null ? null : new ServerInputState(this, computer); terminal = containerData == null ? null : containerData.terminal().create(); displayStack = containerData == null ? ItemStack.EMPTY : containerData.displayStack(); uploadMaxSize = containerData == null ? Config.uploadMaxSize : containerData.uploadMaxSize(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java index 881ec3e8a..7fe405bcf 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java @@ -4,7 +4,7 @@ package dan200.computercraft.shared.computer.menu; -import dan200.computercraft.shared.computer.core.InputHandler; +import dan200.computercraft.core.input.ComputerInput; import dan200.computercraft.shared.computer.upload.FileSlice; import dan200.computercraft.shared.computer.upload.FileUpload; import dan200.computercraft.shared.network.server.ComputerServerMessage; @@ -14,13 +14,20 @@ import java.util.List; import java.util.UUID; /** - * An {@link InputHandler} which operates on the server, receiving data from the client over the network. + * An {@link ComputerInput} which operates on the server, receiving data from the client over the network. * * @see ServerInputState The default implementation of this interface. * @see ComputerServerMessage Packets which consume this interface. * @see ComputerMenu */ -public interface ServerInputHandler extends InputHandler { +public interface ServerInputHandler { + /** + * Get a {@link ComputerInput} that handles events for this computer. + * + * @return The computer input. + */ + ComputerInput getComputerInput(); + /** * Start a file upload into this container. * diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java index 3079f5be8..6ea675edb 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java @@ -7,15 +7,14 @@ package dan200.computercraft.shared.computer.menu; import dan200.computercraft.core.apis.handles.ByteBufferChannel; import dan200.computercraft.core.apis.transfer.TransferredFile; import dan200.computercraft.core.apis.transfer.TransferredFiles; -import dan200.computercraft.core.computer.ComputerEvents; -import dan200.computercraft.core.util.StringUtil; +import dan200.computercraft.core.input.ComputerInput; +import dan200.computercraft.core.input.UserComputerInput; +import dan200.computercraft.shared.computer.core.ServerComputer; 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.network.server.ServerNetworking; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.inventory.AbstractContainerMenu; @@ -23,7 +22,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.ByteBuffer; import java.util.List; import java.util.UUID; @@ -31,113 +29,31 @@ import java.util.UUID; * The default concrete implementation of {@link ServerInputHandler}. *

* This keeps track of the current key and mouse state, and releases them when the container is closed. - * - * @param The type of container this server input belongs to. */ -public class ServerInputState implements ServerInputHandler { +public class ServerInputState implements ServerInputHandler { private static final Logger LOG = LoggerFactory.getLogger(ServerInputState.class); - private final T owner; - private final IntSet keysDown = new IntOpenHashSet(4); - - private int lastMouseX; - private int lastMouseY; - private int lastMouseDown = -1; + private final AbstractContainerMenu owner; + private final ServerComputer computer; + private final UserComputerInput input; private @Nullable UUID toUploadId; private @Nullable List toUpload; - public ServerInputState(T owner) { + public ServerInputState(AbstractContainerMenu owner, ServerComputer computer) { this.owner = owner; + this.computer = computer; + this.input = computer.createComputerInput(); } @Override - public void keyDown(int key, boolean repeat) { - keysDown.add(key); - ComputerEvents.keyDown(owner.getComputer(), key, repeat); + public ComputerInput getComputerInput() { + return input; } @Override - public void keyUp(int key) { - keysDown.remove(key); - ComputerEvents.keyUp(owner.getComputer(), key); - } - - @Override - public void charTyped(byte chr) { - if (StringUtil.isTypableChar(chr)) ComputerEvents.charTyped(owner.getComputer(), chr); - } - - @Override - public void paste(ByteBuffer contents) { - if (contents.remaining() > 0 && isValidClipboard(contents)) ComputerEvents.paste(owner.getComputer(), contents); - } - - private static boolean isValidClipboard(ByteBuffer buffer) { - for (int i = buffer.position(), max = buffer.limit(); i < max; i++) { - if (!StringUtil.isTypableChar(buffer.get(i))) return false; - } - return true; - } - - @Override - public void mouseClick(int button, int x, int y) { - lastMouseX = x; - lastMouseY = y; - lastMouseDown = button; - - ComputerEvents.mouseClick(owner.getComputer(), button, x, y); - } - - @Override - public void mouseUp(int button, int x, int y) { - lastMouseX = x; - lastMouseY = y; - lastMouseDown = -1; - - ComputerEvents.mouseUp(owner.getComputer(), button, x, y); - } - - @Override - public void mouseDrag(int button, int x, int y) { - lastMouseX = x; - lastMouseY = y; - lastMouseDown = button; - - ComputerEvents.mouseDrag(owner.getComputer(), button, x, y); - } - - @Override - public void mouseScroll(int direction, int x, int y) { - lastMouseX = x; - lastMouseY = y; - - ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y); - } - - @Override - public void terminate() { - owner.getComputer().queueEvent("terminate"); - } - - @Override - public void shutdown() { - owner.getComputer().shutdown(); - } - - @Override - public void turnOn() { - owner.getComputer().turnOn(); - } - - @Override - public void reboot() { - owner.getComputer().reboot(); - } - - @Override - public void startUpload(UUID uuid, List files) { - toUploadId = uuid; + public void startUpload(UUID uploadId, List files) { + toUploadId = uploadId; toUpload = files; } @@ -162,7 +78,6 @@ public class ServerInputState im } private UploadResultMessage finishUpload(ServerPlayer player) { - var computer = owner.getComputer(); if (toUpload == null) { return UploadResultMessage.error(owner, UploadResult.COMPUTER_OFF_MSG); } @@ -187,13 +102,6 @@ public class ServerInputState im } public void close() { - var computer = owner.getComputer(); - var keys = keysDown.iterator(); - while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt()); - - if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY); - - keysDown.clear(); - lastMouseDown = -1; + input.releaseInputs(); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/details/BlockDetails.java b/projects/common/src/main/java/dan200/computercraft/shared/details/BlockDetails.java index 0940234c7..6b6aca478 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/details/BlockDetails.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/details/BlockDetails.java @@ -27,6 +27,7 @@ public class BlockDetails { public static void fill(Map data, BlockReference block) { data.put("tags", DetailHelpers.getTags(block.state().getTags())); + DetailHelpers.fillMapColour(data, block.level(), block.pos(), block.state()); } @SuppressWarnings({ "unchecked", "rawtypes" }) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/details/DetailHelpers.java b/projects/common/src/main/java/dan200/computercraft/shared/details/DetailHelpers.java index f2ba58e16..156d5f9c5 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/details/DetailHelpers.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/details/DetailHelpers.java @@ -5,9 +5,14 @@ package dan200.computercraft.shared.details; import dan200.computercraft.shared.util.RegistryHelper; +import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.tags.TagKey; +import net.minecraft.util.ARGB; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.MapColor; import java.util.Map; import java.util.stream.Collectors; @@ -28,4 +33,13 @@ public final class DetailHelpers { public static String getId(Registry registry, T entry) { return RegistryHelper.getKeyOrThrow(registry, entry).toString(); } + + public static void fillMapColour(Map data, BlockGetter level, BlockPos pos, BlockState state) { + var mapColour = state.getMapColor(level, pos); + if (mapColour == MapColor.NONE) return; + + var colour = ARGB.transparent(mapColour.col); + data.put("mapColor", colour); + data.put("mapColour", colour); + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java b/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java index d9324466c..443031ad0 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/details/ItemDetails.java @@ -5,6 +5,7 @@ package dan200.computercraft.shared.details; import dan200.computercraft.shared.util.NBTUtil; +import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponents; @@ -12,18 +13,19 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.NbtOps; import net.minecraft.network.chat.Component; import net.minecraft.resources.RegistryOps; -import net.minecraft.world.item.CreativeModeTab; -import net.minecraft.world.item.CreativeModeTabs; -import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.*; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.item.enchantment.ItemEnchantments; +import net.minecraft.world.level.EmptyBlockGetter; +import net.minecraft.world.level.block.Block; import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.StreamSupport; /** * Data providers for items. @@ -69,8 +71,13 @@ public class ItemDetails { var enchants = getAllEnchants(stack); if (!enchants.isEmpty()) data.put("enchantments", enchants); + var effects = getAllEffects(stack); + if (!effects.isEmpty()) data.put("potionEffects", effects); + var unbreakable = stack.get(DataComponents.UNBREAKABLE); if (unbreakable != null) data.put("unbreakable", true); + + DetailHelpers.fillMapColour(data, EmptyBlockGetter.INSTANCE, BlockPos.ZERO, Block.byItem(stack.getItem()).defaultBlockState()); } /** @@ -130,4 +137,51 @@ public class ItemDetails { enchants.add(enchant); } } + + /** + * Retrieve all potions from given stack. + * + * @param stack Stack to analyse. + * @return A filled list that contain all visible potions. + */ + private static List> getAllEffects(ItemStack stack) { + var effects = stack.get(DataComponents.POTION_CONTENTS); + if (effects == null) return List.of(); + + return StreamSupport.stream(effects.getAllEffects().spliterator(), false).map(p -> { + Map potion = new HashMap<>(4); + potion.put("name", p.getEffect().getRegisteredName()); + + var displayName = Component.translatable(p.getDescriptionId()); + if (p.getAmplifier() > 0) { + displayName = Component.translatable( + "potion.withAmplifier", displayName, Component.translatable("potion.potency." + p.getAmplifier()) + ); + } + potion.put("displayName", displayName.getString()); + + // Expose the roman numerals (e.g. Instant Health II), rather than the raw amplifier value. + if (p.getAmplifier() > 0) potion.put("potency", p.getAmplifier() + 1); + + if (p.isInfiniteDuration()) { + potion.put("duration", Double.POSITIVE_INFINITY); + } else if (p.getDuration() > 1) { + potion.put("duration", p.getDuration() / 20.0 * getPotionDurationMultiplier(stack)); + } + return potion; + }).toList(); + } + + /** + * Get the potion duration multiplier for an item, to handle items which have a shorter duration than a normal + * potion. + * + * @param stack The current stack. + * @return The duration multiplier. + */ + private static double getPotionDurationMultiplier(ItemStack stack) { + if (stack.getItem() instanceof LingeringPotionItem) return 0.25; + if (stack.getItem() instanceof TippedArrowItem) return 0.125; + return 1.0; + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java index 71469c7d5..77ce05829 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java @@ -4,8 +4,10 @@ package dan200.computercraft.shared.lectern; +import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.media.items.PrintoutItem; +import dan200.computercraft.shared.pocket.core.PocketHolder; import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.util.BlockEntityHelpers; import net.minecraft.core.BlockPos; @@ -21,12 +23,14 @@ import net.minecraft.world.item.Items; import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.LecternBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.redstone.Orientation; import net.minecraft.world.phys.BlockHitResult; import org.jspecify.annotations.Nullable; @@ -102,6 +106,14 @@ public class CustomLecternBlock extends LecternBlock { return new ItemStack(Items.LECTERN); } + @Override + protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { + if (!level.isClientSide() && level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern && + lectern.getItem().getItem() instanceof PocketComputerItem) { + lectern.markRefreshPeripheral(); + } + } + @Override public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { // If we've no lectern, remove it. @@ -122,6 +134,12 @@ public class CustomLecternBlock extends LecternBlock { var entity = new ItemEntity(level, pos.getX() + 0.5 + dx, pos.getY() + 1, pos.getZ() + 0.5 + dz, stack); entity.setDefaultPickUpDelay(); level.addFreshEntity(entity); + + // If we're a pocket computer, update the holder and clear the peripheral. + if (stack.getItem() instanceof PocketComputerItem pocket) { + var brain = pocket.getBrain(new PocketHolder.ItemEntityHolder(entity), stack); + if (brain != null) brain.computer().setPeripheral(ComputerSide.BOTTOM, null); + } } @Override @@ -130,6 +148,7 @@ public class CustomLecternBlock extends LecternBlock { } @Override + @Deprecated protected int getAnalogOutputSignal(BlockState blockState, Level level, BlockPos pos, Direction direction) { return level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern ? lectern.getRedstoneSignal() : 0; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java index a8b836a0d..003a3e781 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java @@ -4,16 +4,23 @@ package dan200.computercraft.shared.lectern; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.container.BasicContainer; import dan200.computercraft.shared.container.SingleContainerData; import dan200.computercraft.shared.media.PrintoutMenu; import dan200.computercraft.shared.media.items.PrintoutData; import dan200.computercraft.shared.media.items.PrintoutItem; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.platform.ComponentAccess; +import dan200.computercraft.shared.platform.PlatformHelper; +import dan200.computercraft.shared.pocket.core.PocketBrain; import dan200.computercraft.shared.pocket.core.PocketHolder; import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.util.BlockEntityHelpers; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.Packet; @@ -33,6 +40,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.TagValueOutput; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,10 +63,21 @@ public final class CustomLecternBlockEntity extends BlockEntity { private ItemStack item = ItemStack.EMPTY; private int page, pageCount; + private final PocketHolder.LecternHolder pocketHolder = new PocketHolder.LecternHolder(this); + private @Nullable PocketBrain activePocketBrain; + private final ComponentAccess peripherals = PlatformHelper.get().createPeripheralAccess(this, d -> markRefreshPeripheral()); + private boolean refreshPeripheral; + public CustomLecternBlockEntity(BlockPos pos, BlockState blockState) { super(ModRegistry.BlockEntities.LECTERN.get(), pos, blockState); } + @Override + public void clearRemoved() { + refreshPeripheral = true; + super.clearRemoved(); + } + public ItemStack getItem() { return item; } @@ -88,11 +107,27 @@ public final class CustomLecternBlockEntity extends BlockEntity { } else { pageCount = page = 0; } + + activePocketBrain = null; + } + + void markRefreshPeripheral() { + refreshPeripheral = true; } void tick() { if (item.getItem() instanceof PocketComputerItem pocket) { - pocket.tick(item, new PocketHolder.LecternHolder(this), false); + // Get our pocket computer, and tick it. + var brain = pocket.getOrCreateBrain(pocketHolder, item); + brain.computer().keepAlive(); + pocket.tick(item, pocketHolder, brain); + + // Update the peripheral if the peripheral or brain has changed. + if (refreshPeripheral || brain != activePocketBrain) { + refreshPeripheral = false; + activePocketBrain = brain; + brain.computer().setPeripheral(ComputerSide.BOTTOM, peripherals.get(Direction.DOWN)); + } } } @@ -154,7 +189,13 @@ public final class CustomLecternBlockEntity extends BlockEntity { new PrintoutContainerData() ), getItem().getDisplayName())); } else if (item.getItem() instanceof PocketComputerItem pocket) { - pocket.open(player, item, new PocketHolder.LecternHolder(this), true); + var computer = pocket.getOrCreateBrain(pocketHolder, item).computer(); + computer.turnOn(); + PlatformHelper.get().openMenu( + player, item.getHoverName(), + (id, inv, entity) -> new PocketComputerLecternMenu(id, inv, pocketHolder, computer), + new PocketComputerLecternMenu.Data(new ComputerContainerData(computer, item), getBlockPos()) + ); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/PocketComputerLecternMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/PocketComputerLecternMenu.java new file mode 100644 index 000000000..8da9d042b --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/PocketComputerLecternMenu.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.lectern; + +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.network.container.ContainerData; +import dan200.computercraft.shared.pocket.core.PocketHolder; +import dan200.computercraft.shared.pocket.items.PocketComputerItem; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.entity.player.Inventory; + +/** + * The menu for opening a {@linkplain PocketComputerItem pocket computer} on a + * {@linkplain CustomLecternBlockEntity lectern}. + *

+ * This contains the lectern's position, so that the client is able to map the look/hit vector back to a position on the + * computer's terminal. + */ +public final class PocketComputerLecternMenu extends ComputerMenuWithoutInventory { + private final BlockPos lectern; + + public PocketComputerLecternMenu(int id, Inventory player, PocketHolder.LecternHolder holder, ServerComputer computer) { + super(ModRegistry.Menus.POCKET_COMPUTER_LECTERN.get(), id, player, p -> holder.isValid(computer), computer); + this.lectern = holder.blockPos(); + } + + public PocketComputerLecternMenu(int id, Inventory player, Data menuData) { + super(ModRegistry.Menus.POCKET_COMPUTER_LECTERN.get(), id, player, menuData.computer()); + this.lectern = menuData.lectern(); + } + + public BlockPos lectern() { + return lectern; + } + + public record Data(ComputerContainerData computer, BlockPos lectern) implements ContainerData { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ComputerContainerData.STREAM_CODEC, Data::computer, + BlockPos.STREAM_CODEC, Data::lectern, + Data::new + ); + + @Override + public void toBytes(RegistryFriendlyByteBuf buf) { + STREAM_CODEC.encode(buf, this); + } + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java index 688b872b6..c491c2236 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java @@ -37,10 +37,10 @@ public final class ComputerActionServerMessage extends ComputerServerMessage { @Override protected void handle(ServerNetworkContext context, ComputerMenu container) { switch (action) { - case TERMINATE -> container.getInput().terminate(); - case TURN_ON -> container.getInput().turnOn(); - case REBOOT -> container.getInput().reboot(); - case SHUTDOWN -> container.getInput().shutdown(); + case TERMINATE -> container.getComputer().queueEvent("terminate"); + case TURN_ON -> container.getComputer().turnOn(); + case REBOOT -> container.getComputer().reboot(); + case SHUTDOWN -> container.getComputer().shutdown(); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java index e526b8323..553e8cc02 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java @@ -39,7 +39,7 @@ public final class KeyEventServerMessage extends ComputerServerMessage { @Override protected void handle(ServerNetworkContext context, ComputerMenu container) { - var input = container.getInput(); + var input = container.getInput().getComputerInput(); switch (action) { case UP -> input.keyUp(key); case DOWN -> input.keyDown(key, false); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java index b958c3c7e..c2d72e71b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java @@ -45,7 +45,7 @@ public final class MouseEventServerMessage extends ComputerServerMessage { @Override protected void handle(ServerNetworkContext context, ComputerMenu container) { - var input = container.getInput(); + var input = container.getInput().getComputerInput(); switch (action) { case CLICK -> input.mouseClick(arg, x, y); case DRAG -> input.mouseDrag(arg, x, y); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java index b33a95df2..274bf5757 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/server/PasteEventComputerMessage.java @@ -4,10 +4,10 @@ package dan200.computercraft.shared.network.server; +import dan200.computercraft.core.input.ComputerInput; import dan200.computercraft.core.util.StringUtil; import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.menu.ComputerMenu; -import dan200.computercraft.shared.computer.menu.ServerInputHandler; import dan200.computercraft.shared.network.NetworkMessages; import dan200.computercraft.shared.network.codec.MoreStreamCodecs; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -21,7 +21,7 @@ import java.nio.ByteBuffer; /** * Paste a string on a {@link ServerComputer}. * - * @see ServerInputHandler#paste(ByteBuffer) + * @see ComputerInput#paste(ByteBuffer) */ public class PasteEventComputerMessage extends ComputerServerMessage { public static final StreamCodec STREAM_CODEC = StreamCodec.composite( @@ -43,7 +43,7 @@ public class PasteEventComputerMessage extends ComputerServerMessage { @Override protected void handle(ServerNetworkContext context, ComputerMenu container) { - container.getInput().paste(text); + container.getInput().getComputerInput().paste(text); } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java index 3ae454a09..fc01f2f5c 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java @@ -5,7 +5,6 @@ package dan200.computercraft.shared.peripheral.generic.methods; import dan200.computercraft.api.ComputerCraftAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.GenericPeripheral; @@ -46,9 +45,8 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { /** * List all items in this inventory. This returns a table, with an entry for each slot. *

- * Each item in the inventory is represented by a table containing some basic information, much like - * {@link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail(ILuaContext, Optional, Optional)} - * includes. More information can be fetched with {@link #getItemDetail}. The table contains the item `name`, the + * Each item in the inventory is represented by a table containing [some basic information][`item_details`]. More + * information can be fetched with {@link #getItemDetail}. The table contains the item `name`, the * `count` and an a (potentially nil) hash of the item's `nbt.` This NBT data doesn't contain anything useful, but * allows you to distinguish identical items. *

@@ -66,23 +64,13 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * print(("%d x %s in slot %d"):format(item.count, item.name, slot)) * end * } + * @cc.see item_details */ @LuaFunction(mainThread = true) public abstract Map> list(T inventory); /** - * Get detailed information about an item. - *

- * The returned information contains the same information as each item in - * {@link #list}, as well as additional details like the display name - * (`displayName`), item groups (`itemGroups`), which are the creative tabs - * an item will appear under, and item and item durability (`damage`, - * `maxDamage`, `durability`). - *

- * Some items include more information (such as enchantments) - it is - * recommended to print it out using [`textutils.serialize`] or in the Lua - * REPL, to explore what is available. - *

+ * Get [detailed information][`item_details`] about an item. * * @param inventory The current inventory. * @param slot The slot to get information about. @@ -102,6 +90,7 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * print(("Damage: %d/%d"):format(item.damage, item.maxDamage)) * end * } + * @cc.see item_details */ @Nullable @LuaFunction(mainThread = true) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java index 3cb05926d..019fe54b2 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java @@ -113,7 +113,7 @@ public class CableBlockEntity extends BlockEntity { void queueRefreshPeripheral() { refreshPeripheral = true; - TickScheduler.schedule(tickToken); + getLevel().scheduleTick(getBlockPos(), getBlockState().getBlock(), 0); } InteractionResult use(Player player) { @@ -181,7 +181,7 @@ public class CableBlockEntity extends BlockEntity { void scheduleConnectionsChanged() { refreshConnections = true; - TickScheduler.schedule(tickToken); + getLevel().scheduleTick(getBlockPos(), getBlockState().getBlock(), 0); } void connectionsChanged() { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java index 043e2b87c..3f09eef3b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemFullBlockEntity.java @@ -115,7 +115,7 @@ public class WiredModemFullBlockEntity extends BlockEntity { void queueRefreshPeripheral(Direction facing) { invalidSides |= 1 << facing.ordinal(); - TickScheduler.schedule(tickToken); + getLevel().scheduleTick(getBlockPos(), getBlockState().getBlock(), 0); } public InteractionResult use(Player player) { @@ -190,7 +190,7 @@ public class WiredModemFullBlockEntity extends BlockEntity { private void scheduleConnectionsChanged() { refreshConnections = true; - TickScheduler.schedule(tickToken); + getLevel().scheduleTick(getBlockPos(), getBlockState().getBlock(), 0); } private void connectionsChanged() { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlock.java index 8799dd84c..5cbe9e377 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlock.java @@ -4,6 +4,7 @@ package dan200.computercraft.shared.peripheral.redstone; import com.mojang.serialization.MapCodec; +import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.shared.common.IBundledRedstoneBlock; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -12,6 +13,7 @@ import net.minecraft.util.RandomSource; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.HorizontalDirectionalBlock; @@ -80,6 +82,13 @@ public final class RedstoneRelayBlock extends HorizontalDirectionalBlock impleme return level.getBlockEntity(pos) instanceof RedstoneRelayBlockEntity relay ? relay.getBundledRedstoneOutput(side) : 0; } + @ForgeOverride + public void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) { + // This is only required for MoreRed, which does not fire block updates when bundled redstone changes, see + // #2316. + if (world.getBlockEntity(pos) instanceof RedstoneRelayBlockEntity relay) relay.neighborChanged(); + } + @Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbourBlock, @Nullable Orientation orientation, boolean isMoving) { if (level.getBlockEntity(pos) instanceof RedstoneRelayBlockEntity relay) relay.neighborChanged(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlockEntity.java index 21dad139d..08a8f190d 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/redstone/RedstoneRelayBlockEntity.java @@ -22,6 +22,7 @@ public final class RedstoneRelayBlockEntity extends BlockEntity { private final RedstoneState redstoneState = new RedstoneState(() -> TickScheduler.schedule(tickToken)); private final RedstoneRelayPeripheral peripheral = new RedstoneRelayPeripheral(redstoneState); + private boolean updateAll = false; public RedstoneRelayBlockEntity(BlockPos pos, BlockState blockState) { super(ModRegistry.BlockEntities.REDSTONE_RELAY.get(), pos, blockState); @@ -30,17 +31,18 @@ public final class RedstoneRelayBlockEntity extends BlockEntity { @Override public void clearRemoved() { super.clearRemoved(); + updateAll = true; TickScheduler.schedule(tickToken); } void update() { var changes = redstoneState.updateOutput(); - if (changes != 0) { - for (var direction : DirectionUtil.FACINGS) { - if ((changes & (1 << mapSide(direction).ordinal())) != 0) updateRedstoneTo(direction); - } + for (var direction : DirectionUtil.FACINGS) { + if (updateAll || (changes & (1 << mapSide(direction).ordinal())) != 0) updateRedstoneTo(direction); } + updateAll = false; + if (redstoneState.pollInputChanged()) peripheral.queueRedstoneEvent(); } @@ -61,7 +63,7 @@ public final class RedstoneRelayBlockEntity extends BlockEntity { // If the input has changed, and we're not currently in update(), then schedule a new tick so we can queue a // redstone event. - if (changed && !ticking) TickScheduler.schedule(tickToken); + if (changed && !ticking) getLevel().scheduleTick(getBlockPos(), getBlockState().getBlock(), 0); } private ComputerSide mapSide(Direction globalSide) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java index 6bde2e68e..2b3df3547 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java @@ -51,23 +51,14 @@ public class PocketComputerItem extends Item { /** * Tick a pocket computer. * - * @param stack The current pocket computer stack. - * @param holder The entity holding the pocket item. - * @param passive If set, the pocket computer will not be created if it doesn't exist, and will not be kept alive. + * @param stack The current pocket computer stack. + * @param holder The entity holding the pocket item. + * @param brain The pocket brain. */ - public void tick(ItemStack stack, PocketHolder holder, boolean passive) { - PocketBrain brain; - if (passive) { - var computer = getServerComputer(holder.level().getServer(), stack); - if (computer == null) return; - brain = computer.getBrain(); - } else { - brain = getOrCreateBrain(holder.level(), holder, stack); - brain.computer().keepAlive(); - } - + public void tick(ItemStack stack, PocketHolder holder, PocketBrain brain) { brain.tick(); + // Sync pocket state back to the item if (updateItem(stack, brain)) holder.setChanged(); } @@ -102,9 +93,17 @@ public class PocketComputerItem extends Item { public void inventoryTick(ItemStack stack, ServerLevel level, Entity entity, @Nullable EquipmentSlot slot) { if (entity instanceof ServerPlayer player) { var invSlot = InventoryUtil.findItemInInventory(player.getInventory(), stack); - if (invSlot != -1) tick(stack, new PocketHolder.PlayerHolder(player, invSlot), false); + if (invSlot < 0) return; + + // If we're in the inventory, create a computer and keep it alive. + var holder = new PocketHolder.PlayerHolder(player, invSlot); + var brain = getOrCreateBrain(holder, stack); + brain.computer().keepAlive(); + tick(stack, holder, brain); } else if (slot != null && entity instanceof LivingEntity living) { - tick(stack, new PocketHolder.LivingEntityHolder(living, slot), true); + var holder = new PocketHolder.LivingEntityHolder(living, slot); + var brain = getBrain(holder, stack); + if (brain != null) tick(stack, holder, brain); } } @@ -115,7 +114,9 @@ public class PocketComputerItem extends Item { // If we're an item entity, tick an already existing computer (as to update the position), but do not keep the // computer alive. - tick(stack, new PocketHolder.ItemEntityHolder(entity), true); + var holder = new PocketHolder.ItemEntityHolder(entity); + var brain = getBrain(holder, stack); + if (brain != null) tick(stack, holder, brain); return false; } @@ -125,32 +126,17 @@ public class PocketComputerItem extends Item { var stack = player.getItemInHand(hand); if (!world.isClientSide()) { var holder = new PocketHolder.PlayerHolder((ServerPlayer) player, InventoryUtil.getHandSlot(player, hand)); - var brain = getOrCreateBrain((ServerLevel) world, holder, stack); + var brain = getOrCreateBrain(holder, stack); var computer = brain.computer(); computer.turnOn(); var stop = brain.onRightClick((ServerLevel) world); - if (!stop) openImpl(player, stack, holder, hand == InteractionHand.OFF_HAND, computer); + if (!stop) openMenu(player, stack, holder, hand == InteractionHand.OFF_HAND, computer); } return InteractionResult.SUCCESS; } - /** - * Open a container for this pocket computer. - * - * @param player The player to show the menu for. - * @param stack The pocket computer stack. - * @param holder The holder of the pocket computer. - * @param isTypingOnly Open the off-hand pocket screen (only supporting typing, with no visible terminal). - */ - public void open(Player player, ItemStack stack, PocketHolder holder, boolean isTypingOnly) { - var brain = getOrCreateBrain(holder.level(), holder, stack); - var computer = brain.computer(); - computer.turnOn(); - openImpl(player, stack, holder, isTypingOnly, computer); - } - - private static void openImpl(Player player, ItemStack stack, PocketHolder holder, boolean isTypingOnly, ServerComputer computer) { + private static void openMenu(Player player, ItemStack stack, PocketHolder holder, boolean isTypingOnly, ServerComputer computer) { PlatformHelper.get().openMenu(player, stack.getHoverName(), (id, inventory, entity) -> new ComputerMenuWithoutInventory( isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(), id, inventory, p -> holder.isValid(computer), @@ -169,8 +155,16 @@ public class PocketComputerItem extends Item { return PocketUpgrades.instance().getOwner(getUpgradeWithData(stack, PocketSide.BACK), getUpgradeWithData(stack, PocketSide.BOTTOM)); } - private PocketBrain getOrCreateBrain(ServerLevel level, PocketHolder holder, ItemStack stack) { - var registry = ServerContext.get(level.getServer()).registry(); + /** + * Get (or create) the pocket brain and turn it on, ready for the player to interact with. + * + * @param stack The pocket computer stack. + * @param holder The holder of the pocket computer. + * @return The pocket brain. + */ + public PocketBrain getOrCreateBrain(PocketHolder holder, ItemStack stack) { + var server = holder.level().getServer(); + var registry = ServerContext.get(server).registry(); { var computer = getServerComputer(registry, stack); if (computer != null) { @@ -180,14 +174,14 @@ public class PocketComputerItem extends Item { } } - var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), NonNegativeId.Computer::new, IDAssigner.COMPUTER); + var computerID = NonNegativeId.getOrCreate(server, stack, ModRegistry.DataComponents.COMPUTER_ID.get(), NonNegativeId.Computer::new, IDAssigner.COMPUTER); var brain = new PocketBrain(holder, ServerComputer.properties(computerID, getFamily()) .label(getLabel(stack)) .storageCapacity(StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), -1)) .terminalSize(stack.getOrDefault( - ModRegistry.DataComponents.TERMINAL_SIZE.get(), - new TerminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get()) - )) + ModRegistry.DataComponents.TERMINAL_SIZE.get(), + new TerminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get()) + )) ); brain.setUpgrades(getUpgradeWithData(stack, PocketSide.BACK), getUpgradeWithData(stack, PocketSide.BOTTOM)); var computer = brain.computer(); @@ -203,18 +197,25 @@ public class PocketComputerItem extends Item { return brain; } + public @Nullable PocketBrain getBrain(PocketHolder holder, ItemStack stack) { + var computer = getServerComputer(holder.level().getServer(), stack); + if (computer == null) return null; + + var brain = computer.getBrain(); + brain.updateHolder(holder); + return brain; + } + public static boolean isServerComputer(ServerComputer computer, ItemStack stack) { return stack.getItem() instanceof PocketComputerItem && getServerComputer(computer.getLevel().getServer(), stack) == computer; } - @Nullable - public static PocketServerComputer getServerComputer(ServerComputerRegistry registry, ItemStack stack) { + private static @Nullable PocketServerComputer getServerComputer(ServerComputerRegistry registry, ItemStack stack) { return (PocketServerComputer) ServerComputerReference.get(stack, registry); } - @Nullable - public static PocketServerComputer getServerComputer(MinecraftServer server, ItemStack stack) { + private static @Nullable PocketServerComputer getServerComputer(MinecraftServer server, ItemStack stack) { return getServerComputer(ServerContext.get(server).registry(), stack); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index dbb045355..f4f8e043b 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -11,7 +11,6 @@ import dan200.computercraft.api.turtle.TurtleCommandResult; import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.MetricsObserver; -import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods; import dan200.computercraft.shared.turtle.core.*; import org.jspecify.annotations.Nullable; @@ -691,11 +690,11 @@ public class TurtleAPI implements ILuaAPI { /** * Get the upgrade currently equipped on the left of the turtle. *

- * This returns information about the currently equipped item, in the same form as - * {@link #getItemDetail(ILuaContext, Optional, Optional)}. + * This returns [information about the currently equipped item][`item_details`]. * * @return Information about the currently equipped item, or {@code nil} if no upgrade is equipped. * @cc.since 1.116.0 + * @cc.see item_details * @see #equipLeft() */ @LuaFunction(mainThread = true) @@ -707,11 +706,11 @@ public class TurtleAPI implements ILuaAPI { /** * Get the upgrade currently equipped on the right of the turtle. *

- * This returns information about the currently equipped item, in the same form as - * {@link #getItemDetail(ILuaContext, Optional, Optional)}. + * This returns [information about the currently equipped item][`item_details`]. * * @return Information about the currently equipped item, or {@code nil} if no upgrade is equipped. * @cc.since 1.116.0 + * @cc.see item_details * @see #equipRight() */ @LuaFunction(mainThread = true) @@ -721,13 +720,14 @@ public class TurtleAPI implements ILuaAPI { } /** - * Get information about the block in front of the turtle. + * Get [information about the block][`block_details`] in front of the turtle. * * @return The turtle command result. * @cc.treturn boolean Whether there is a block in front of the turtle. * @cc.treturn table|string Information about the block in front, or a message explaining that there is no block. * @cc.since 1.64 * @cc.changed 1.76 Added block state to return value. + * @cc.see block_details * @cc.usage

{@code
      * local has_block, data = turtle.inspect()
      * if has_block then
@@ -747,12 +747,13 @@ public class TurtleAPI implements ILuaAPI {
     }
 
     /**
-     * Get information about the block above the turtle.
+     * Get [information about the block][`block_details`] above the turtle.
      *
      * @return The turtle command result.
      * @cc.treturn boolean Whether there is a block above the turtle.
-     * @cc.treturn table|string Information about the above below, or a message explaining that there is no block.
+     * @cc.treturn table|string Information about the block above, or a message explaining that there is no block.
      * @cc.since 1.64
+     * @cc.see block_details
      */
     @LuaFunction
     public final MethodResult inspectUp() {
@@ -760,12 +761,13 @@ public class TurtleAPI implements ILuaAPI {
     }
 
     /**
-     * Get information about the block below the turtle.
+     * Get [information about the block][`block_details`] below the turtle.
      *
      * @return The turtle command result.
      * @cc.treturn boolean Whether there is a block below the turtle.
      * @cc.treturn table|string Information about the block below, or a message explaining that there is no block.
      * @cc.since 1.64
+     * @cc.see block_details
      */
     @LuaFunction
     public final MethodResult inspectDown() {
@@ -773,7 +775,7 @@ public class TurtleAPI implements ILuaAPI {
     }
 
     /**
-     * Get detailed information about the items in the given slot.
+     * Get [information about the items][`item_details`] in the given slot.
      *
      * @param context  The Lua context
      * @param slot     The slot to get information about. Defaults to the {@link #select selected slot}.
@@ -793,7 +795,7 @@ public class TurtleAPI implements ILuaAPI {
      * --  count = 13,
      * -- }
      * }
- * @see AbstractInventoryMethods#getItemDetail Describes the information returned by a detailed query. + * @cc.see item_details */ @LuaFunction public final MethodResult getItemDetail(ILuaContext context, Optional slot, Optional detailed) throws LuaException { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java index ff660a148..67ffd174e 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java @@ -146,6 +146,7 @@ public final class TurtlePlayer { // Load up the fake inventory inventory.setSelectedSlot(0); + inventory.clearContent(); for (var i = 0; i < slots; i++) { inventory.setItem(i, turtleInventory.getItem((currentSlot + i) % slots)); } @@ -171,7 +172,8 @@ public final class TurtlePlayer { TurtleUtil.storeItemOrDrop(turtle, inventory.getItem(i)); } - inventory.setChanged(); + inventory.clearContent(); + turtleInventory.setChanged(); } public boolean isBlockProtected(ServerLevel level, BlockPos pos) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java b/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java index 4f6543218..5c997b137 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/util/TickScheduler.java @@ -26,6 +26,10 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; * We use this when modems and other peripherals change a block in a different thread. */ public final class TickScheduler { + // FIXME: We also use this to schedule ticks in {@link BlockEntity#clearRemoved()}, as the chunk is not fully + // loaded at this point ({@link LevelChunk#registerTickContainerInLevel(ServerLevel)} has not been called). This + // delays this a tick, which works in practice, but relies on us winning a race condition. + // It might be worth using Forge's BlockEntity.onLoad or having some custom hook based on chunk load. private TickScheduler() { } diff --git a/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java b/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java new file mode 100644 index 000000000..ca7e27633 --- /dev/null +++ b/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.details; + +import dan200.computercraft.api.detail.VanillaDetailRegistries; +import dan200.computercraft.test.core.CustomMatchers; +import dan200.computercraft.test.shared.WithMinecraft; +import net.minecraft.data.registries.VanillaRegistries; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.alchemy.Potions; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; + +@WithMinecraft +class ItemDetailsTest { + @BeforeAll + public static void setup() { + VanillaDetailRegistries.ITEM_STACK.addProvider(ItemDetails::fill); + } + + /** + * Test that all potion-imbued items (potions, throwables and arrows) have the correct duration. + */ + @Test + public void testPotionDurations() { + var registries = VanillaRegistries.createLookup(); + assertThat( + VanillaDetailRegistries.ITEM_STACK.getDetails(registries, PotionContents.createItemStack(Items.POTION, Potions.LONG_NIGHT_VISION)), + containsEntryWith("potionEffects", contains(allOf(containsEntry("name", "minecraft:night_vision"), containsEntry("duration", 480.0)))) + ); + + assertThat( + VanillaDetailRegistries.ITEM_STACK.getDetails(registries, PotionContents.createItemStack(Items.LINGERING_POTION, Potions.LONG_NIGHT_VISION)), + containsEntryWith("potionEffects", contains(allOf(containsEntry("name", "minecraft:night_vision"), containsEntry("duration", 120.0)))) + ); + + assertThat( + VanillaDetailRegistries.ITEM_STACK.getDetails(registries, PotionContents.createItemStack(Items.SPLASH_POTION, Potions.LONG_NIGHT_VISION)), + containsEntryWith("potionEffects", contains(allOf(containsEntry("name", "minecraft:night_vision"), containsEntry("duration", 480.0)))) + ); + + assertThat( + VanillaDetailRegistries.ITEM_STACK.getDetails(registries, PotionContents.createItemStack(Items.TIPPED_ARROW, Potions.LONG_NIGHT_VISION)), + containsEntryWith("potionEffects", contains(allOf(containsEntry("name", "minecraft:night_vision"), containsEntry("duration", 60.0)))) + ); + } + + private static Matcher> containsEntry(String key, Object value) { + return CustomMatchers.containsEntry(key, value); + } + + @SuppressWarnings("unchecked") + private static Matcher> containsEntryWith(String key, Matcher value) { + return CustomMatchers.containsEntryWith(key, (Matcher) value); + } +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Relay_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Relay_Test.kt index d18260930..4eb2467a5 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Relay_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Relay_Test.kt @@ -9,6 +9,7 @@ import dan200.computercraft.gametest.api.GameTest import dan200.computercraft.gametest.api.assertBlockHas import dan200.computercraft.gametest.api.modifyBlock import dan200.computercraft.gametest.api.sequence +import dan200.computercraft.shared.ModRegistry import dan200.computercraft.shared.peripheral.redstone.RedstoneRelayBlockEntity import dan200.computercraft.shared.peripheral.redstone.RedstoneRelayPeripheral import net.minecraft.core.BlockPos @@ -17,6 +18,7 @@ import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.LeverBlock import net.minecraft.world.level.block.RedstoneLampBlock import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue class Relay_Test { /** @@ -95,4 +97,24 @@ class Relay_Test { thenIdle(2) thenExecute { assertEquals(false, relay().getInput(ComputerSide.BACK), "Input should be off") } } + + /** + * Check redstone input is detected when placed/loaded + * + * @see [#2175](https://github.com/cc-tweaked/CC-Tweaked/issues/2175) + */ + @GameTest(template = "default") + fun Initial_input(context: GameTestHelper) = context.sequence { + thenExecute { + context.setBlock(1, 1, 2, Blocks.REDSTONE_BLOCK) + context.setBlock(2, 1, 2, ModRegistry.Blocks.REDSTONE_RELAY.get()) + } + thenIdle(1) + thenExecute { + val relay = context.getBlockEntity(BlockPos(2, 1, 2), RedstoneRelayBlockEntity::class.java) + .peripheral() as RedstoneRelayPeripheral + + assertTrue(relay.getInput(ComputerSide.LEFT), "Input should be on") + } + } } diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt index 2425c6ad2..8f79e4a34 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt @@ -451,7 +451,7 @@ class Turtle_Test { } /** - * Checks turtles can use IDetailProviders by getting details for a printed page. + * Checks turtles can use [VanillaDetailRegistries.ITEM_STACK] by getting details for a printed page. */ @GameTest fun Item_detail_provider(helper: GameTestHelper) = helper.sequence { diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java index e2c54fc52..d0d322b7a 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -326,15 +326,15 @@ public class OSAPI implements ILuaAPI { * * If called with {@code local}, returns the number of days since 1 * January 1970 in the server's local timezone. * - * @param args The locale to get the day for. Defaults to {@code ingame} if not set. + * @param locale The locale to get the day for. Defaults to {@code ingame} if not set. * @return The day depending on the selected locale. * @throws LuaException If an invalid locale is passed. * @cc.since 1.48 * @cc.changed 1.82.0 Arguments are now case insensitive. */ @LuaFunction - public final int day(Optional args) throws LuaException { - return switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) { + public final int day(Optional locale) throws LuaException { + return switch (locale.orElse("ingame").toLowerCase(Locale.ROOT)) { case "utc" -> getDayForCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); case "local" -> getDayForCalendar(Calendar.getInstance()); case "ingame" -> day; @@ -359,7 +359,7 @@ public class OSAPI implements ILuaAPI { * > milliseconds. If you wish to convert this value to real time, divide by 72000; to * > convert to ticks (where a day is 24000 ticks), divide by 3600. * - * @param args The locale to get the milliseconds for. Defaults to {@code ingame} if not set. + * @param locale The locale to get the milliseconds for. Defaults to {@code ingame} if not set. * @return The milliseconds since the epoch depending on the selected locale. * @throws LuaException If an invalid locale is passed. * @cc.since 1.80pr1 @@ -372,26 +372,13 @@ public class OSAPI implements ILuaAPI { * } */ @LuaFunction - public final long epoch(Optional args) throws LuaException { - switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) { - case "utc": { - // Get utc epoch - var c = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - return getEpochForCalendar(c); - } - case "local": { - // Get local epoch - var c = Calendar.getInstance(); - return getEpochForCalendar(c); - } - case "ingame": - // Get in-game epoch - synchronized (alarms) { - return day * 86400000L + (long) (time * 3600000.0); - } - default: - throw new LuaException("Unsupported operation"); - } + public final long epoch(Optional locale) throws LuaException { + return switch (locale.orElse("ingame").toLowerCase(Locale.ROOT)) { + case "utc" -> getEpochForCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); // Get utc epoch + case "local" -> getEpochForCalendar(Calendar.getInstance()); // Get local epoch + case "ingame" -> day * 86400000L + (long) (time * 3600000.0); // Get in-game epoch + default -> throw new LuaException("Unsupported operation"); + }; } /** diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java index 1714c0b41..3d9fb3d30 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java @@ -18,6 +18,8 @@ import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.websocketx.CorruptedWebSocketFrameException; +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.Socks4ProxyHandler; @@ -246,6 +248,10 @@ public final class NetworkUtils { return "Timed out"; } else if (cause instanceof SSLHandshakeException || (cause instanceof DecoderException && cause.getCause() instanceof SSLHandshakeException)) { return "Could not create a secure connection"; + } else if (cause instanceof CorruptedWebSocketFrameException e) { + return e.closeStatus() == WebSocketCloseStatus.MESSAGE_TOO_BIG + ? "Received a too-large message" + : "Corrupted websocket message"; } else { return "Could not connect"; } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/NoOriginWebSocketHandshaker.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/CustomWebSocketHandshaker.java similarity index 51% rename from projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/NoOriginWebSocketHandshaker.java rename to projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/CustomWebSocketHandshaker.java index 48910943c..4a66b42ca 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/NoOriginWebSocketHandshaker.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/CustomWebSocketHandshaker.java @@ -5,19 +5,23 @@ package dan200.computercraft.core.apis.http.websocket; import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13; import io.netty.handler.codec.http.websocketx.WebSocketVersion; +import org.jspecify.annotations.Nullable; import java.net.URI; /** - * A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the - * original HTTP request. + * A version of {@link WebSocketClientHandshaker13} which retains the response headers, and doesn't add the + * {@link HttpHeaderNames#ORIGIN} header to the original HTTP request. */ -class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 { - NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) { +class CustomWebSocketHandshaker extends WebSocketClientHandshaker13 { + private @Nullable HttpHeaders responseHeaders; + + CustomWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) { super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength); } @@ -28,4 +32,14 @@ class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 { if (!customHeaders.contains(HttpHeaderNames.ORIGIN)) headers.remove(HttpHeaderNames.ORIGIN); return request; } + + @Override + protected void verify(FullHttpResponse response) { + super.verify(response); + responseHeaders = response.headers(); + } + + public @Nullable HttpHeaders getResponseHeaders() { + return responseHeaders; + } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index a4ebc4c9e..11e0f1cf3 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory; import java.net.URI; import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; @@ -96,7 +98,7 @@ public class Websocket extends Resource implements WebsocketClient { NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, proxy, timeout); var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL); - var handshaker = new NoOriginWebSocketHandshaker( + var handshaker = new CustomWebSocketHandshaker( uri, WebSocketVersion.V13, subprotocol, true, headers, options.websocketMessage() <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage() ); @@ -107,7 +109,7 @@ public class Websocket extends Resource implements WebsocketClient { new HttpObjectAggregator(8192), WebsocketCompressionHandler.INSTANCE, new WebSocketClientProtocolHandler(handshaker, false, timeout), - new WebsocketHandler(Websocket.this, options) + new WebsocketHandler(Websocket.this, handshaker, options) ); } }) @@ -127,10 +129,16 @@ public class Websocket extends Resource implements WebsocketClient { } } - void success(Options options) { + void success(HttpHeaders responseHeaders, Options options) { if (isClosed()) return; - var handle = new WebsocketHandle(environment, address, this, options); + Map headers = new HashMap<>(); + for (var header : responseHeaders) { + headers.compute(header.getKey(), (k, existing) -> existing == null ? header.getValue() : existing + "," + header.getValue()); + } + + + var handle = new WebsocketHandle(environment, address, this, headers, options); environment().queueEvent(SUCCESS_EVENT, address, handle); createOwnerReference(handle); diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index c1b704b44..25ff6ed92 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -5,6 +5,7 @@ package dan200.computercraft.core.apis.http.websocket; import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.apis.HTTPAPI; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.options.Options; import org.jspecify.annotations.Nullable; @@ -15,6 +16,7 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -35,12 +37,14 @@ public class WebsocketHandle { private final IAPIEnvironment environment; private final String address; private final WebsocketClient websocket; + private final Map responseHeaders; private final Options options; - public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Options options) { + public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Map responseHeaders, Options options) { this.environment = environment; this.address = address; this.websocket = websocket; + this.responseHeaders = responseHeaders; this.options = options; } @@ -53,8 +57,11 @@ public class WebsocketHandle { * @cc.treturn [1] string The received message. * @cc.treturn boolean If this was a binary message. * @cc.treturn [2] nil If the websocket was closed while waiting, or if we timed out. + * @cc.treturn [2] string The reason we failed to receive a message. Either the reason the websocket was closed + * (as returned by [`websocket_closed`], or the string {@code "Timed out"}. * @cc.changed 1.80pr1.13 Added return value indicating whether the message was binary. * @cc.changed 1.87.0 Added timeout argument. + * @cc.changed 1.117.0 Added return value indicating why receiving the message failed. */ @LuaFunction public final MethodResult receive(Optional timeout) throws LuaException { @@ -105,6 +112,30 @@ public class WebsocketHandle { websocket.close(); } + /** + * Get a table containing the headers from the handshake response, in a format similar to that required by + * {@link HTTPAPI#request}. If multiple headers are sent with the same name, they will be combined with a comma. + * + * @return The response's headers. + * @cc.usage Make a websocket connection to [example.tweaked.cc](https://example.tweaked.cc), and print the + * returned headers. + *
{@code
+     * local ws = http.websocket("wss://example.tweaked.cc/echo")
+     * print(textutils.serialize(ws.getResponseHeaders()))
+     * -- => {
+     * --  Connection = "Upgrade",
+     * --  Upgrade = "websocket",
+     * --  ...
+     * -- }
+     * ws.close()
+     * }
+ * @since 1.107.0 + */ + @LuaFunction + public final Map getResponseHeaders() { + return responseHeaders; + } + private void checkOpen() throws LuaException { if (websocket.isClosed()) throw new LuaException("attempt to use a closed file"); } @@ -127,11 +158,11 @@ public class WebsocketHandle { } else if (event.length >= 2 && Objects.equals(event[0], CLOSE_EVENT) && Objects.equals(event[1], address) && websocket.isClosed()) { // If the socket is closed abort. environment.cancelTimer(timeoutId); - return MethodResult.of(); + return MethodResult.of(null, event.length > 2 ? event[2] : "Connection closed"); } else if (event.length >= 2 && timeoutId != -1 && Objects.equals(event[0], TIMER_EVENT) && event[1] instanceof Number id && id.intValue() == timeoutId) { // If we received a matching timer event then abort. - return MethodResult.of(); + return MethodResult.of(null, "Timed out"); } return pull; diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java index 8d5e2e5ae..924ffebfa 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -18,10 +18,12 @@ import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESS class WebsocketHandler extends SimpleChannelInboundHandler { private final Websocket websocket; private final Options options; + private final CustomWebSocketHandshaker handshaker; private boolean handshakeComplete = false; - WebsocketHandler(Websocket websocket, Options options) { + WebsocketHandler(Websocket websocket, CustomWebSocketHandshaker handshaker, Options options) { this.websocket = websocket; + this.handshaker = handshaker; this.options = options; } @@ -34,7 +36,9 @@ class WebsocketHandler extends SimpleChannelInboundHandler { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) { - websocket.success(options); + var headers = handshaker.getResponseHeaders(); + if (headers == null) throw new NullPointerException("Headers cannot be null once handshake is complete"); + websocket.success(headers, options); handshakeComplete = true; } else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) { websocket.failure("Timed out"); @@ -67,9 +71,8 @@ class WebsocketHandler extends SimpleChannelInboundHandler { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - ctx.close(); - fail(NetworkUtils.toFriendlyError(cause)); + ctx.close(); } private void fail(String message) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java b/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java index 5973d0444..09d4c309e 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/projects/core/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicLong; *
  • Passes main thread tasks to the {@link MainThreadScheduler.Executor}.
  • * */ -public class Computer implements ComputerEvents.Receiver { +public class Computer { private static final int START_DELAY = 50; // Various properties of the computer @@ -114,7 +114,6 @@ public class Computer implements ComputerEvents.Receiver { executor.queueStop(false, true); } - @Override public void queueEvent(String event, @Nullable Object @Nullable [] args) { executor.queueEvent(event, args); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java b/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java deleted file mode 100644 index 678607cb1..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/computer/ComputerEvents.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.computer; - -import dan200.computercraft.core.util.StringUtil; -import org.jspecify.annotations.Nullable; - -import java.nio.ByteBuffer; - -/** - * Built-in events that can be queued on a computer. - */ -public final class ComputerEvents { - private ComputerEvents() { - } - - public static void keyDown(Receiver receiver, int key, boolean repeat) { - receiver.queueEvent("key", new Object[]{ key, repeat }); - } - - public static void keyUp(Receiver receiver, int key) { - receiver.queueEvent("key_up", new Object[]{ key }); - } - - /** - * Type a character on the computer. - * - * @param receiver The computer to queue the event on. - * @param chr The character to type. - * @see StringUtil#isTypableChar(byte) - */ - public static void charTyped(Receiver receiver, byte chr) { - receiver.queueEvent("char", new Object[]{ new byte[]{ chr } }); - } - - /** - * Paste a string. - * - * @param receiver The computer to queue the event on. - * @param contents The string to paste. - * @see StringUtil#getClipboardString(String) - */ - public static void paste(Receiver receiver, ByteBuffer contents) { - receiver.queueEvent("paste", new Object[]{ contents }); - } - - public static void mouseClick(Receiver receiver, int button, int x, int y) { - receiver.queueEvent("mouse_click", new Object[]{ button, x, y }); - } - - public static void mouseUp(Receiver receiver, int button, int x, int y) { - receiver.queueEvent("mouse_up", new Object[]{ button, x, y }); - } - - public static void mouseDrag(Receiver receiver, int button, int x, int y) { - receiver.queueEvent("mouse_drag", new Object[]{ button, x, y }); - } - - public static void mouseScroll(Receiver receiver, int direction, int x, int y) { - receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y }); - } - - /** - * An object that can receive computer events. - */ - @FunctionalInterface - public interface Receiver { - void queueEvent(String event, @Nullable Object @Nullable [] arguments); - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index 509ee63f8..6fb31162f 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -352,7 +352,7 @@ public class FileSystem { return sanitizePath(path, false); } - private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$"); + private static final Pattern manyDotsPattern = Pattern.compile("^[. ]+$"); // IMPORTANT: Both arrays are sorted by ASCII value. private static final char[] specialChars = new char[]{ '"', '*', ':', '<', '>', '?', '|' }; @@ -376,27 +376,19 @@ public class FileSystem { for (var fullPart : Splitter.on('/').split(path)) { var part = fullPart.strip(); - if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part).matches()) { - // . is redundant - // ... and more are treated as . - continue; - } + // Limit part length to 255. + if (part.length() > 255) part = part.substring(0, 255).strip(); if (part.equals("..")) { // .. can cancel out the last folder entered - if (!outputParts.isEmpty()) { - var top = outputParts.peekLast(); - if (!top.equals("..")) { - outputParts.removeLast(); - } else { - outputParts.addLast(".."); - } - } else { + if (outputParts.isEmpty() || outputParts.peekLast().equals("..")) { outputParts.addLast(".."); + } else { + outputParts.removeLast(); } - } else if (part.length() >= 255) { - // If part length > 255 and it is the last part - outputParts.addLast(part.substring(0, 255).strip()); + } else if (part.isEmpty() || (part.startsWith(".") && manyDotsPattern.matcher(part).matches())) { + // Skip empty paths, ".", or any other sequence of "[. ]+" (as this is also treated as "." on Windows). + continue; } else { // Anything else we add to the stack outputParts.addLast(part); diff --git a/projects/core/src/main/java/dan200/computercraft/core/input/ComputerInput.java b/projects/core/src/main/java/dan200/computercraft/core/input/ComputerInput.java new file mode 100644 index 000000000..8fa13282c --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/input/ComputerInput.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.input; + +import dan200.computercraft.core.util.StringUtil; + +import java.nio.ByteBuffer; + +/** + * Input events that can be performed on a computer. + * + * @see EventComputerInput + * @see UserComputerInput + */ +public interface ComputerInput { + /** + * Queue a {@code key} event. + * + * @param key The key that was pressed. + * @param repeat Whether this is a repeat input. + */ + void keyDown(int key, boolean repeat); + + /** + * Queue a {@code key_up} event. + * + * @param key The key that was released. + */ + void keyUp(int key); + + /** + * Type a character on the computer. + * + * @param chr The character to type. + * @see StringUtil#isTypableChar(byte) + */ + void charTyped(byte chr); + + /** + * Paste a string. + * + * @param contents The string to paste. + * @see StringUtil#getClipboardString(String) + */ + void paste(ByteBuffer contents); + + /** + * Queue a {@code mouse_click} event. + * + * @param button The mouse button that was pressed, between 1 and 3 (inclusive). + * @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive). + * @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive). + */ + void mouseClick(int button, int x, int y); + + /** + * Queue a {@code mouse_up} event. + * + * @param button The mouse button that was released, between 1 and 3 (inclusive). + * @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive). + * @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive). + */ + void mouseUp(int button, int x, int y); + + /** + * Queue a {@code mouse_drag} event. + * + * @param button The mouse button that is being pressed, between 1 and 3 (inclusive). + * @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive). + * @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive). + */ + void mouseDrag(int button, int x, int y); + + /** + * Queue a {@code mouse_scroll} event. + * + * @param direction The direction of the scroll, where negative values are up and positive ones are down. + * @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive). + * @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive). + */ + void mouseScroll(int direction, int x, int y); +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/input/EventComputerInput.java b/projects/core/src/main/java/dan200/computercraft/core/input/EventComputerInput.java new file mode 100644 index 000000000..495b45f40 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/input/EventComputerInput.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.input; + +import dan200.computercraft.core.computer.Computer; +import org.jspecify.annotations.Nullable; + +import java.nio.ByteBuffer; + +/** + * A {@link ComputerInput} that queues events on the computer. + */ +public final class EventComputerInput implements ComputerInput { + private final QueueEvent receiver; + + public EventComputerInput(QueueEvent receiver) { + this.receiver = receiver; + } + + public EventComputerInput(Computer computer) { + this(computer::queueEvent); + } + + @Override + public void keyDown(int key, boolean repeat) { + receiver.queueEvent("key", new Object[]{ key, repeat }); + } + + @Override + public void keyUp(int key) { + receiver.queueEvent("key_up", new Object[]{ key }); + } + + @Override + public void charTyped(byte chr) { + receiver.queueEvent("char", new Object[]{ new byte[]{ chr } }); + } + + @Override + public void paste(ByteBuffer contents) { + receiver.queueEvent("paste", new Object[]{ contents }); + } + + @Override + public void mouseClick(int button, int x, int y) { + receiver.queueEvent("mouse_click", new Object[]{ button, x, y }); + } + + @Override + public void mouseUp(int button, int x, int y) { + receiver.queueEvent("mouse_up", new Object[]{ button, x, y }); + } + + @Override + public void mouseDrag(int button, int x, int y) { + receiver.queueEvent("mouse_drag", new Object[]{ button, x, y }); + } + + @Override + public void mouseScroll(int direction, int x, int y) { + receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y }); + } + + /** + * A function to queue events. + */ + @FunctionalInterface + public interface QueueEvent { + void queueEvent(String event, @Nullable Object @Nullable [] arguments); + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/input/UserComputerInput.java b/projects/core/src/main/java/dan200/computercraft/core/input/UserComputerInput.java new file mode 100644 index 000000000..b52bfeebf --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/input/UserComputerInput.java @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.input; + +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.util.StringUtil; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; + +import java.nio.ByteBuffer; + +/** + * A {@link ComputerInput} that wraps an existing {@link ComputerInput}. This both validates any user inputs (e.g. + * ensuring mouse presses only happen on an advanced terminal), and supports {@linkplain #releaseInputs() + * releasing any held inputs} (e.g. for when the computer loses focus). + */ +public final class UserComputerInput implements ComputerInput { + private final ComputerInput delegate; + private final boolean mouseSupport; + private final int termWidth; + private final int termHeight; + + private final IntSet keysDown = new IntOpenHashSet(4); + + private int lastMouseX; + private int lastMouseY; + private int lastMouseDown = -1; + + public UserComputerInput(ComputerInput delegate, boolean mouseSupport, int termWidth, int termHeight) { + this.delegate = delegate; + this.mouseSupport = mouseSupport; + this.termWidth = termWidth; + this.termHeight = termHeight; + } + + public UserComputerInput(ComputerInput delegate, Terminal terminal) { + this(delegate, terminal.isColour(), terminal.getWidth(), terminal.getHeight()); + } + + @Override + public void keyDown(int key, boolean repeat) { + if (key < 0) return; + + keysDown.add(key); + delegate.keyDown(key, repeat); + } + + /** + * Queue a {@code key} event on the computer. This behaves the same as {@link #keyDown(int, boolean)}, but infers + * the {@code "repeat} state from the currently held keys. + * + * @param key The key to press. + */ + public void keyDown(int key) { + keyDown(key, keysDown.contains(key)); + } + + @Override + public void keyUp(int key) { + if (key < 0) return; + + keysDown.remove(key); + delegate.keyUp(key); + } + + @Override + public void charTyped(byte chr) { + delegate.charTyped(chr); + } + + public void codepointTyped(int codepoint) { + var terminalChar = StringUtil.unicodeToTerminal(codepoint); + if (StringUtil.isTypableChar(terminalChar)) charTyped((byte) terminalChar); + } + + private static boolean isValidClipboard(ByteBuffer buffer) { + for (int i = buffer.position(), max = buffer.limit(); i < max; i++) { + if (!StringUtil.isTypableChar(buffer.get(i))) return false; + } + return true; + } + + @Override + public void paste(ByteBuffer contents) { + if (contents.remaining() > 0 && isValidClipboard(contents)) delegate.paste(contents); + } + + /** + * Paste a string. + * + * @param contents The string to paste. + */ + public void paste(String contents) { + paste(StringUtil.getClipboardString(contents)); + } + + @Override + public void mouseClick(int button, int x, int y) { + if (!mouseSupport || button < 1 || button > 3) return; + var clampedX = lastMouseX = Math.min(Math.max(x, 1), termWidth); + var clampedY = lastMouseY = Math.min(Math.max(y, 1), termHeight); + + delegate.mouseClick(button, clampedX, clampedY); + lastMouseDown = button; + } + + /** + * Queue a {@code mouse_click} event on the computer. This behaves the same as {@link #mouseClick(int, int, int)}, + * but infers the mouse position from the last mouse position. + * + * @param button The mouse button pressed, between 1 and 3. + */ + public void mouseClick(int button) { + mouseClick(button, lastMouseX, lastMouseY); + } + + @Override + public void mouseUp(int button, int x, int y) { + if (!mouseSupport || button < 1 || button > 3) return; + var clampedX = lastMouseX = Math.min(Math.max(x, 1), termWidth); + var clampedY = lastMouseY = Math.min(Math.max(y, 1), termHeight); + + if (lastMouseDown == button) { + delegate.mouseUp(button, clampedX, clampedY); + lastMouseDown = -1; + } + } + + /** + * Queue a {@code mouse_scroll} event on the computer. This behaves the same as {@link #mouseUp(int, int, int)}, + * but infers the mouse position from the last mouse position. + * + * @param button The mouse button released, between 1 and 3. + */ + public void mouseUp(int button) { + mouseUp(button, lastMouseX, lastMouseY); + } + + @Override + public void mouseDrag(int button, int x, int y) { + if (!mouseSupport || button < 1 || button > 3) return; + var clampedX = Math.min(Math.max(x, 1), termWidth); + var clampedY = Math.min(Math.max(y, 1), termHeight); + + if (button == lastMouseDown && (clampedX != lastMouseX || clampedY != lastMouseY)) { + delegate.mouseDrag(button, clampedX, clampedY); + lastMouseX = clampedX; + lastMouseY = clampedY; + } + } + + /** + * Update the mouse position, and optionally queue a {@code mouse_drag} event on the computer. + *

    + * This is similar to {@link #mouseDrag(int, int, int)}, but when the currently clicked button is not available. + * + * @param x The X position of the mouse, between 1 and the terminal width. + * @param y The Y position of the mouse, between 1 and the terminal width. + */ + public void mouseMove(int x, int y) { + if (!mouseSupport) return; + var clampedX = Math.min(Math.max(x, 1), termWidth); + var clampedY = Math.min(Math.max(y, 1), termHeight); + + if (lastMouseDown != -1 && (clampedX != lastMouseX || clampedY != lastMouseY)) { + delegate.mouseDrag(lastMouseDown, clampedX, clampedY); + } + + lastMouseX = clampedX; + lastMouseY = clampedY; + } + + @Override + public void mouseScroll(int direction, int x, int y) { + if (!mouseSupport || direction == 0) return; + var clampedX = lastMouseX = Math.min(Math.max(x, 1), termWidth); + var clampedY = lastMouseY = Math.min(Math.max(y, 1), termHeight); + + delegate.mouseScroll(direction, clampedX, clampedY); + } + + /** + * Queue a {@code mouse_scroll} event on the computer. This behaves the same as {@link #mouseScroll(int, int, int)}, + * but infers the mouse position from the last mouse position. + * + * @param direction The direction of the scroll. + */ + public void mouseScroll(int direction) { + mouseScroll(direction, lastMouseX, lastMouseY); + } + + /** + * Release all currently held inputs, such as held keys and pressed mouse buttons. + */ + public void releaseInputs() { + // Release all keys + var keys = keysDown.iterator(); + while (keys.hasNext()) delegate.keyUp(keys.nextInt()); + keysDown.clear(); + + // Release last held mouse button. + if (lastMouseDown != -1) { + delegate.mouseUp(lastMouseDown, lastMouseX, lastMouseY); + lastMouseDown = -1; + } + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java index 2916929f1..b8b062c85 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java +++ b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java @@ -4,7 +4,7 @@ package dan200.computercraft.core.util; -import dan200.computercraft.core.computer.ComputerEvents; +import dan200.computercraft.core.input.ComputerInput; import java.nio.ByteBuffer; @@ -71,8 +71,8 @@ public final class StringUtil { } /** - * Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte) - * "char" event}. + * Check if a character is capable of being input and passed to a {@linkplain ComputerInput#charTyped(byte) "char" + * event}. * * @param chr The character to check. * @return Whether this character can be typed. @@ -82,8 +82,8 @@ public final class StringUtil { } /** - * Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte) - * "char" event}. + * Check if a character is capable of being input and passed to a {@linkplain ComputerInput#charTyped(byte) "char" + * * event}. * * @param chr The character to check. * @return Whether this character can be typed. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/gps.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/gps.lua index e9c33b833..34f787473 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/gps.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/gps.lua @@ -5,29 +5,14 @@ --[[- Use [modems][`modem`] to locate the position of the current turtle or computers. -It broadcasts a PING message over [`rednet`] and wait for responses. In order for -this system to work, there must be at least 4 computers used as gps hosts which -will respond and allow trilateration. Three of these hosts should be in a plane, -and the fourth should be either above or below the other three. The three in a -plane should not be in a line with each other. You can set up hosts using the -gps program. - -> [!NOTE] -> When entering in the coordinates for the host you need to put in the `x`, `y`, -> and `z` coordinates of the block that the modem is connected to, not the modem. -> All modem distances are measured from the block that the modem is placed on. - -Also note that you may choose which axes x, y, or z refers to - so long as your -systems have the same definition as any GPS servers that're in range, it works -just the same. For example, you might build a GPS cluster according to [this -tutorial][1], using z to account for height, or you might use y to account for -height in the way that Minecraft's debug screen displays. - -[1]: https://ccf.squiddev.cc/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/ +This works by communicating with other computers (called GPS hosts) that already +know their position, finding the distance to those computers (with +[`modem_message`]), and using that to derive its position from theirs (with a +process known as [trilateration](https://en.wikipedia.org/wiki/Trilateration). @module gps @since 1.31 -@see gps_setup For more detailed instructions on setting up GPS +@see gps_setup ]] local expect = dofile("rom/modules/main/cc/expect.lua").expect diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua index 013a51fed..a311f65b6 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua @@ -72,18 +72,20 @@ function parseImage(image) return tImage end ---- Loads an image from a file. --- --- You can create a file suitable for being loaded using the `paint` program. --- --- @tparam string path The file to load. --- --- @treturn table|nil The parsed image data, suitable for use with --- [`paintutils.drawImage`], or `nil` if the file does not exist. --- @usage Load an image and draw it. --- --- local image = paintutils.loadImage("data/example.nfp") --- paintutils.drawImage(image, term.getCursorPos()) +--[[- Loads an image from a file. + +You can create a file suitable for being loaded using the `paint` program. + +@tparam string path The file to load. + +@treturn table|nil The parsed image data, suitable for use with +[`paintutils.drawImage`], or `nil` if the file does not exist. + +@usage Load an image and draw it. + + local image = paintutils.loadImage("data/example.nfp") + paintutils.drawImage(image, term.getCursorPos()) +]] function loadImage(path) expect(1, path, "string") @@ -96,15 +98,17 @@ function loadImage(path) return nil end ---- Draws a single pixel to the current term at the specified position. --- --- Be warned, this may change the position of the cursor and the current --- background colour. You should not expect either to be preserved. --- --- @tparam number xPos The x position to draw at, where 1 is the far left. --- @tparam number yPos The y position to draw at, where 1 is the very top. --- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be --- the current background colour if not specified. +--[[- Draws a single pixel to the current term at the specified position. + +> [!WARNING] +> This function may change the position of the cursor and the current +> background colour. You should not expect either to be preserved. + +@tparam number xPos The x position to draw at, where 1 is the far left. +@tparam number yPos The y position to draw at, where 1 is the very top. +@tparam[opt] number colour The [color][`colors`] of this pixel. This will be +the current background colour if not specified. +]] function drawPixel(xPos, yPos, colour) expect(1, xPos, "number") expect(2, yPos, "number") @@ -116,18 +120,21 @@ function drawPixel(xPos, yPos, colour) return drawPixelInternal(xPos, yPos) end ---- Draws a straight line from the start to end position. --- --- Be warned, this may change the position of the cursor and the current --- background colour. You should not expect either to be preserved. --- --- @tparam number startX The starting x position of the line. --- @tparam number startY The starting y position of the line. --- @tparam number endX The end x position of the line. --- @tparam number endY The end y position of the line. --- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be --- the current background colour if not specified. --- @usage paintutils.drawLine(2, 3, 30, 7, colors.red) +--[[- Draws a straight line from the start to end position. + +> [!WARNING] +> This function may change the position of the cursor and the current +> background colour. You should not expect either to be preserved. + +@tparam number startX The starting x position of the line. +@tparam number startY The starting y position of the line. +@tparam number endX The end x position of the line. +@tparam number endY The end y position of the line. +@tparam[opt] number colour The [color][`colors`] of this pixel. This will be +the current background colour if not specified. + +@usage paintutils.drawLine(2, 3, 30, 7, colors.red) +]] function drawLine(startX, startY, endX, endY, colour) expect(1, startX, "number") expect(2, startY, "number") @@ -189,19 +196,22 @@ function drawLine(startX, startY, endX, endY, colour) end end ---- Draws the outline of a box on the current term from the specified start --- position to the specified end position. --- --- Be warned, this may change the position of the cursor and the current --- background colour. You should not expect either to be preserved. --- --- @tparam number startX The starting x position of the line. --- @tparam number startY The starting y position of the line. --- @tparam number endX The end x position of the line. --- @tparam number endY The end y position of the line. --- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be --- the current background colour if not specified. --- @usage paintutils.drawBox(2, 3, 30, 7, colors.red) +--[[- Draws the outline of a box on the current term from the specified start +position to the specified end position. + +> [!WARNING] +> This function may change the position of the cursor and the current +> background colour. You should not expect either to be preserved. + +@tparam number startX The starting x position of the line. +@tparam number startY The starting y position of the line. +@tparam number endX The end x position of the line. +@tparam number endY The end y position of the line. +@tparam[opt] number colour The [color][`colors`] of this pixel. This will be +the current background colour if not specified. + +@usage paintutils.drawBox(2, 3, 30, 7, colors.red) +]] function drawBox(startX, startY, endX, endY, nColour) expect(1, startX, "number") expect(2, startY, "number") @@ -242,19 +252,22 @@ function drawBox(startX, startY, endX, endY, nColour) end end ---- Draws a filled box on the current term from the specified start position to --- the specified end position. --- --- Be warned, this may change the position of the cursor and the current --- background colour. You should not expect either to be preserved. --- --- @tparam number startX The starting x position of the line. --- @tparam number startY The starting y position of the line. --- @tparam number endX The end x position of the line. --- @tparam number endY The end y position of the line. --- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be --- the current background colour if not specified. --- @usage paintutils.drawFilledBox(2, 3, 30, 7, colors.red) +--[[- Draws a filled box on the current term from the specified start position to +the specified end position. + +> [!WARNING] +> This function may change the position of the cursor and the current +> background colour. You should not expect either to be preserved. + +@tparam number startX The starting x position of the line. +@tparam number startY The starting y position of the line. +@tparam number endX The end x position of the line. +@tparam number endY The end y position of the line. +@tparam[opt] number colour The [color][`colors`] of this pixel. This will be +the current background colour if not specified. + +@usage paintutils.drawFilledBox(2, 3, 30, 7, colors.red) +]] function drawFilledBox(startX, startY, endX, endY, nColour) expect(1, startX, "number") expect(2, startY, "number") @@ -288,11 +301,21 @@ function drawFilledBox(startX, startY, endX, endY, nColour) end end ---- Draw an image loaded by [`paintutils.parseImage`] or [`paintutils.loadImage`]. --- --- @tparam table image The parsed image data. --- @tparam number xPos The x position to start drawing at. --- @tparam number yPos The y position to start drawing at. +--[[- Draw an image loaded by [`paintutils.parseImage`] or [`paintutils.loadImage`]. + +> [!WARNING] +> This function may change the position of the cursor and the current +> background colour. You should not expect either to be preserved. + +@tparam table image The parsed image data. +@tparam number xPos The x position to start drawing at. +@tparam number yPos The y position to start drawing at. + +@usage Load an image and draw it. + + local image = paintutils.loadImage("data/example.nfp") + paintutils.drawImage(image, term.getCursorPos()) +]] function drawImage(image, xPos, yPos) expect(1, image, "table") expect(2, xPos, "number") diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md index 3ca71ebc5..e16b6e53f 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md @@ -1,3 +1,21 @@ +# New features in CC: Tweaked 1.117.0 + +* Support mouse input for pocket computers on a lectern. +* Pocket computers on a lectern now attach the peripheral below. +* Add map colour to block and item details (ShreksHellraiser). +* Add potion effects to item details. +* add `getResponseHeaders` method to websocket handles. +* Update translations. + +Several bug fixes: +* Many documentation fixes (McJack123, tomodachi94). +* Fix crash when CC:T blocks are placed with Building Gadgets. +* Fix redstone relays not updating redstone input/output on chunk load. +* Fix inconsistency with handling `. .` on Windows. +* Fix bundled cable input not updating with MoreRed. +* Fix `websocket_closed` not always being closed when the socket closes due to an error. +* Fix `nbt` hash failing to be computed for some items. + # New features in CC: Tweaked 1.116.2 Several bug fixes: @@ -30,9 +48,9 @@ Several bug fixes: * Fix pocket computer dyes being lost when equipping/unequipping upgrades. * Fix superflous warnings from allocation tracking. * Fix `__lt`/`__le` not working on heterogeneous types. -* Many documentation fixes (Lemmmy, matematikaadit, McJack12). +* Many documentation fixes (Lemmmy, matematikaadit, McJack123). * Fix `0` being treated as a valid colour in `window` and `colour.toBlit`. -* Fix out-of-bounds when pasting too lon text. +* Fix out-of-bounds when pasting too long text. * Fix syntax highlighting of string escapes (LorneHyde). * Fix sidebar texture of advanced computers being offset. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md index a7599344a..7bb48b898 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md @@ -1,10 +1,19 @@ -New features in CC: Tweaked 1.116.2 +New features in CC: Tweaked 1.117.0 + +* Support mouse input for pocket computers on a lectern. +* Pocket computers on a lectern now attach the peripheral below. +* Add map colour to block and item details (ShreksHellraiser). +* Add potion effects to item details. +* add `getResponseHeaders` method to websocket handles. +* Update translations. Several bug fixes: -* Update Create compatibility to Create Fabric 6.0. -* Various documentation fixes (Zirunis). -* Fix crash with Inventorio. -* Various fixes to SNBT parsing. -* Fix Regex DDoS in string pattern matching. +* Many documentation fixes (McJack123, tomodachi94). +* Fix crash when CC:T blocks are placed with Building Gadgets. +* Fix redstone relays not updating redstone input/output on chunk load. +* Fix inconsistency with handling `. .` on Windows. +* Fix bundled cable input not updating with MoreRed. +* Fix `websocket_closed` not always being closed when the socket closes due to an error. +* Fix `nbt` hash failing to be computed for some items. Type "help changelog" to see the full version history. diff --git a/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 43d61eafa..9521cbe7b 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -432,17 +432,11 @@ public class ComputerTestDelegate { } switch (status) { - case "ok": - break; - case "pending": - runResult = new TestAbortedException("Test is pending"); - break; - case "fail": - runResult = new AssertionFailedError(wholeMessage.toString()); - break; - case "error": - runResult = new IllegalStateException(wholeMessage.toString()); - break; + case "ok" -> { + } + case "pending" -> runResult = new TestAbortedException("Test is pending"); + case "fail" -> runResult = new AssertionFailedError(wholeMessage.toString()); + case "error" -> runResult = new IllegalStateException(wholeMessage.toString()); } runFinished = true; diff --git a/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java b/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java index 90f67a9b5..56284f60a 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java @@ -89,6 +89,7 @@ public class FileSystemTest { new String[]{ "a/./b", "a/b" }, new String[]{ "a/../b", "b" }, new String[]{ "a/.../b", "a/b" }, + new String[]{ "a/. ./b", "a/b" }, new String[]{ " a ", "a" }, new String[]{ "a b c", "a b c" }, }; diff --git a/projects/core/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java b/projects/core/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java index 37d8d9336..a7d70c3a3 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java @@ -585,7 +585,7 @@ class TerminalTest { } } - public Matcher matches() { + private Matcher matches() { return allOf( textMatches(textLines), textColourMatches(textColourLines), backgroundColourMatches(backgroundColourLines) ); diff --git a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt index a2c282d6f..544b9ab14 100644 --- a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt +++ b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt @@ -17,43 +17,52 @@ import io.netty.handler.codec.http.websocketx.WebSocketFrame import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler +import java.net.InetSocketAddress import java.nio.charset.StandardCharsets -/** - * Runs a small HTTP server to run alongside [TestHttpApi] - */ -object HttpServer { - const val PORT: Int = 8378 - const val URL: String = "http://127.0.0.1:$PORT" - const val WS_URL: String = "ws://127.0.0.1:$PORT/ws" +class HttpServer(val port: Int, private val workerGroup: EventLoopGroup, private val activeConnections: Set) { + /** Stop the server from running */ + fun stop() { + workerGroup.shutdownGracefully() + } - fun runServer(run: (stop: () -> Unit) -> Unit) { - val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(2, NioIoHandler.newFactory()) - try { - val ch = ServerBootstrap() - .group(workerGroup) - .channel(NioServerSocketChannel::class.java) - .childHandler( - object : ChannelInitializer() { - override fun initChannel(ch: SocketChannel) { - val p: ChannelPipeline = ch.pipeline() - p.addLast(HttpServerCodec()) - p.addLast(HttpContentCompressor()) - p.addLast(HttpObjectAggregator(8192)) - p.addLast(HttpServerHandler()) - p.addLast(WebSocketServerCompressionHandler(0)) - p.addLast(WebSocketServerProtocolHandler("/ws", null, true)) - p.addLast(WebSocketFrameHandler()) - } - }, - ).bind(PORT).sync().channel() + /** Broadcast this message to every connected websocket */ + fun broadcast(message: WebSocketFrame) { + for (chan in activeConnections) chan.writeAndFlush(message) + } + + companion object { + /** Runs a small HTTP server to run alongside [TestHttpApi] */ + fun runServer(run: (server: HttpServer) -> Unit) { + val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(2, NioIoHandler.newFactory()) + val activeConnections = mutableSetOf() try { - run { workerGroup.shutdownGracefully() } + val ch = ServerBootstrap() + .group(workerGroup) + .channel(NioServerSocketChannel::class.java) + .childHandler( + object : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val p: ChannelPipeline = ch.pipeline() + p.addLast(HttpServerCodec()) + p.addLast(HttpContentCompressor()) + p.addLast(HttpObjectAggregator(8192)) + p.addLast(HttpServerHandler()) + p.addLast(WebSocketServerCompressionHandler(0)) + p.addLast(WebSocketServerProtocolHandler("/ws", null, true)) + p.addLast(WebSocketFrameHandler(activeConnections)) + } + }, + ).bind(0).sync().channel() + val port = (ch.localAddress() as InetSocketAddress).port + try { + run(HttpServer(port, workerGroup, activeConnections)) + } finally { + ch.close().sync() + } } finally { - ch.close().sync() + workerGroup.shutdownGracefully().get() } - } finally { - workerGroup.shutdownGracefully().get() } } } @@ -70,7 +79,7 @@ private class HttpServerHandler : SimpleChannelInboundHandler() ctx.flush() } - public override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) { + override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) { when (request.uri()) { "/", "/index.html" -> handleIndex(ctx, request) "/ws" -> handleWebsocket(ctx, request) @@ -113,7 +122,7 @@ private class HttpServerHandler : SimpleChannelInboundHandler() /** * A basic WS server which just sends back the original message. */ -private class WebSocketFrameHandler : SimpleChannelInboundHandler() { +private class WebSocketFrameHandler(private val activeConnections: MutableSet) : SimpleChannelInboundHandler() { override fun channelRead0(ctx: ChannelHandlerContext, frame: WebSocketFrame) { if (frame is TextWebSocketFrame) { // Send the uppercase string back. @@ -126,10 +135,16 @@ private class WebSocketFrameHandler : SimpleChannelInboundHandler LuaTaskRunner.runTest { + val url = "http://127.0.0.1:${server.port}" val httpApi = addApi(HTTPAPI(environment)) - assertThat("http.request succeeded", httpApi.request(ObjectArguments(URL)), array(equalTo(true))) + assertThat("http.request succeeded", httpApi.request(ObjectArguments(url)), array(equalTo(true))) val result = pullEvent("http_success") - assertThat(result, array(equalTo("http_success"), equalTo(URL), isA(HttpResponseHandle::class.java))) + assertThat(result, array(equalTo("http_success"), equalTo(url), isA(HttpResponseHandle::class.java))) val handle = result[2] as HttpResponseHandle val reader = handle.extra.iterator().next() as ReadHandle @@ -67,13 +68,14 @@ class TestHttpApi { @Test fun `Connects to websocket`() { - runServer { + runServer { server -> LuaTaskRunner.runTest { + val url = "ws://127.0.0.1:${server.port}/ws" val httpApi = addApi(HTTPAPI(environment)) - assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true))) + assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(url)), array(equalTo(true))) val connectEvent = pullEvent() - assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java))) + assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(url), isA(WebsocketHandle::class.java))) val websocket = connectEvent[2] as WebsocketHandle websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) @@ -91,13 +93,14 @@ class TestHttpApi { @Test fun `Errors if too many websocket messages are sent`() { - runServer { + runServer { server -> LuaTaskRunner.runTest { + val url = "ws://127.0.0.1:${server.port}/ws" val httpApi = addApi(HTTPAPI(environment)) - assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true))) + assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(url)), array(equalTo(true))) val connectEvent = pullEvent() - assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java))) + assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(url), isA(WebsocketHandle::class.java))) val websocket = connectEvent[2] as WebsocketHandle val error = assertThrows { @@ -114,24 +117,46 @@ class TestHttpApi { } @Test - fun `Queues an event when the socket is externally closed`() { - runServer { stop -> + fun `Closes if a websocket message is too large`() { + runServer { server -> LuaTaskRunner.runTest { + val url = "ws://127.0.0.1:${server.port}/ws" val httpApi = addApi(HTTPAPI(environment)) - assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(WS_URL)), array(equalTo(true))) + assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(url)), array(equalTo(true))) val connectEvent = pullEvent() - assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java))) + assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(url), isA(WebsocketHandle::class.java))) + + val out = ByteArray(AddressRule.WEBSOCKET_MESSAGE + 1) + Random(0xDEADBEEF).nextBytes(out) + server.broadcast(BinaryWebSocketFrame(Unpooled.wrappedBuffer(out))) + + val closeEvent = pullEvent() + assertThat(closeEvent, array(equalTo("websocket_closed"), equalTo(url), equalTo("Received a too-large message"), nullValue())) + } + } + } + + @Test + fun `Queues an event when the socket is externally closed`() { + runServer { server -> + LuaTaskRunner.runTest { + val url = "ws://127.0.0.1:${server.port}/ws" + val httpApi = addApi(HTTPAPI(environment)) + assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(url)), array(equalTo(true))) + + val connectEvent = pullEvent() + assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(url), isA(WebsocketHandle::class.java))) val websocket = connectEvent[2] as WebsocketHandle - stop() + server.stop() val closeEvent = pullEvent("websocket_closed") assertThat( "Websocket was closed", closeEvent, - array(equalTo("websocket_closed"), equalTo(WS_URL), equalTo("Connection closed"), equalTo(null)), + array(equalTo("websocket_closed"), equalTo(url), equalTo("Connection closed"), equalTo(null)), ) assertThrows("Throws an exception when sending") { diff --git a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua index 05fc9c849..b5e256084 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -156,6 +156,11 @@ describe("The fs library", function() expect(fs.combine("a", "../../c")):eq("../c") end) + it("handles weird Windows paths", function() + expect(fs.combine("a", "...")):eq("a") + expect(fs.combine("a", ". .")):eq("a") + end) + it("combines empty paths", function() expect(fs.combine("a")):eq("a") expect(fs.combine("a", "")):eq("a") diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java index 679f79258..8841bbf39 100644 --- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java +++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java @@ -5,11 +5,14 @@ package dan200.computercraft.test.core; import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; import java.util.List; +import java.util.Map; import java.util.function.Function; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; public class CustomMatchers { /** @@ -25,4 +28,32 @@ public class CustomMatchers { public static Matcher> containsWith(List items, Function> matcher) { return contains(items.stream().map(matcher).toList()); } + + /** + * An alternative to {@link org.hamcrest.Matchers#hasEntry(Object, Object)}, that acts as a projection, rather than + * searching the map. + * + * @param key The key to extract. + * @param value The expected value. + * @param The type of keys in the map. + * @param The type of values in the map. + * @return A matcher that projects out of a map. + */ + public static Matcher> containsEntry(K key, V value) { + return containsEntryWith(key, is(value)); + } + + /** + * An alternative to {@link org.hamcrest.Matchers#hasEntry(Matcher, Matcher)}, that acts as a projection, rather + * than searching the map. + * + * @param key The key to extract. + * @param value The expected value. + * @param The type of keys in the map. + * @param The type of values in the map. + * @return A matcher that projects out of a map. + */ + public static Matcher> containsEntryWith(K key, Matcher value) { + return ContramapMatcher.contramap(value, new StringDescription().appendValue(key).toString(), x -> x.get(key)); + } } diff --git a/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt b/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt index 89ac89d3f..6221dfb3f 100644 --- a/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt +++ b/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt @@ -10,9 +10,11 @@ import dan200.computercraft.api.lua.MethodResult import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.core.apis.OSAPI import dan200.computercraft.core.apis.PeripheralAPI -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.withTimeoutOrNull +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration /** @@ -69,7 +71,8 @@ interface LuaTaskContext { inline fun LuaTaskContext.getApi(): T = getApi(T::class.java) abstract class AbstractLuaTaskContext : LuaTaskContext, AutoCloseable { - private val pullEvents = mutableListOf() + private val isReceiving = AtomicBoolean(false) + private val eventStream: Channel = Channel(Channel.UNLIMITED) private val apis = mutableMapOf, ILuaAPI>() protected fun addApi(api: ILuaAPI) { @@ -77,34 +80,40 @@ abstract class AbstractLuaTaskContext : LuaTaskContext, AutoCloseable { } protected val hasEventListeners - get() = pullEvents.isNotEmpty() + get() = isReceiving.get() protected fun queueEvent(eventName: String?, arguments: Array?) { - val fullEvent: Array = when { - eventName == null && arguments == null -> arrayOf() - eventName != null && arguments == null -> arrayOf(eventName) - eventName == null && arguments != null -> arguments - else -> arrayOf(eventName, *arguments!!) - } - for (i in pullEvents.size - 1 downTo 0) { - val puller = pullEvents[i] - if (puller.name == null || puller.name == eventName || eventName == "terminate") { - pullEvents.removeAt(i) - puller.cont.resumeWith(Result.success(fullEvent)) - } - } + eventStream.trySend(Event(eventName, arguments)).getOrThrow() } override fun close() { - for (pullEvent in pullEvents) pullEvent.cont.cancel() - pullEvents.clear() + eventStream.close() } final override fun getApi(api: Class): T = api.cast(apis[api] ?: throw IllegalStateException("No API of type ${api.name}")) - final override suspend fun pullEvent(event: String?): Array = - suspendCancellableCoroutine { cont -> pullEvents.add(PullEvent(event, cont)) } + final override suspend fun pullEvent(event: String?): Array { + if (!isReceiving.compareAndSet(false, true)) { + throw IllegalStateException("Multiple listeners not currently supported") + } - private class PullEvent(val name: String?, val cont: CancellableContinuation>) + try { + while (true) { + val received = eventStream.receive() + if (event == null || received.name == event) { + return received.full + } + } + } catch (e: ClosedReceiveChannelException) { + throw CancellationException(e) + } finally { + isReceiving.set(false) + } + } + + private class Event(val name: String?, val args: Array?) { + val full: Array + get() = if (args == null) arrayOf(name) else arrayOf(name, *args) + } } diff --git a/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt b/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt index abb100512..d289de872 100644 --- a/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt +++ b/projects/core/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt @@ -9,24 +9,18 @@ import dan200.computercraft.api.lua.ILuaContext import dan200.computercraft.api.lua.LuaException import dan200.computercraft.core.apis.IAPIEnvironment import dan200.computercraft.test.core.apis.BasicApiEnvironment -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class LuaTaskRunner : AbstractLuaTaskContext() { - private val eventStream: Channel = Channel(Channel.UNLIMITED) private val apis = mutableListOf() val environment: IAPIEnvironment = object : BasicApiEnvironment(BasicEnvironment()) { override fun queueEvent(event: String?, vararg args: Any?) = this@LuaTaskRunner.queueEvent(event, args) - - override fun shutdown() { - super.shutdown() - eventStream.close() - } } + override val context = ILuaContext { throw LuaException("Cannot queue main thread task") } @@ -38,11 +32,10 @@ class LuaTaskRunner : AbstractLuaTaskContext() { } override fun close() { + super.close() environment.shutdown() } - private class Event(val name: String?, val args: Array) - companion object { fun runTest(timeout: Duration = 5.seconds, fn: suspend LuaTaskRunner.() -> Unit) { runBlocking { diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java b/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java index 4adbfd3a9..4ca32a5db 100644 --- a/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java +++ b/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java @@ -8,8 +8,8 @@ import dan200.computercraft.core.apis.handles.ArrayByteChannel; import dan200.computercraft.core.apis.transfer.TransferredFile; import dan200.computercraft.core.apis.transfer.TransferredFiles; import dan200.computercraft.core.computer.Computer; -import dan200.computercraft.core.computer.ComputerEvents; -import dan200.computercraft.core.util.StringUtil; +import dan200.computercraft.core.input.EventComputerInput; +import dan200.computercraft.core.input.UserComputerInput; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWDropCallback; import org.lwjgl.glfw.GLFWKeyCallbackI; @@ -21,7 +21,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.BitSet; import java.util.List; /** @@ -35,23 +34,19 @@ public class InputState { private static final float KEY_SUPPRESS_DELAY = 0.2f; private final Computer computer; - private final BitSet keysDown = new BitSet(256); + private final UserComputerInput input; private float terminateTimer = -1; private float rebootTimer = -1; private float shutdownTimer = -1; - private int lastMouseButton = -1; - private int lastMouseX = -1; - private int lastMouseY = -1; - public InputState(Computer computer) { this.computer = computer; + this.input = new UserComputerInput(new EventComputerInput(computer), computer.getEnvironment().getTerminal()); } public void onCharEvent(int codepoint) { - var terminalChar = StringUtil.unicodeToTerminal(codepoint); - if (StringUtil.isTypableChar(terminalChar)) ComputerEvents.charTyped(computer, (byte) terminalChar); + input.codepointTyped(codepoint); } public void onKeyEvent(long window, int key, int action, int modifiers) { @@ -66,10 +61,7 @@ public class InputState { if (key == GLFW.GLFW_KEY_V && modifiers == GLFW.GLFW_MOD_CONTROL) { var string = GLFW.glfwGetClipboardString(window); - if (string != null) { - var clipboard = StringUtil.getClipboardString(string); - if (clipboard.remaining() > 0) ComputerEvents.paste(computer, clipboard); - } + if (string != null) input.paste(string); return; } @@ -88,19 +80,12 @@ public class InputState { } if (key >= 0 && terminateTimer < KEY_SUPPRESS_DELAY && rebootTimer < KEY_SUPPRESS_DELAY && shutdownTimer < KEY_SUPPRESS_DELAY) { - // Queue the "key" event and add to the down set - var repeat = keysDown.get(key); - keysDown.set(key); - ComputerEvents.keyDown(computer, key, repeat); + input.keyDown(key); } } private void keyReleased(int key) { - // Queue the "key_up" event and remove from the down set - if (key >= 0 && keysDown.get(key)) { - keysDown.set(key, false); - ComputerEvents.keyUp(computer, key); - } + input.keyUp(key); switch (key) { case GLFW.GLFW_KEY_T -> terminateTimer = -1; @@ -113,33 +98,17 @@ public class InputState { public void onMouseClick(int button, int action) { switch (action) { - case GLFW.GLFW_PRESS -> { - ComputerEvents.mouseClick(computer, button + 1, lastMouseX + 1, lastMouseY + 1); - lastMouseButton = button; - } - case GLFW.GLFW_RELEASE -> { - if (button == lastMouseButton) { - ComputerEvents.mouseUp(computer, button + 1, lastMouseX + 1, lastMouseY + 1); - lastMouseButton = -1; - } - } + case GLFW.GLFW_PRESS -> input.mouseClick(button + 1); + case GLFW.GLFW_RELEASE -> input.mouseUp(button + 1); } } public void onMouseMove(int mouseX, int mouseY) { - if (mouseX == lastMouseX && mouseY == lastMouseY) return; - - lastMouseX = mouseX; - lastMouseY = mouseY; - if (lastMouseButton != -1) { - ComputerEvents.mouseDrag(computer, lastMouseButton + 1, mouseX + 1, mouseY + 1); - } + input.mouseMove(mouseX + 1, mouseY + 1); } public void onMouseScroll(double yOffset) { - if (yOffset != 0) { - ComputerEvents.mouseScroll(computer, yOffset < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1); - } + if (yOffset != 0) input.mouseScroll(yOffset < 0 ? 1 : -1); } public void onFileDrop(int count, long names) { diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java index a49640730..3dc2a0440 100644 --- a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java +++ b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java @@ -313,8 +313,6 @@ public class Main { glfwSetCursorPosCallback(window, (w, x, y) -> { var charX = (int) (((x / SCALE) - MARGIN) / PIXEL_WIDTH); var charY = (int) (((y / SCALE) - MARGIN) / PIXEL_HEIGHT); - charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1); - charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1); inputState.onMouseMove(charX, charY); }); glfwSetScrollCallback(window, (w, xOffset, yOffset) -> inputState.onMouseScroll(yOffset)); diff --git a/projects/web/rollup.config.js b/projects/web/rollup.config.js index 6b0c83d4c..9125cc509 100644 --- a/projects/web/rollup.config.js +++ b/projects/web/rollup.config.js @@ -88,9 +88,9 @@ const ccTweaked = minify => { } }, + /** @type {import("rollup").ResolveIdHook} */ async resolveId(source) { - if (source === "cct/classes") return path.resolve("build/teaVM/classes.js"); - if (source === "cct/resources") return path.resolve("build/teaVM/resources.js"); + if (source.startsWith("cct/")) return path.resolve("build/teaVM/" + source.substring(4)); return null; }, @@ -124,7 +124,7 @@ export default args => ({ resolve({ browser: true }), url({ - include: ["**/*.dfpwm", "**/*.worker.js", "**/*.png"], + include: ["**/*.dfpwm", "**/*.worker.js", "**/*.png", "**/*.wasm"], fileName: "[name]-[hash][extname]", publicPath: "/", limit: 0, diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java index 6bcc97038..ea2524102 100644 --- a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java +++ b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -70,14 +71,24 @@ public class Builder { } // Then finally start the compiler! + run(output, remapper, TeaVMTargetType.JAVASCRIPT, TeaVMOptimizationLevel.ADVANCED, minify); + run(output, remapper, TeaVMTargetType.WEBASSEMBLY_GC, TeaVMOptimizationLevel.SIMPLE, minify); + + try (var runtime = Builder.class.getClassLoader().getResourceAsStream("org/teavm/backend/wasm/wasm-gc-module-runtime.js")) { + if (runtime == null) throw new IllegalStateException("Cannot find WASM runtime"); + Files.copy(runtime, output.resolve("wasm-gc-runtime.js"), StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void run(Path output, ClassLoader classes, TeaVMTargetType target, TeaVMOptimizationLevel optimise, boolean minify) throws Exception { var tool = new TeaVMTool(); - tool.setTargetType(TeaVMTargetType.JAVASCRIPT); + tool.setTargetType(target); tool.setJsModuleType(JSModuleType.ES2015); tool.setTargetDirectory(output.toFile()); - tool.setClassLoader(remapper); + tool.setClassLoader(classes); tool.setMainClass("cc.tweaked.web.Main"); - tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); + tool.setOptimizationLevel(optimise); tool.setObfuscated(minify); tool.generate(); diff --git a/projects/web/src/frontend/emu/java.ts b/projects/web/src/frontend/emu/java.ts index 7ced944bb..8419f7423 100644 --- a/projects/web/src/frontend/emu/java.ts +++ b/projects/web/src/frontend/emu/java.ts @@ -4,11 +4,31 @@ import "setimmediate"; -import type { ComputerDisplay, ComputerHandle } from "cct/classes"; -export type { ComputerDisplay, ComputerHandle, PeripheralKind, Side } from "cct/classes"; +import type { ComputerDisplay, ComputerHandle } from "cct/classes.js"; +export type { ComputerDisplay, ComputerHandle, PeripheralKind, Side } from "cct/classes.js"; +import { load as teaVMLoad } from "cct/wasm-gc-runtime.js"; +import { exceptions, gc } from "wasm-feature-detect"; +import wasmClasses from "cct/classes.wasm"; + +const loadClasses = async (): Promise<{ main: (args: string[]) => void }> => { + if ( + typeof WebAssembly === "object" && typeof WebAssembly.compileStreaming === "function" && + await exceptions() && await gc() + ) { + try { + console.log("Loading WASM runtime"); + return (await teaVMLoad(wasmClasses)).exports; + } catch (e) { + console.error("Failed to load WebAssembly runtime", e); + } + } + + console.log("Using JS runtime"); + return await import("cct/classes.js"); +} const load = async (): Promise<(computer: ComputerDisplay) => ComputerHandle> => { - const [classes, { version, resources }] = await Promise.all([import("cct/classes"), import("cct/resources")]); + const [classes, { version, resources }] = await Promise.all([loadClasses(), import("cct/resources.js")]); let addComputer: ((computer: ComputerDisplay) => ComputerHandle) | null = null; const encoder = new TextEncoder(); @@ -18,7 +38,7 @@ const load = async (): Promise<(computer: ComputerDisplay) => ComputerHandle> => listResources: () => Object.keys(resources), getResource: path => new Int8Array(encoder.encode(resources[path])) }; - classes.main(); + classes.main([]); if (!addComputer) throw new Error("Callbacks.setup was never called"); return addComputer; diff --git a/projects/web/src/frontend/index.tsx b/projects/web/src/frontend/index.tsx index 71172b917..19b4662c4 100644 --- a/projects/web/src/frontend/index.tsx +++ b/projects/web/src/frontend/index.tsx @@ -76,6 +76,7 @@ class Window extends Component { const elements = document.querySelectorAll("pre[data-lua-kind]"); for (let i = 0; i < elements.length; i++) { const element = elements[i] as HTMLElement; + if (element.hasAttribute("data-no-run")) continue let example = element.innerText; diff --git a/projects/web/src/frontend/typings.d.ts b/projects/web/src/frontend/typings.d.ts index eecd86f69..00e3ba934 100644 --- a/projects/web/src/frontend/typings.d.ts +++ b/projects/web/src/frontend/typings.d.ts @@ -28,16 +28,21 @@ declare module "*.license" { } declare module "*.dfpwm" { - const contents: string; - export default contents; + const url: string; + export default url; } -declare module "cct/resources" { +declare module "*.wasm" { + const url: string; + export default url; +} + +declare module "cct/resources.js" { export const version: string; export const resources: Record; } -declare module "cct/classes" { +declare module "cct/classes.js" { export const main: () => void; export type Side = "up" | "down" | "left" | "right" | "front" | "back"; @@ -169,10 +174,18 @@ declare module "cct/classes" { } } +declare module "cct/wasm-gc-runtime.js" { + export const load: (url: string, options?: any) => Promise<{ + exports: { main: (args: string[]) => void }, + instance: WebAssembly.Instance, + module: WebAssembly.Module, + }> +} + declare namespace JSX { export type Element = import("preact").JSX.Element; export type IntrinsicElements = import("preact").JSX.IntrinsicElements; export type ElementClass = import("preact").JSX.ElementClass; } -declare var $javaCallbacks: import("cct/classes").Callbacks; // eslint-disable-line no-var +declare var $javaCallbacks: import("cct/classes.js").Callbacks; // eslint-disable-line no-var diff --git a/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java b/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java index 846833bc1..c9a80a958 100644 --- a/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java +++ b/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.teavm.jso.JSObject; import org.teavm.jso.core.JSString; import org.teavm.jso.typedarrays.ArrayBuffer; +import org.teavm.jso.typedarrays.Int8Array; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -161,7 +162,7 @@ class EmulatedComputer implements ComputerEnvironment, ComputerHandle { public void transferFiles(FileContents[] files) { computer.queueEvent(TransferredFiles.EVENT, new Object[]{ new TransferredFiles( Arrays.stream(files) - .map(x -> new TransferredFile(x.getName(), new ArrayByteChannel(bytesOfBuffer(x.getContents())))) + .map(x -> new TransferredFile(x.getName(), new ArrayByteChannel(new Int8Array(x.getContents()).copyToJavaArray()))) .toList() ) }); } @@ -186,8 +187,8 @@ class EmulatedComputer implements ComputerEnvironment, ComputerHandle { @Override public void addFile(String path, JSObject contents) { byte[] bytes; - if (JavascriptConv.isArrayBuffer(contents)) { - bytes = bytesOfBuffer((ArrayBuffer) contents); + if (contents instanceof ArrayBuffer buffer) { + bytes = new Int8Array(buffer).copyToJavaArray(); } else { var string = (JSString) contents; bytes = string.stringValue().getBytes(StandardCharsets.UTF_8); @@ -195,9 +196,4 @@ class EmulatedComputer implements ComputerEnvironment, ComputerHandle { mount.addFile(path, bytes); } - - private byte[] bytesOfBuffer(ArrayBuffer buffer) { - var oldBytes = JavascriptConv.asByteArray(buffer); - return Arrays.copyOf(oldBytes, oldBytes.length); - } } diff --git a/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java b/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java index 5eb09c490..0f17e4415 100644 --- a/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java +++ b/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java @@ -54,7 +54,7 @@ public class Callbacks { * @param resource The path to the resource to load. * @return The loaded resource. */ - @JSByRef + @JSByRef(optional = true) @JSBody(params = "name", script = "return $javaCallbacks.getResource(name);") public static native byte[] getResource(String resource); diff --git a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java index 46216c579..bfa32d85e 100644 --- a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java +++ b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java @@ -6,17 +6,10 @@ package cc.tweaked.web.js; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; -import org.teavm.jso.JSBody; -import org.teavm.jso.JSByRef; import org.teavm.jso.JSObject; import org.teavm.jso.core.JSBoolean; import org.teavm.jso.core.JSNumber; -import org.teavm.jso.core.JSObjects; import org.teavm.jso.core.JSString; -import org.teavm.jso.typedarrays.ArrayBuffer; -import org.teavm.jso.typedarrays.Int8Array; - -import java.nio.ByteBuffer; /** * Utility methods for converting between Java and Javascript representations. @@ -44,46 +37,9 @@ public class JavascriptConv { */ public static @Nullable Object toJava(@Nullable JSObject value) { if (value == null) return null; - return switch (JSObjects.typeOf(value)) { - case "string" -> ((JSString) value).stringValue(); - case "number" -> ((JSNumber) value).doubleValue(); - case "boolean" -> ((JSBoolean) value).booleanValue(); - default -> null; - }; - } - - /** - * Check if an arbitrary object is a {@link ArrayBuffer}. - * - * @param object The object ot check - * @return Whether this is an {@link ArrayBuffer}. - */ - @JSBody(params = "data", script = "return data instanceof ArrayBuffer;") - public static native boolean isArrayBuffer(JSObject object); - - /** - * Wrap a JS {@link Int8Array} into a {@code byte[]}. - * - * @param view The array to wrap. - * @return The wrapped array. - */ - @JSByRef - @JSBody(params = "x", script = "return x;") - public static native byte[] asByteArray(Int8Array view); - - /** - * Wrap a JS {@link ArrayBuffer} into a {@code byte[]}. - * - * @param view The array to wrap. - * @return The wrapped array. - */ - public static byte[] asByteArray(ArrayBuffer view) { - return asByteArray(new Int8Array(view)); - } - - public static Int8Array toArray(ByteBuffer buffer) { - var array = new Int8Array(buffer.remaining()); - for (var i = 0; i < array.getLength(); i++) array.set(i, buffer.get(i)); - return array; + if (value instanceof JSString v) return v.stringValue(); + if (value instanceof JSNumber v) return v.doubleValue(); + if (value instanceof JSBoolean v) return v.booleanValue(); + return null; } } diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java index 38d5ef9ed..7ee0fecc5 100644 --- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java @@ -5,7 +5,6 @@ package dan200.computercraft.core.apis.http.request; import cc.tweaked.web.Main; -import cc.tweaked.web.js.JavascriptConv; import dan200.computercraft.core.Logging; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.handles.ArrayByteChannel; @@ -21,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.teavm.jso.ajax.XMLHttpRequest; import org.teavm.jso.typedarrays.ArrayBuffer; +import org.teavm.jso.typedarrays.Int8Array; import java.net.URI; import java.net.URISyntaxException; @@ -102,7 +102,7 @@ public class THttpRequest extends Resource { request.setRequestHeader(header.getKey(), header.getValue()); } request.setRequestHeader("X-CC-Redirect", followRedirects ? "true" : "false"); - request.send(postBuffer == null ? null : JavascriptConv.toArray(postBuffer)); + request.send(postBuffer == null ? null : Int8Array.fromJavaBuffer(postBuffer)); checkClosed(); } catch (Exception e) { failure("Could not connect"); @@ -118,7 +118,7 @@ public class THttpRequest extends Resource { } var buffer = (ArrayBuffer) request.getResponse(); - SeekableByteChannel contents = new ArrayByteChannel(JavascriptConv.asByteArray(buffer)); + SeekableByteChannel contents = new ArrayByteChannel(new Int8Array(buffer).copyToJavaArray()); var reader = new ReadHandle(contents, binary); Map responseHeaders = new HashMap<>(); diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java index f9154cc5f..29f3890a0 100644 --- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java @@ -5,7 +5,6 @@ package dan200.computercraft.core.apis.http.websocket; import cc.tweaked.web.js.Console; -import cc.tweaked.web.js.JavascriptConv; import com.google.common.base.Strings; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.Resource; @@ -14,11 +13,13 @@ import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.Options; import io.netty.handler.codec.http.HttpHeaders; import org.jspecify.annotations.Nullable; +import org.teavm.jso.typedarrays.ArrayBuffer; import org.teavm.jso.typedarrays.Int8Array; import org.teavm.jso.websocket.WebSocket; import java.net.URI; import java.nio.ByteBuffer; +import java.util.Map; /** * Replaces {@link Websocket} with a version which uses Javascript's built-in {@link WebSocket} client. @@ -49,10 +50,8 @@ public class TWebsocket extends Resource implements WebsocketClient }); client.onMessage(e -> { if (isClosed()) return; - if (JavascriptConv.isArrayBuffer(e.getData())) { - var array = new Int8Array(e.getDataAsArray()); - var contents = new byte[array.getLength()]; - for (var i = 0; i < contents.length; i++) contents[i] = array.get(i); + if (e.getData() instanceof ArrayBuffer buffer) { + var contents = new Int8Array(buffer).copyToJavaArray(); environment.queueEvent("websocket_message", address, contents, true); } else { environment.queueEvent("websocket_message", address, e.getDataAsString(), false); @@ -70,7 +69,7 @@ public class TWebsocket extends Resource implements WebsocketClient @Override public void sendBinary(ByteBuffer message) { if (websocket == null) return; - websocket.send(JavascriptConv.toArray(message)); + websocket.send(Int8Array.fromJavaBuffer(message)); } @Override @@ -85,7 +84,7 @@ public class TWebsocket extends Resource implements WebsocketClient private void success(Options options) { if (isClosed()) return; - var handle = new WebsocketHandle(environment, address, this, options); + var handle = new WebsocketHandle(environment, address, this, Map.of(), options); environment.queueEvent(SUCCESS_EVENT, address, handle); createOwnerReference(handle);