mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2026-05-20 04:22:07 +00:00
Merge branch 'mc-1.21.x' into mc-1.21.y
There's definitely some more work to be done here — I need a datafixer to move pocket upgrades from the bottom to the top — but it otherwise seems to work.
This commit is contained in:
@@ -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<VersionCatalogsExtension>().named("libs")
|
||||
// Minecraft depends on asm, but Fabric forces it to a more recent version
|
||||
override(libs.findLibrary("asm").get(), "9.9")
|
||||
}
|
||||
|
||||
@@ -77,13 +77,8 @@ class IlluaminatePlugin : Plugin<Project> {
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-63
@@ -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
|
||||
<img alt="An example GPS constellation." src="../images/gps-constellation-example.png" class="big-image" />
|
||||
- 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 <kbd>F3+G</kbd> 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.
|
||||
<img alt="An empty 10x10x10 area, with the axis marked with smooth stone." src="../images/gps-constellation-area.png" class="big-image" />
|
||||
|
||||
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:
|
||||
|
||||
<img alt="The same area as before, but with a computer in each corner." src="../images/gps-constellation-built.png" class="big-image" />
|
||||
|
||||
## 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 <kbd>F3</kbd> 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 <kbd>Ctrl+R</kbd> or run the `reboot` program) to run the startup
|
||||
program.
|
||||
|
||||
Escape from the computer GUI and then press <kbd>F3</kbd> 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 <kbd>F3</kbd> 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 <kbd>F3</kbd> 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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 346 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 331 KiB |
@@ -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.
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# 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"
|
||||
@@ -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.
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# 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"
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Generated
+278
-275
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -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",
|
||||
|
||||
@@ -98,6 +98,7 @@ public final class ClientRegistry {
|
||||
public static void registerMenuScreens(RegisterMenuScreen register) {
|
||||
register.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
|
||||
register.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>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);
|
||||
|
||||
+6
-4
@@ -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<T extends AbstractComputerMenu> 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<T extends AbstractComputerMenu> 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<T extends AbstractComputerMenu> 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);
|
||||
}
|
||||
|
||||
|
||||
+38
@@ -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));
|
||||
}
|
||||
}
|
||||
+5
-26
@@ -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.
|
||||
* <p>
|
||||
* 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));
|
||||
@@ -32,7 +32,10 @@ public final class ComputerScreen<T extends AbstractComputerMenu> 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
|
||||
|
||||
+6
-4
@@ -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<T extends AbstractComputerMenu> extends Screen implements MenuAccess<T> {
|
||||
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<T extends AbstractComputerMenu> 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<T extends AbstractComputerMenu> 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);
|
||||
|
||||
+128
@@ -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.
|
||||
* <p>
|
||||
* This extends {@link NoTermComputerScreen}, but with support for interacting with the lectern's pocket computer.
|
||||
*/
|
||||
public final class PocketComputerLecternScreen extends NoTermComputerScreen<PocketComputerLecternMenu> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,10 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
|
||||
@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
|
||||
|
||||
+7
-7
@@ -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<AbstractWidget> add, int x, int y) {
|
||||
public static void addButtons(BooleanSupplier isOn, ClientComputerActions actions, Consumer<AbstractWidget> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-84
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+45
-10
@@ -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<CustomLecternB
|
||||
return new State();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update our {@link PoseStack} for rendering the lectern's contents.
|
||||
*
|
||||
* @param poseStack The pose stack to update.
|
||||
* @param state The lectern block state.
|
||||
*/
|
||||
public static void applyLecternTransform(PoseStack poseStack, BlockState state) {
|
||||
poseStack.translate(0.5f, 1.0625f, 0.5f);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(-state.getValue(LecternBlock.FACING).getClockWise().toYRot()));
|
||||
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5f));
|
||||
poseStack.translate(0, -0.125f, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(CustomLecternBlockEntity lectern, State state, float f, Vec3 camera, ModelFeatureRenderer.@Nullable CrumblingOverlay overlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(lectern, state, f, camera, overlay);
|
||||
@@ -89,10 +104,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
@Override
|
||||
public void submit(State state, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState cameraRenderState) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5f, 1.0625f, 0.5f);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(-state.blockState.getValue(LecternBlock.FACING).getClockWise().toYRot()));
|
||||
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5f));
|
||||
poseStack.translate(0, -0.125f, 0);
|
||||
applyLecternTransform(poseStack, state.blockState);
|
||||
|
||||
if (state.type == Type.PRINTOUT) {
|
||||
if (state.isBook) {
|
||||
@@ -111,10 +123,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
} else if (state.type == Type.POCKET_COMPUTER) {
|
||||
pocketModel.submit(poseStack, collector, materials, state.lightCoords, state.pocketFamily, state.pocketColour, state.pocketLight);
|
||||
|
||||
// Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(90f));
|
||||
poseStack.translate(-0.5 * LecternPocketModel.TERM_WIDTH, 0.5 * LecternPocketModel.TERM_HEIGHT + 1f / 32.0f, 1 / 16.0f);
|
||||
poseStack.mulPose(Axis.XP.rotationDegrees(180));
|
||||
applyPocketComputerTerminalTransform(poseStack);
|
||||
|
||||
// Either render the terminal or a black screen.
|
||||
if (state.pocketTerminal != null) {
|
||||
@@ -127,7 +136,27 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private static void renderPocketTerminal(PoseStack poseStack, SubmitNodeCollector collector, Terminal terminal) {
|
||||
/**
|
||||
* Update our {@link PoseStack} for rendering the pocket computer terminal. This jiggles the terminal about a bit,
|
||||
* so {@code (0, 0)} is in the top left of the model's terminal hole.
|
||||
*
|
||||
* @param poseStack The pose stack to update.
|
||||
*/
|
||||
public static void applyPocketComputerTerminalTransform(PoseStack poseStack) {
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(90f));
|
||||
poseStack.translate(-0.5 * LecternPocketModel.TERM_WIDTH, 0.5 * LecternPocketModel.TERM_HEIGHT + 1f / 32.0f, 1 / 16.0f);
|
||||
poseStack.mulPose(Axis.XP.rotationDegrees(180));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update our {@link PoseStack} for rendering the pocket computer terminal itself. This scales us to work in
|
||||
* terminal space, and translates so that {@code (0, 0)} is the top left of the pocket terminal.
|
||||
*
|
||||
* @param poseStack The pose stack to update.
|
||||
* @param terminal The current terminal.
|
||||
* @return The margins of the terminal.
|
||||
*/
|
||||
public static Vector2f applyScaledPocketComputerTerminalTransform(PoseStack poseStack, Terminal terminal) {
|
||||
var width = terminal.getWidth() * FONT_WIDTH;
|
||||
var height = terminal.getHeight() * FONT_HEIGHT;
|
||||
|
||||
@@ -140,9 +169,15 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
// Convert the model dimensions to terminal space, then find out how large the margin should be.
|
||||
var marginX = ((LecternPocketModel.TERM_WIDTH / scale) - width) / 2;
|
||||
var marginY = ((LecternPocketModel.TERM_HEIGHT / scale) - height) / 2;
|
||||
poseStack.translate(marginX, marginY, 0);
|
||||
|
||||
return new Vector2f(marginX, marginY);
|
||||
}
|
||||
|
||||
private static void renderPocketTerminal(PoseStack poseStack, SubmitNodeCollector collector, Terminal terminal) {
|
||||
var margin = applyScaledPocketComputerTerminalTransform(poseStack, terminal);
|
||||
collector.submitCustomGeometry(poseStack, FixedWidthFontRenderer.TERMINAL_TEXT, (pose, buffer) ->
|
||||
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 {
|
||||
|
||||
@@ -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<MenuType<ComputerMenuWithoutInventory>> 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<MenuType<PocketComputerLecternMenu>> POCKET_COMPUTER_LECTERN = REGISTRY.register("pocket_computer_lectern",
|
||||
() -> ContainerData.toType(PocketComputerLecternMenu.Data.STREAM_CODEC, PocketComputerLecternMenu::new));
|
||||
|
||||
public static final RegistryEntry<MenuType<TurtleMenu>> TURTLE = REGISTRY.register("turtle",
|
||||
() -> ContainerData.toType(ComputerContainerData.STREAM_CODEC, TurtleMenu::ofMenuData));
|
||||
|
||||
|
||||
+2
-3
@@ -255,9 +255,8 @@ public class CommandAPI implements ILuaAPI {
|
||||
/**
|
||||
* Get some basic information about a block.
|
||||
* <p>
|
||||
* 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.
|
||||
|
||||
+7
@@ -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<T extends AbstractComputerBlockEntit
|
||||
return super.useWithoutItem(state, level, pos, player, hit);
|
||||
}
|
||||
|
||||
@ForgeOverride
|
||||
public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) {
|
||||
var be = world.getBlockEntity(pos);
|
||||
if (be instanceof AbstractComputerBlockEntity computer) computer.neighborBlockEntityChanged(neighbour);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void neighborChanged(BlockState blockState, Level level, BlockPos blockPos, Block block, @Nullable Orientation orientation, boolean isMoving) {
|
||||
if (level.getBlockEntity(blockPos) instanceof AbstractComputerBlockEntity computer) computer.neighborChanged();
|
||||
|
||||
+21
@@ -300,6 +300,27 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
invalidSides = DirectionUtil.ALL_SIDES; // Mark all peripherals as dirty.
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a neighbour block entity changes.
|
||||
* <p>
|
||||
* This is only required for MoreRed, which does not fire block updates when bundled redstone changes, see
|
||||
* <a href="https://github.com/cc-tweaked/CC-Tweaked/issues/2316">#2316</a>
|
||||
*
|
||||
* @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.
|
||||
* <p>
|
||||
|
||||
-42
@@ -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();
|
||||
}
|
||||
+7
-3
@@ -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<MetricsObserver> 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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ public abstract class AbstractComputerMenu extends AbstractContainerMenu impleme
|
||||
private final ContainerData data;
|
||||
|
||||
private final @Nullable ServerComputer computer;
|
||||
private final @Nullable ServerInputState<AbstractComputerMenu> 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();
|
||||
|
||||
+10
-3
@@ -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.
|
||||
*
|
||||
|
||||
+15
-107
@@ -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}.
|
||||
* <p>
|
||||
* This keeps track of the current key and mouse state, and releases them when the container is closed.
|
||||
*
|
||||
* @param <T> The type of container this server input belongs to.
|
||||
*/
|
||||
public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> 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<FileUpload> 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<FileUpload> files) {
|
||||
toUploadId = uuid;
|
||||
public void startUpload(UUID uploadId, List<FileUpload> files) {
|
||||
toUploadId = uploadId;
|
||||
toUpload = files;
|
||||
}
|
||||
|
||||
@@ -162,7 +78,6 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> 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<T extends AbstractContainerMenu & ComputerMenu> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class BlockDetails {
|
||||
|
||||
public static void fill(Map<? super String, Object> data, BlockReference block) {
|
||||
data.put("tags", DetailHelpers.getTags(block.state().getTags()));
|
||||
DetailHelpers.fillMapColour(data, block.level(), block.pos(), block.state());
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
|
||||
@@ -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 <T> String getId(Registry<T> registry, T entry) {
|
||||
return RegistryHelper.getKeyOrThrow(registry, entry).toString();
|
||||
}
|
||||
|
||||
public static void fillMapColour(Map<? super String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Map<String, Object>> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
|
||||
+43
-2
@@ -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<IPeripheral> 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
@@ -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}.
|
||||
* <p>
|
||||
* 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<RegistryFriendlyByteBuf, Data> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+3
-3
@@ -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<RegistryFriendlyByteBuf, PasteEventComputerMessage> 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
|
||||
|
||||
+5
-16
@@ -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<T> implements GenericPeripheral {
|
||||
/**
|
||||
* List all items in this inventory. This returns a table, with an entry for each slot.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
@@ -66,23 +64,13 @@ public abstract class AbstractInventoryMethods<T> implements GenericPeripheral {
|
||||
* print(("%d x %s in slot %d"):format(item.count, item.name, slot))
|
||||
* end
|
||||
* }</pre>
|
||||
* @cc.see item_details
|
||||
*/
|
||||
@LuaFunction(mainThread = true)
|
||||
public abstract Map<Integer, Map<String, ?>> list(T inventory);
|
||||
|
||||
/**
|
||||
* Get detailed information about an item.
|
||||
* <p>
|
||||
* 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`).
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<T> implements GenericPeripheral {
|
||||
* print(("Damage: %d/%d"):format(item.damage, item.maxDamage))
|
||||
* end
|
||||
* }</pre>
|
||||
* @cc.see item_details
|
||||
*/
|
||||
@Nullable
|
||||
@LuaFunction(mainThread = true)
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+9
@@ -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
|
||||
// <a href="https://github.com/cc-tweaked/CC-Tweaked/issues/2316">#2316</a>.
|
||||
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();
|
||||
|
||||
+7
-5
@@ -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) {
|
||||
|
||||
+46
-45
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+13
-11
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 <pre>{@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,
|
||||
* -- }
|
||||
* }</pre>
|
||||
* @see AbstractInventoryMethods#getItemDetail Describes the information returned by a detailed query.
|
||||
* @cc.see item_details
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult getItemDetail(ILuaContext context, Optional<Integer> slot, Optional<Boolean> detailed) throws LuaException {
|
||||
|
||||
+3
-1
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
+66
@@ -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<Map<? extends String, ?>> containsEntry(String key, Object value) {
|
||||
return CustomMatchers.containsEntry(key, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Matcher<Map<? extends String, ?>> containsEntryWith(String key, Matcher<?> value) {
|
||||
return CustomMatchers.containsEntryWith(key, (Matcher<? super Object>) value);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> args) throws LuaException {
|
||||
return switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) {
|
||||
public final int day(Optional<String> 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 {
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final long epoch(Optional<String> 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<String> 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");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
+18
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -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<Websocket> 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<Websocket> 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<Websocket> implements WebsocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
void success(Options options) {
|
||||
void success(HttpHeaders responseHeaders, Options options) {
|
||||
if (isClosed()) return;
|
||||
|
||||
var handle = new WebsocketHandle(environment, address, this, options);
|
||||
Map<String, String> 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);
|
||||
|
||||
|
||||
+34
-3
@@ -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<String, String> responseHeaders;
|
||||
private final Options options;
|
||||
|
||||
public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Options options) {
|
||||
public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Map<String, String> 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<Double> 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.
|
||||
* <pre>{@code
|
||||
* local ws = http.websocket("wss://example.tweaked.cc/echo")
|
||||
* print(textutils.serialize(ws.getResponseHeaders()))
|
||||
* -- => {
|
||||
* -- Connection = "Upgrade",
|
||||
* -- Upgrade = "websocket",
|
||||
* -- ...
|
||||
* -- }
|
||||
* ws.close()
|
||||
* }</pre>
|
||||
* @since 1.107.0
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Map<String, String> 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;
|
||||
|
||||
+7
-4
@@ -18,10 +18,12 @@ import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESS
|
||||
class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
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<Object> {
|
||||
@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<Object> {
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
ctx.close();
|
||||
|
||||
fail(NetworkUtils.toFriendlyError(cause));
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private void fail(String message) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
};
|
||||
|
||||
@@ -585,7 +585,7 @@ class TerminalTest {
|
||||
}
|
||||
}
|
||||
|
||||
public Matcher<Terminal> matches() {
|
||||
private Matcher<Terminal> matches() {
|
||||
return allOf(
|
||||
textMatches(textLines), textColourMatches(textColourLines), backgroundColourMatches(backgroundColourLines)
|
||||
);
|
||||
|
||||
@@ -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<Channel>) {
|
||||
/** 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<SocketChannel>() {
|
||||
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<Channel>()
|
||||
try {
|
||||
run { workerGroup.shutdownGracefully() }
|
||||
val ch = ServerBootstrap()
|
||||
.group(workerGroup)
|
||||
.channel(NioServerSocketChannel::class.java)
|
||||
.childHandler(
|
||||
object : ChannelInitializer<SocketChannel>() {
|
||||
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<FullHttpRequest>()
|
||||
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<FullHttpRequest>()
|
||||
/**
|
||||
* A basic WS server which just sends back the original message.
|
||||
*/
|
||||
private class WebSocketFrameHandler : SimpleChannelInboundHandler<WebSocketFrame>() {
|
||||
private class WebSocketFrameHandler(private val activeConnections: MutableSet<Channel>) : SimpleChannelInboundHandler<WebSocketFrame>() {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, frame: WebSocketFrame) {
|
||||
if (frame is TextWebSocketFrame) {
|
||||
// Send the uppercase string back.
|
||||
@@ -126,10 +135,16 @@ private class WebSocketFrameHandler : SimpleChannelInboundHandler<WebSocketFrame
|
||||
|
||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||
if (evt is HandshakeComplete) {
|
||||
// Channel upgrade to websocket, remove WebSocketIndexPageHandler.
|
||||
// Channel upgrade to websocket, remove HttpServerHandler.
|
||||
ctx.pipeline().remove(HttpServerHandler::class.java)
|
||||
activeConnections.add(ctx.channel())
|
||||
} else {
|
||||
super.userEventTriggered(ctx, evt)
|
||||
}
|
||||
}
|
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||
super.channelInactive(ctx)
|
||||
activeConnections.remove(ctx.channel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.CoreConfig
|
||||
import dan200.computercraft.core.apis.HTTPAPI
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle
|
||||
import dan200.computercraft.core.apis.http.HttpServer.URL
|
||||
import dan200.computercraft.core.apis.http.HttpServer.WS_URL
|
||||
import dan200.computercraft.core.apis.http.HttpServer.runServer
|
||||
import dan200.computercraft.core.apis.http.HttpServer.Companion.runServer
|
||||
import dan200.computercraft.core.apis.http.options.Action
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule
|
||||
import dan200.computercraft.core.apis.http.request.HttpResponseHandle
|
||||
import dan200.computercraft.core.apis.http.websocket.WebsocketHandle
|
||||
import dan200.computercraft.test.core.computer.LuaTaskRunner
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.*
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
@@ -50,13 +50,14 @@ class TestHttpApi {
|
||||
|
||||
@Test
|
||||
fun `Connects to a HTTP server`() {
|
||||
runServer {
|
||||
runServer { server ->
|
||||
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<LuaException> {
|
||||
@@ -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<LuaException>("Throws an exception when sending") {
|
||||
|
||||
@@ -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")
|
||||
|
||||
+31
@@ -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 <T> Matcher<Iterable<? extends T>> containsWith(List<T> items, Function<T, Matcher<? super T>> 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 <K> The type of keys in the map.
|
||||
* @param <V> The type of values in the map.
|
||||
* @return A matcher that projects out of a map.
|
||||
*/
|
||||
public static <K, V> Matcher<Map<? extends K, ? extends V>> 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 <K> The type of keys in the map.
|
||||
* @param <V> The type of values in the map.
|
||||
* @return A matcher that projects out of a map.
|
||||
*/
|
||||
public static <K, V> Matcher<Map<? extends K, ? extends V>> containsEntryWith(K key, Matcher<? super V> value) {
|
||||
return ContramapMatcher.contramap(value, new StringDescription().appendValue(key).toString(), x -> x.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
+31
-22
@@ -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 <reified T : ILuaAPI> LuaTaskContext.getApi(): T = getApi(T::class.java)
|
||||
|
||||
abstract class AbstractLuaTaskContext : LuaTaskContext, AutoCloseable {
|
||||
private val pullEvents = mutableListOf<PullEvent>()
|
||||
private val isReceiving = AtomicBoolean(false)
|
||||
private val eventStream: Channel<Event> = Channel(Channel.UNLIMITED)
|
||||
private val apis = mutableMapOf<Class<out ILuaAPI>, 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<out Any?>?) {
|
||||
val fullEvent: Array<out Any?> = 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 <T : ILuaAPI> getApi(api: Class<T>): T =
|
||||
api.cast(apis[api] ?: throw IllegalStateException("No API of type ${api.name}"))
|
||||
|
||||
final override suspend fun pullEvent(event: String?): Array<out Any?> =
|
||||
suspendCancellableCoroutine { cont -> pullEvents.add(PullEvent(event, cont)) }
|
||||
final override suspend fun pullEvent(event: String?): Array<out Any?> {
|
||||
if (!isReceiving.compareAndSet(false, true)) {
|
||||
throw IllegalStateException("Multiple listeners not currently supported")
|
||||
}
|
||||
|
||||
private class PullEvent(val name: String?, val cont: CancellableContinuation<Array<out Any?>>)
|
||||
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<out Any?>?) {
|
||||
val full: Array<out Any?>
|
||||
get() = if (args == null) arrayOf(name) else arrayOf(name, *args)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -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<Event> = Channel(Channel.UNLIMITED)
|
||||
private val apis = mutableListOf<ILuaAPI>()
|
||||
|
||||
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<out Any?>)
|
||||
|
||||
companion object {
|
||||
fun runTest(timeout: Duration = 5.seconds, fn: suspend LuaTaskRunner.() -> Unit) {
|
||||
runBlocking {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -76,6 +76,7 @@ class Window extends Component<WindowProps, WindowState> {
|
||||
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;
|
||||
|
||||
|
||||
+18
-5
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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<THttpRequest> {
|
||||
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<THttpRequest> {
|
||||
}
|
||||
|
||||
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<String, String> responseHeaders = new HashMap<>();
|
||||
|
||||
+6
-7
@@ -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<TWebsocket> 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<TWebsocket> 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<TWebsocket> 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user