1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-03 15:13:07 +00:00

Compare commits

..

29 Commits

Author SHA1 Message Date
Jonathan Coates
3f8c3b026a Merge branch 'mc-1.20.x' into mc-1.21.x 2025-02-14 20:44:39 +00:00
Jonathan Coates
0a8d505323 Bump CC:T to 1.115.0 2025-02-14 20:20:30 +00:00
Jonathan Coates
237a0ac3bb Expose printout contents to the API
Closes #2099
2025-02-14 18:13:20 +00:00
Jonathan Coates
b185d088b3 Suggest alternative table keys on nil errors (#2097)
We now suggest alternative table keys when code errors with "attempt
to index/call 'foo' (a nil value)". For example: "redstone.getinput()",
will now suggest "Did you mean: getInput".

This is a bit tricky to get right! In the above example, our code reads
like:

   1    GETTABUP 0 0 0 ; r0 := _ENV["redstone"]
   2    GETFIELD 0 0 1 ; r0 := r0["getinput"]
   3    CALL 0 1 1     ; r0()

Note, that when we get to the problematic line, we don't have access to
the original table that we attempted to index. In order to do this, we
borrow ideas from Lua's getobjname — we effectively write an evaluator
that walks back over the code and tries to reconstruct the expression
that resulted in nil.

For example, in the above case:
 - We know an instruction happened at pc=3, so we try to find the
   expression that computed r0.
 - We know this was set at pc=2, so we step back one. This is a GETFIELD
   instruction, so we check the key (it's a constant, so worth
   reporting), and then try to evaluate the table.
 - This version of r0 was set at pc=1, so we step back again. It's a
   GETTABUP instruction, so we can just evaluate that directly.

We then use this information (indexing _ENV.redstone with "getinput") to
find alternative keys (e.g. getInput, getOutput, etc...) and then pick
some likely suggestions with Damerau-Levenshtein/OSD.

I'm not entirely thrilled by the implementation here. The core
interpretation logic is implemented in Java. Which is *fine*, but a)
feels a little cheaty and b) means we're limited to what Lua bytecode
can provide (for instance, we can't inspect outer functions, or list all
available names in scope). We obviously can expand the bytecode if
needed, but something we'd want to be careful with.

The alternative approach would be to handle all the parsing in
Lua. Unfortunately, this is quite hard to get right — I think we'd need
some lazy parsing strategy to avoid constructing the whole AST, while
still retaining all the scope information we need.

I don't know. We really could make this as complex as we like, and I
don't know what the right balance is. It'd be cool to detect patterns
like the following, but is it *useful*?

    local monitor = peripheral.wrap("left")
    monitor.write("Hello")
        -- ^ monitor is nil. Is there a peripheral to the left of the
        -- computer?

For now, the current approach feels the easiest, and should allow us to
prototype things and see what does/doesn't work.
2025-02-13 21:57:29 +00:00
Jonathan Coates
051c70a731 Propagate exceptions from parallel where possible (#2095)
In the original implementation of our prettier runtime errors (#1320), we
wrapped the errors thrown within parallel functions into an exception
object. This means the call-stack is available to the catching-code, and
so is able to report a pretty exception message.

Unfortunately, this was a breaking change, and so we had to roll that
back. Some people were pcalling the parallel function, and matching on
the result of the error.

This is a second attempt at this, using a technique I've affectionately
dubbed "magic throws". The parallel API is now aware of whether it is
being pcalled or not, and thus able to decide whether to wrap the error
into an exception or not:

 - Add a new `cc.internal.tiny_require` module. This is a tiny
   reimplementation of require, for use in our global APIs.

 - Add a new (global, in the debug registry) `cc_try_barrier` function.
   This acts as a marker function, and is used to store additional
   information about the current coroutine.

   Currently this stores the parent coroutine (used to walk the full call
   stack) and a cache of whether any `pcall`-like function is on the
   stack.

   Both `parallel` and `cc.internal.exception.try` add this function to
   the root of the call stack.

 - When an error occurs within `parallel`, we walk up the call stack,
   using `cc_try_barrier` to traverse up the parent coroutine's stack
   too. If we do not find any `pcall`-like functions, then we know the
   error is never intercepted by user code, and so its safe to throw a
   full exception.
2025-02-13 17:38:57 +00:00
Jonathan Coates
2e2f308ff3 Support placing pocket computers on lecterns (#2098)
This allows shift+clicking a pocket computer on to a lectern. These
computers can be right clicked, opening the no-term computer GUI.
Terminal contents is rendered in-world, and broadcast to everyone in
range.

 - Add a new lectern PocketHolder.
 - Refactor some of the `PocketItemComputer` code to allow ticking pocket
   computers from a non-player/entity source.
 - Add a new model for pocket computers. This requires several new
   textures (somewhat mirroring the item ones), which is a little
   unfortunate, but looks much better than reusing the map renderer or
   item form.
2025-02-13 17:31:08 +00:00
Jonathan Coates
0f123b5efd Ignore unrepresentable characters when typing
In 94ad6dab0e, we changed it so typing
characters outside of CC's codepage were replaced with '?' rather than
ignored. This can be quite annoying for non-European users (where latin1
isn't very helpful!), so it makes sense to revert this change.

See discussion in #860 for more context.
2025-02-12 18:45:40 +00:00
Jonathan Coates
1278246cf7 Add back MoreRed support
I removed this in fc834cd97f, way back in
late 2024. Looks like it's been updating in the meantime and I hadn't
noticed, so add it back.

I've simplified the code a little bit, to make use of our NeoForge's new
capability system, but otherwise it's almost exactly the same :D.
2025-02-12 13:40:58 +00:00
Jonathan Coates
88cb03be6b Clean up the parallel API
- Store the filter alongside the coroutine rather than in a separate
   table (like we do in multishell).

 - Remove the redudant (I think!) second loop that checks for dead
   coroutines. We already check for dead coroutines in the main loop.

 - Rename some variables to be a bit more consistent. This makes this
   commit look noisier than it is. Sorry!
2025-02-09 16:53:59 +00:00
Jonathan Coates
1e25fa9bc3 Bump CC:T to 1.114.5 2025-02-09 10:16:45 +00:00
Jonathan Coates
74f707aaea Create a new CraftingInput for each craft
It's not actually safe to reuse this, as we need to recompute the
internal StackedContents each time the inventory changes, otherwise
ShapelessRecipe.matches will continue to return true, even if the actual
inventory doesn't include the required items.

Fixes #2094
2025-02-07 09:45:14 +00:00
Jonathan Coates
9bb62b047a Merge branch 'mc-1.20.x' into mc-1.21.x 2025-01-31 21:41:15 +00:00
Jonathan Coates
4360485880 Bump CC:T to 1.114.4 2025-01-31 21:09:57 +00:00
Jonathan Coates
b69a44a927 Redo turtle move checks
Oh, this is so broken, and really has been since the 1.13 update, if not
earlier.

 - Fix call to isUnobstructed using the bounding box of the
   *destination* block rather than the turtle. This is almost always
   air, so the box is empty.

 - Because the above check has been wrong for so many years, we now
   significantly relax the "can push" checks for entities. We now allow
   pushing entities in any direction.

   We also remove the "isUnobstructed" check for the destination entity
   pos. This causes problems (if two entities are standing on a turtle,
   they'll obstruct each other), and given pistons don't perform such a
   check, I don't think we need it.

 - Also do a bit of cleanup around air/liquid checks. We often ended up
   reading the block state multiple times, which is a little ugly.
2025-01-27 22:45:52 +00:00
Jonathan Coates
7d8f609c49 literally sobbing rn 2025-01-26 22:27:50 +00:00
Jonathan Coates
e7f56c4d25 Remove ComponentMap
I'd originally kept this around to prevent mods that use CC internals,
but fa2140d00b has probably broken those
anyway, so let's not worry too much.
2025-01-26 15:13:24 +00:00
Jonathan Coates
fa2140d00b Clean up container data interface
- Remove ContainerData.open.

 - Change PlatformHelper.openMenu to take a separate display name and
   MenuConstructor, rather than a MenuProvider. This makes the interface
   slightly easier to use in the common case, where we want to use
   lambdas instead.
2025-01-26 14:56:09 +00:00
Jonathan Coates
03388149b1 Fix command computers being exposed as peripherals
- Check whether the computer is a command computer before registering
   the capability.

 - Add tests to check what is/isn't a peripheral. See also #2020, where
   we forgot to register a peripheral on NeoForge 1.21.1.

Fixes #2070.
2025-01-26 11:13:52 +00:00
Jonathan Coates
f212861370 Mark command computers as onlyOpCanSetNbt
This isn't required in vanilla, as the command computer is a
GameMasterBlock, and so isn't placeable in the first place.

*However*, this is a problem with Create contraptions — with those it's
possible to "place" a command computer complete with NBT. We override
onlyOpCanSetNbt to prevent this [^1].

[^1]: 7a7993deb8/src/main/java/com/simibubi/create/foundation/utility/NBTProcessors.java (L179)
2025-01-21 20:41:55 +00:00
Jonathan Coates
4f3663ccc9 Allow overriding computer/floppy capacity
This adds a new "computercraft:storage_capacity" component to items (and
"Capacity" NBT tag to BEs), that overrides the capacity for the given
item.

Fixes #1814
2025-01-21 10:04:46 +00:00
Jonathan Coates
53425c1e76 Merge branch 'mc-1.20.x' into mc-1.21.x 2025-01-20 22:22:09 +00:00
Jonathan Coates
55edced9de Move GUI sprites to the sprites/ folder
This is where vanilla will read the sprites from in future versions, so
means we have a consistent layout between versions.

Also move the turtle "selected slot" texture to a sprite sheet. It would
be good to do more of these in the future (e.g. printer progress, maybe
bits of printouts).

Sorry to resource pack artists for causing trouble again.
2025-01-20 20:21:22 +00:00
Jonathan Coates
dc969c5a78 Configure ServerComputer via a Properties builder
We currently need to pass a whole bunch of arguments to a ServerComputer
in order to construct it, and if we implement #1814, this will get a
whole lot worse. Instead, we now pass most parameters (computer id,
family, label, term size, components) via a separate Properties class,
much like Minecraft does for blocks and items.

I'm not wild about the design of the API here, but I think it's a step
in the right direction.
2025-01-20 19:47:18 +00:00
Jonathan Coates
94ad6dab0e Map Unicode to CC's charset for char/paste events
We now convert uncode characters from "char" and "paste" events to CC's
charset[^1], rather than just leaving them unconverted. This means you
can paste in special characters like "♠" or "🮙" and they will be
converted correctly. Characters outside that range will be replaced with
"?", as before.

It would be nice to make this a bi-directional mapping, and do this for
Lua methods too (e.g. os.setComputerLabel). However, that has much wider
ramifications (and more likelyhood of breaking something), so avoiding
that for now.

 - Remove the generic "queue event" client->server message, and replace
   it with separate char/terminate/paste messages. This allows us to
   delete a chunk of code (all the NBT<->Object conversion), and makes
   server-side validation of events possible.

 - Fix os.setComputerLabel accepting the section sign — this is treated
   as special by Minecraft's formatting code. Sorry, no fun allowed.

 - Convert paste/char codepoints to CC's charset. Sadly MC's char hook
   splits the codepoint into surrogate pairs, which we *don't* attempt
   to reconstruct, so you can't currently use unicode input for block
   characters — you can paste them though!

[^1]: I'm referring this to the "terminal charset" within the code. I've
flip-flopped between "CraftOS", "terminal", "ComputerCraft", but feel
especially great.
2025-01-19 11:07:29 +00:00
Jonathan Coates
938eb38ad5 Move computer events to a single point
This abstraction never made much sense on InputHandler, as we only leave
the default methods on ServerComputer.

We now add a new class (ComputerEvents), which has a series of *static*
methods, that can queue an event on a ComputerEvents.Receiver object.
This is a bit of an odd indirection (why not just make them instance
methods on Receiver?!), but I don't really want those methods leaking
everywhere.
2025-01-18 19:26:10 +00:00
Jonathan Coates
6739c4c6c0 Wait for computers to run each tick in gametests 2025-01-17 18:43:19 +00:00
Jonathan Coates
d6749f8461 Set issue type in the templates
We already have the label, so it's not quite clear if it's worth it, but
let make our issue board look even more like a tube of smarties.
2025-01-17 17:25:39 +00:00
Jonathan Coates
d697c47b80 Catch ModdedConfig.getFilePath errors
This occurs when syncing the server config to the client. Ideally we'd
not hit this code path in the first place, but unfortunately there's no
way to tell where the config file comes from.

Fixes #2065
2025-01-17 17:14:06 +00:00
Jonathan Coates
5ba7f99326 Add back inputs on processResources
I kinda thought that Gradle would be smart enough to know that these
were input (given they're passed to expand), but apparently not :/.
2025-01-14 21:26:31 +00:00
121 changed files with 2394 additions and 814 deletions

View File

@@ -1,6 +1,7 @@
name: Bug report
description: Report some misbehaviour in the mod
labels: [ bug ]
type: bug
body:
- type: dropdown
id: mc-version
@@ -29,3 +30,5 @@ body:
Description of the bug. Please include the following:
- Logs: These will be located in the `logs/` directory of your Minecraft instance. This is always useful, even if it doesn't include errors, so please upload this!
- Detailed reproduction steps: sometimes I can spot a bug pretty easily, but often it's much more obscure. The more information I have to help reproduce it, the quicker it'll get fixed.
![A gif of burning text reading "Upload your logs!!!"](https://tweaked.cc/images/logs.gif)

View File

@@ -2,6 +2,7 @@
name: Feature request
about: Suggest an idea or improvement
labels: enhancement
type: feature
---
<!--

View File

@@ -48,7 +48,7 @@ repositories {
includeGroup("cc.tweaked")
// Things we mirror
includeGroup("com.simibubi.create")
includeGroup("commoble.morered")
includeGroup("net.commoble.morered")
includeGroup("dev.architectury")
includeGroup("dev.emi")
includeGroup("maven.modrinth")

View File

@@ -10,9 +10,7 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.SourceSet
@@ -33,6 +31,10 @@ import java.net.URI
import java.util.regex.Pattern
abstract class CCTweakedExtension(private val project: Project) {
/** Get the hash of the latest git commit. */
val gitHash: Provider<String> =
gitProvider("<no git commit>", listOf("rev-parse", "HEAD")) { it.trim() }
/** Get the current git branch. */
val gitBranch: Provider<String> =
gitProvider("<no git branch>", listOf("rev-parse", "--abbrev-ref", "HEAD")) { it.trim() }
@@ -164,6 +166,7 @@ abstract class CCTweakedExtension(private val project: Project) {
jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
excludes = listOf(
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them.

View File

@@ -10,8 +10,9 @@ kotlin.jvm.target.validation.mode=error
neogradle.subsystems.conventions.runs.enabled=false
# Mod properties
isUnstable=true
modVersion=1.114.3
modVersion=1.115.0
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.21.1

View File

@@ -42,7 +42,7 @@ iris-fabric = "1.8.0-beta.3+1.21-fabric"
iris-forge = "1.8.0-beta.3+1.21-neoforge"
jei = "19.8.2.99"
modmenu = "11.0.0-rc.4"
moreRed = "4.0.0.4"
moreRed = "6.0.0.3"
rei = "16.0.729"
sodium-fabric = "mc1.21-0.6.0-beta.1-fabric"
sodium-forge = "mc1.21-0.6.0-beta.1-neoforge"
@@ -119,7 +119,7 @@ jei-forge = { module = "mezz.jei:jei-1.21-neoforge", version.ref = "jei" }
mixin = { module = "org.spongepowered:mixin", version.ref = "mixin" }
mixinExtra = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinExtra" }
modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" }
moreRed = { module = "commoble.morered:morered-1.20.1", version.ref = "moreRed" }
moreRed = { module = "net.commoble.morered:morered-1.21.1", version.ref = "moreRed" }
rei-api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" }
rei-builtin = { module = "me.shedaniel:RoughlyEnoughItems-default-plugin", version.ref = "rei" }
rei-fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" }

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.media;
import dan200.computercraft.impl.ComputerCraftAPIService;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.stream.Stream;
/**
* The contents of a page (or book) created by a ComputerCraft printer.
*
* @since 1.115
*/
@Nullable
public interface PrintoutContents {
/**
* Get the (possibly empty) title for this printout.
*
* @return The title of this printout.
*/
String getTitle();
/**
* Get the text contents of this printout, as a sequence of lines.
* <p>
* The lines in the printout may include blank lines at the end of the document, as well as trailing spaces on each
* line.
*
* @return The text contents of this printout.
*/
Stream<String> getTextLines();
/**
* Get the printout contents for a particular stack.
*
* @param stack The stack to get the contents for.
* @return The printout contents, or {@code null} if this is not a printout item.
*/
static @Nullable PrintoutContents get(ItemStack stack) {
return ComputerCraftAPIService.get().getPrintoutContents(stack);
}
}

View File

@@ -19,8 +19,8 @@ import dan200.computercraft.api.ComputerCraftAPI;
*/
public interface WiredElement extends WiredSender {
/**
* Called when objects on the network change. This may occur when network nodes are added or removed, or when
* peripherals change.
* Called when peripherals on the network change. This may occur when network nodes are added or removed, or when
* peripherals are attached or detached from a modem.
*
* @param change The change which occurred.
* @see WiredNetworkChange

View File

@@ -13,6 +13,7 @@ import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.api.media.PrintoutContents;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
@@ -84,6 +85,9 @@ public interface ComputerCraftAPIService {
DetailRegistry<BlockReference> getBlockInWorldDetailRegistry();
@Nullable
PrintoutContents getPrintoutContents(ItemStack stack);
final class Instance {
static final @Nullable ComputerCraftAPIService INSTANCE;
static final @Nullable Throwable ERROR;

View File

@@ -10,10 +10,10 @@ 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.QueueEventServerMessage;
import dan200.computercraft.shared.network.server.PasteEventComputerMessage;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* An {@link InputHandler} for use on the client.
@@ -27,6 +27,11 @@ public final class ClientInputHandler implements InputHandler {
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));
@@ -42,11 +47,6 @@ public final class ClientInputHandler implements InputHandler {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
}
@Override
public void keyDown(int key, boolean repeat) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key));
@@ -57,6 +57,16 @@ public final class ClientInputHandler implements InputHandler {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.UP, key));
}
@Override
public void charTyped(byte chr) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.CHAR, chr));
}
@Override
public void paste(ByteBuffer contents) {
ClientNetworking.sendToServer(new PasteEventComputerMessage(menu, contents));
}
@Override
public void mouseClick(int button, int x, int y) {
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.Action.CLICK, button, x, y));

View File

@@ -34,8 +34,8 @@ public final class GuiSprites extends TextureAtlasHolder {
private static ButtonTextures button(String name) {
return new ButtonTextures(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name + "_hover")
);
}
@@ -96,12 +96,8 @@ public final class GuiSprites extends TextureAtlasHolder {
* @param active The texture for the button when it is active (hovered or focused).
*/
public record ButtonTextures(ResourceLocation normal, ResourceLocation active) {
public TextureAtlasSprite get(boolean active) {
return GuiSprites.get(active ? this.active : normal);
}
public Stream<ResourceLocation> textures() {
return Stream.of(normal, active);
public ResourceLocation get(boolean isActive) {
return isActive ? active : normal;
}
}

View File

@@ -26,6 +26,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
private static final ResourceLocation BACKGROUND_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
private static final ResourceLocation BACKGROUND_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
private static final ResourceLocation SELECTED_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_normal_selected_slot");
private static final ResourceLocation SELECTED_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_selected_slot");
private static final int TEX_WIDTH = 278;
private static final int TEX_HEIGHT = 217;
@@ -54,9 +57,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
if (slot >= 0) {
var slotX = slot % 4;
var slotY = slot / 4;
graphics.blit(texture,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0,
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE
graphics.blitSprite(
advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
);
}

View File

@@ -55,7 +55,7 @@ public final class ComputerSidebar {
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT,
GuiSprites.TERMINATE::get,
b -> input.queueEvent("terminate"),
b -> input.terminate(),
new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key")

View File

@@ -10,8 +10,8 @@ import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.util.function.Supplier;
@@ -21,11 +21,11 @@ import java.util.function.Supplier;
* dynamically.
*/
public class DynamicImageButton extends Button {
private final Boolean2ObjectFunction<TextureAtlasSprite> texture;
private final Boolean2ObjectFunction<ResourceLocation> texture;
private final Supplier<HintedMessage> message;
public DynamicImageButton(
int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress,
int x, int y, int width, int height, Boolean2ObjectFunction<ResourceLocation> texture, OnPress onPress,
HintedMessage message
) {
this(x, y, width, height, texture, onPress, () -> message);
@@ -33,7 +33,7 @@ public class DynamicImageButton extends Button {
public DynamicImageButton(
int x, int y, int width, int height,
Boolean2ObjectFunction<TextureAtlasSprite> texture,
Boolean2ObjectFunction<ResourceLocation> texture,
OnPress onPress, Supplier<HintedMessage> message
) {
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION);
@@ -50,7 +50,7 @@ public class DynamicImageButton extends Button {
var texture = this.texture.get(isHoveredOrFocused());
RenderSystem.disableDepthTest();
graphics.blit(getX(), getY(), 0, width, height, texture);
graphics.blitSprite(texture, getX(), getY(), 0, width, height);
RenderSystem.enableDepthTest();
}

View File

@@ -69,11 +69,8 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean charTyped(char ch, int modifiers) {
if (ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255) {
// Queue the char event for any printable chars in byte range
computer.queueEvent("char", new Object[]{ Character.toString(ch) });
}
var terminalChar = StringUtil.unicodeToTerminal(ch);
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
return true;
}
@@ -110,8 +107,8 @@ public class TerminalWidget extends AbstractWidget {
}
private void paste() {
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
var clipboard = StringUtil.getClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (clipboard.remaining() > 0) computer.paste(clipboard);
}
@Override
@@ -220,7 +217,7 @@ public class TerminalWidget extends AbstractWidget {
public void update() {
if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) {
computer.queueEvent("terminate");
computer.terminate();
}
if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) {

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.pocket.PocketComputerData;
import dan200.computercraft.client.render.CustomLecternRenderer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.item.component.DyedItemColor;
/**
* A model for {@linkplain PocketComputerItem pocket computers} placed on a lectern.
*
* @see CustomLecternRenderer
*/
public class LecternPocketModel {
public static final ResourceLocation TEXTURE_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_normal");
public static final ResourceLocation TEXTURE_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_advanced");
public static final ResourceLocation TEXTURE_COLOUR = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_colour");
public static final ResourceLocation TEXTURE_FRAME = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
public static final ResourceLocation TEXTURE_LIGHT = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_light");
private static final Material MATERIAL_NORMAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_NORMAL);
private static final Material MATERIAL_ADVANCED = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_ADVANCED);
private static final Material MATERIAL_COLOUR = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_COLOUR);
private static final Material MATERIAL_FRAME = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_FRAME);
private static final Material MATERIAL_LIGHT = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_LIGHT);
// The size of the terminal within the model.
public static final float TERM_WIDTH = 12.0f / 32.0f;
public static final float TERM_HEIGHT = 14.0f / 32.0f;
// The size of the texture. The texture is 36x36, but is at 2x resolution.
private static final int TEXTURE_WIDTH = 36 / 2;
private static final int TEXTURE_HEIGHT = 36 / 2;
private final ModelPart root;
public LecternPocketModel() {
root = buildPages();
}
private static ModelPart buildPages() {
var mesh = new MeshDefinition();
var parts = mesh.getRoot();
parts.addOrReplaceChild(
"root",
CubeListBuilder.create().texOffs(0, 0).addBox(0f, -5.0f, -4.0f, 1f, 10.0f, 8.0f),
PartPose.ZERO
);
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
}
/**
* Render the pocket computer model.
*
* @param poseStack The current pose stack.
* @param bufferSource The buffer source to draw to.
* @param packedLight The current light level.
* @param packedOverlay The overlay texture (used for entity hurt animation).
* @param family The computer family.
* @param frameColour The pocket computer's {@linkplain DyedItemColor colour}.
* @param lightColour The pocket computer's {@linkplain PocketComputerData#getLightState() light colour}.
*/
public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay, ComputerFamily family, int frameColour, int lightColour) {
if (frameColour != -1) {
root.render(poseStack, MATERIAL_FRAME.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay);
root.render(poseStack, MATERIAL_COLOUR.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay, frameColour);
} else {
var buffer = (family == ComputerFamily.ADVANCED ? MATERIAL_ADVANCED : MATERIAL_NORMAL).buffer(bufferSource, RenderType::entityCutout);
root.render(poseStack, buffer, packedLight, packedOverlay);
}
root.render(poseStack, MATERIAL_LIGHT.buffer(bufferSource, RenderType::entityCutout), LightTexture.FULL_BRIGHT, packedOverlay, lightColour);
}
}

View File

@@ -6,16 +6,30 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
import dan200.computercraft.shared.media.items.PrintoutData;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.LecternRenderer;
import net.minecraft.util.FastColor;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.phys.Vec3;
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;
/**
* A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}.
@@ -23,10 +37,17 @@ import net.minecraft.world.level.block.LecternBlock;
* This largely follows {@link LecternRenderer}, but with support for multiple types of item.
*/
public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternBlockEntity> {
private static final int POCKET_TERMINAL_RENDER_DISTANCE = 32;
private final BlockEntityRenderDispatcher berDispatcher;
private final LecternPrintoutModel printoutModel;
private final LecternPocketModel pocketModel;
public CustomLecternRenderer(BlockEntityRendererProvider.Context context) {
berDispatcher = context.getBlockEntityRenderDispatcher();
printoutModel = new LecternPrintoutModel();
pocketModel = new LecternPocketModel();
}
@Override
@@ -45,8 +66,46 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
} else {
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutData.getOrEmpty(item).pages());
}
} else if (item.getItem() instanceof PocketComputerItem pocket) {
var computer = ClientPocketComputers.get(item);
pocketModel.render(
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), DyedItemColor.getOrDefault(item, -1),
FastColor.ARGB32.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
);
// 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));
// Either render the terminal or a black screen, depending on how close we are.
var terminal = computer == null ? null : computer.getTerminal();
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(RenderTypes.TERMINAL));
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
renderPocketTerminal(poseStack, quadEmitter, terminal);
} else {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);
}
}
poseStack.popPose();
}
private static void renderPocketTerminal(PoseStack poseStack, FixedWidthFontRenderer.QuadEmitter quadEmitter, Terminal terminal) {
var width = terminal.getWidth() * FONT_WIDTH;
var height = terminal.getHeight() * FONT_HEIGHT;
// Scale the terminal down to fit in the available space.
var scaleX = LecternPocketModel.TERM_WIDTH / (width + MARGIN * 2);
var scaleY = LecternPocketModel.TERM_HEIGHT / (height + MARGIN * 2);
var scale = Math.min(scaleX, scaleY);
poseStack.scale(scale, scale, -1.0f);
// 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;
FixedWidthFontRenderer.drawTerminal(quadEmitter, marginX, marginY, terminal, marginY, marginY, marginX, marginX);
}
}

View File

@@ -39,8 +39,8 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
int termWidth, termHeight;
if (terminal == null) {
termWidth = Config.pocketTermWidth;
termHeight = Config.pocketTermHeight;
termWidth = Config.DEFAULT_POCKET_TERM_WIDTH;
termHeight = Config.DEFAULT_POCKET_TERM_HEIGHT;
} else {
termWidth = terminal.getWidth();
termHeight = terminal.getHeight();

View File

@@ -43,7 +43,7 @@ public final class FixedWidthFontRenderer {
static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
private static final float Z_OFFSET = 1e-3f;
private static final float Z_OFFSET = 1e-4f;
private FixedWidthFontRenderer() {
}

View File

@@ -8,6 +8,7 @@ import com.mojang.serialization.Codec;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.data.client.ExtraModelsProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay;
@@ -71,13 +72,11 @@ public final class DataProviders {
out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
UpgradeSlot.LEFT_UPGRADE,
UpgradeSlot.RIGHT_UPGRADE,
LecternPrintoutModel.TEXTURE
LecternPrintoutModel.TEXTURE,
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
)));
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
// Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),

View File

@@ -1,11 +1,5 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},

View File

@@ -2,6 +2,11 @@
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"},
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"},
{"type": "minecraft:single", "resource": "computercraft:entity/printout"}
{"type": "minecraft:single", "resource": "computercraft:entity/printout"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_normal"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_advanced"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_colour"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_frame"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_light"}
]
}

View File

@@ -13,6 +13,7 @@ import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.api.media.PrintoutContents;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
@@ -25,6 +26,7 @@ import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.impl.network.wired.WiredNodeImpl;
import dan200.computercraft.impl.upgrades.TurtleToolSpec;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.details.BlockDetails;
@@ -153,4 +155,9 @@ public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIServic
public final DetailRegistry<BlockReference> getBlockInWorldDetailRegistry() {
return blockDetails;
}
@Override
public @Nullable PrintoutContents getPrintoutContents(ItemStack stack) {
return stack.get(ModRegistry.DataComponents.PRINTOUT.get());
}
}

View File

@@ -31,6 +31,7 @@ import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.items.AbstractComputerItem;
import dan200.computercraft.shared.computer.items.CommandComputerItem;
@@ -90,9 +91,9 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable;
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
import dan200.computercraft.shared.turtle.upgrades.TurtleSpeaker;
import dan200.computercraft.shared.turtle.upgrades.TurtleTool;
import dan200.computercraft.shared.util.ComponentMap;
import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.NonNegativeId;
import dan200.computercraft.shared.util.StorageCapacity;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
@@ -316,6 +317,17 @@ public final class ModRegistry {
.persistent(NonNegativeId.CODEC).networkSynchronized(NonNegativeId.STREAM_CODEC)
);
/**
* The storage capacity of a computer or disk.
*
* @see AbstractComputerItem
* @see PocketComputerItem
* @see DiskItem
*/
public static final RegistryEntry<DataComponentType<StorageCapacity>> STORAGE_CAPACITY = register("storage_capacity", b -> b
.persistent(StorageCapacity.CODEC).networkSynchronized(StorageCapacity.STREAM_CODEC)
);
/**
* The left upgrade of a turtle.
*
@@ -603,7 +615,7 @@ public final class ModRegistry {
ComputerCraftAPI.registerAPIFactory(computer -> {
var turtle = computer.getComponent(ComputerComponents.TURTLE);
var metrics = Objects.requireNonNull(computer.getComponent(ComponentMap.METRICS));
var metrics = Objects.requireNonNull(computer.getComponent(ServerComputer.METRICS));
return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle);
});

View File

@@ -24,15 +24,13 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.RelativeMovement;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
@@ -259,18 +257,11 @@ public final class CommandComputerCraft {
* @return The constant {@code 1}.
*/
private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException {
var player = source.getPlayerOrException();
new ComputerContainerData(computer, new ItemStack(ModRegistry.Items.COMPUTER_NORMAL.get())).open(player, new MenuProvider() {
@Override
public Component getDisplayName() {
return Component.translatable("gui.computercraft.view_computer");
}
@Override
public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer);
}
});
PlatformHelper.get().openMenu(
source.getPlayerOrException(), Component.translatable("gui.computercraft.view_computer"),
(id, player, entity) -> new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer),
new ComputerContainerData(computer, new ItemStack(ModRegistry.Items.COMPUTER_NORMAL.get()))
);
return 1;
}

View File

@@ -7,6 +7,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;
import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos;
@@ -121,7 +122,7 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
var serverComputer = computer.createServerComputer();
serverComputer.turnOn();
new ComputerContainerData(serverComputer, getItem(computer)).open(player, computer);
PlatformHelper.get().openMenu(player, computer.getName(), computer, new ComputerContainerData(serverComputer, getItem(computer)));
}
return InteractionResult.sidedSuccess(level.isClientSide);
}

View File

@@ -23,13 +23,15 @@ import net.minecraft.core.HolderLookup;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.Container;
import net.minecraft.world.LockCode;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.Nameable;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -39,14 +41,16 @@ import javax.annotation.Nullable;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuProvider {
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuConstructor {
private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On";
private static final String NBT_CAPACITY = "Capacity";
private @Nullable UUID instanceID = null;
private int computerID = -1;
protected @Nullable String label = null;
protected long storageCapacity = -1;
private boolean on = false;
boolean startOn = false;
private boolean fresh = false;
@@ -143,6 +147,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
// Save ID, label and power state
if (computerID >= 0) nbt.putInt(NBT_ID, computerID);
if (label != null) nbt.putString(NBT_LABEL, label);
if (storageCapacity > 0) nbt.putLong(NBT_CAPACITY, storageCapacity);
nbt.putBoolean(NBT_ON, on);
lockCode.addToTag(nbt);
@@ -164,6 +169,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
// Load ID, label and power state
computerID = nbt.contains(NBT_ID) ? nbt.getInt(NBT_ID) : -1;
label = nbt.contains(NBT_LABEL) ? nbt.getString(NBT_LABEL) : null;
storageCapacity = nbt.contains(NBT_CAPACITY, Tag.TAG_ANY_NUMERIC) ? nbt.getLong(NBT_CAPACITY) : -1;
on = startOn = nbt.getBoolean(NBT_ON);
lockCode = LockCode.fromTag(nbt);
@@ -174,6 +180,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
super.applyImplicitComponents(component);
label = DataComponentUtil.getCustomName(component.get(DataComponents.CUSTOM_NAME));
computerID = NonNegativeId.getId(component.get(ModRegistry.DataComponents.COMPUTER_ID.get()));
storageCapacity = StorageCapacity.getOrDefault(component.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), -1);
lockCode = component.getOrDefault(DataComponents.LOCK, LockCode.NO_LOCK);
}
@@ -182,6 +189,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
super.collectImplicitComponents(builder);
builder.set(ModRegistry.DataComponents.COMPUTER_ID.get(), NonNegativeId.of(computerID));
builder.set(DataComponents.CUSTOM_NAME, label == null ? null : Component.literal(label));
builder.set(ModRegistry.DataComponents.STORAGE_CAPACITY.get(), storageCapacity > 0 ? new StorageCapacity(storageCapacity) : null);
if (lockCode != LockCode.NO_LOCK) builder.set(DataComponents.LOCK, lockCode);
}
@@ -191,6 +199,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
super.removeComponentsFromTag(tag);
tag.remove(NBT_ID);
tag.remove(NBT_LABEL);
tag.remove(NBT_CAPACITY);
tag.remove(LockCode.TAG_LOCK);
}
@@ -336,6 +345,10 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return label;
}
public final boolean isAdminOnly() {
return getBlockState().getBlock() instanceof GameMasterBlock;
}
public final void setComputerID(int id) {
if (getLevel().isClientSide || computerID == id) return;
@@ -397,14 +410,16 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
// We need this for pick block on the client side.
var nbt = super.getUpdateTag(registries);
if (label != null) nbt.putString(NBT_LABEL, label);
if (computerID >= 0) nbt.putInt(NBT_ID, computerID);
if (label != null) nbt.putString(NBT_LABEL, label);
if (storageCapacity > 0) nbt.putLong(NBT_CAPACITY, storageCapacity);
return nbt;
}
protected void loadClient(CompoundTag nbt, HolderLookup.Provider registries) {
label = nbt.contains(NBT_LABEL) ? nbt.getString(NBT_LABEL) : null;
computerID = nbt.contains(NBT_ID) ? nbt.getInt(NBT_ID) : -1;
label = nbt.contains(NBT_LABEL) ? nbt.getString(NBT_LABEL) : null;
storageCapacity = nbt.contains(NBT_CAPACITY, Tag.TAG_ANY_NUMERIC) ? nbt.getLong(NBT_CAPACITY) : -1;
}
protected void transferStateFrom(AbstractComputerBlockEntity copy) {
@@ -413,6 +428,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
instanceID = copy.instanceID;
computerID = copy.computerID;
label = copy.label;
storageCapacity = copy.storageCapacity;
on = copy.on;
startOn = copy.startOn;
lockCode = copy.lockCode;
@@ -443,4 +459,9 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
public Component getDisplayName() {
return Nameable.super.getDisplayName();
}
@Override
public boolean onlyOpCanSetNbt() {
return isAdminOnly();
}
}

View File

@@ -11,8 +11,7 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.util.ComponentMap;
import dan200.computercraft.shared.config.ConfigSpec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@@ -33,10 +32,10 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Override
protected ServerComputer createComputer(int id) {
return new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.computerTermWidth, Config.computerTermHeight,
ComponentMap.empty()
return new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
.label(getLabel())
.terminalSize(ConfigSpec.computerTermWidth.get(), ConfigSpec.computerTermHeight.get())
.storageCapacity(storageCapacity)
);
}

View File

@@ -11,7 +11,6 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.apis.ComputerAccess;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.computer.ApiLifecycle;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
@@ -26,11 +25,11 @@ import java.util.Map;
final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle {
private final ServerComputer computer;
private final IAPIEnvironment environment;
private final ComponentMap components;
private final Map<ComputerComponent<?>, Object> components;
private boolean active;
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, ComponentMap components) {
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, Map<ComputerComponent<?>, Object> components) {
super(environment);
this.computer = computer;
this.environment = environment;
@@ -95,7 +94,8 @@ final class ComputerSystem extends ComputerAccess implements IComputerSystem, Ap
}
@Override
@SuppressWarnings("unchecked")
public <T> @Nullable T getComponent(ComputerComponent<T> component) {
return components.get(component);
return (T) components.get(component);
}
}

View File

@@ -6,44 +6,33 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* Handles user-provided input, forwarding it to a computer. This is used
* 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 queueEvent(String event, @Nullable Object[] arguments);
void keyDown(int key, boolean repeat);
default void queueEvent(String event) {
queueEvent(event, null);
}
void keyUp(int key);
default void keyDown(int key, boolean repeat) {
queueEvent("key", new Object[]{ key, repeat });
}
void charTyped(byte chr);
default void keyUp(int key) {
queueEvent("key_up", new Object[]{ key });
}
void paste(ByteBuffer contents);
default void mouseClick(int button, int x, int y) {
queueEvent("mouse_click", new Object[]{ button, x, y });
}
void mouseClick(int button, int x, int y);
default void mouseUp(int button, int x, int y) {
queueEvent("mouse_up", new Object[]{ button, x, y });
}
void mouseUp(int button, int x, int y);
default void mouseDrag(int button, int x, int y) {
queueEvent("mouse_drag", new Object[]{ button, x, y });
}
void mouseDrag(int button, int x, int y);
default void mouseScroll(int direction, int x, int y) {
queueEvent("mouse_scroll", new Object[]{ direction, x, y });
}
void mouseScroll(int direction, int x, int y);
void terminate();
void shutdown();

View File

@@ -6,12 +6,14 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.AdminComputer;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.filesystem.WritableMount;
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.metrics.MetricsObserver;
import dan200.computercraft.impl.ApiFactories;
@@ -19,22 +21,26 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment {
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level;
@@ -47,31 +53,30 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
private final NetworkedTerminal terminal;
private final AtomicBoolean terminalChanged = new AtomicBoolean(false);
private final long storageCapacity;
private int ticksSincePing;
public ServerComputer(
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight,
ComponentMap baseComponents
) {
public ServerComputer(ServerLevel level, BlockPos position, Properties properties) {
this.level = level;
this.position = position;
this.family = family;
this.family = properties.family;
var context = ServerContext.get(level.getServer());
terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
terminal = new NetworkedTerminal(properties.terminalWidth, properties.terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
metrics = context.metrics().createMetricObserver(this);
var componentBuilder = ComponentMap.builder();
componentBuilder.add(ComponentMap.METRICS, metrics);
storageCapacity = properties.storageCapacity;
properties.addComponent(METRICS, metrics);
if (family == ComputerFamily.COMMAND) {
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
properties.addComponent(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
});
}
componentBuilder.add(baseComponents);
var components = componentBuilder.build();
var components = Map.copyOf(properties.components);
computer = new Computer(context.computerContext(), this, terminal, computerID);
computer.setLabel(label);
computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
computer.setLabel(properties.label);
// Load in the externally registered APIs.
for (var factory : ApiFactories.getAll()) {
@@ -84,24 +89,24 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
}
}
public ComputerFamily getFamily() {
public final ComputerFamily getFamily() {
return family;
}
public ServerLevel getLevel() {
public final ServerLevel getLevel() {
return level;
}
public BlockPos getPosition() {
public final BlockPos getPosition() {
return position;
}
public void setPosition(ServerLevel level, BlockPos pos) {
public final void setPosition(ServerLevel level, BlockPos pos) {
this.level = level;
position = pos.immutable();
}
protected void markTerminalChanged() {
protected final void markTerminalChanged() {
terminalChanged.set(true);
}
@@ -115,11 +120,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState()));
}
public TerminalState getTerminalState() {
public final TerminalState getTerminalState() {
return TerminalState.create(terminal);
}
public void keepAlive() {
public final void keepAlive() {
ticksSincePing = 0;
}
@@ -132,7 +137,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
*
* @return What sides on the computer have changed.
*/
public int pollRedstoneChanges() {
public final int pollRedstoneChanges() {
return computer.pollRedstoneChanges();
}
@@ -145,7 +150,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
computer.unload();
}
public void close() {
public final void close() {
unload();
ServerContext.get(level.getServer()).registry().remove(this);
}
@@ -165,7 +170,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
var server = level.getServer();
for (var player : server.getPlayerList().getPlayers()) {
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
if (player.containerMenu instanceof ComputerMenu menu && menu.getComputer() == this) {
ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
}
}
@@ -174,93 +179,144 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
protected void onRemoved() {
}
public UUID getInstanceUUID() {
public final UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() {
public final int getID() {
return computer.getID();
}
public @Nullable String getLabel() {
public final @Nullable String getLabel() {
return computer.getLabel();
}
public boolean isOn() {
public final boolean isOn() {
return computer.isOn();
}
public ComputerState getState() {
public final ComputerState getState() {
if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
}
@Override
public void turnOn() {
public final void turnOn() {
computer.turnOn();
}
@Override
public void shutdown() {
public final void shutdown() {
computer.shutdown();
}
@Override
public void reboot() {
public final void reboot() {
computer.reboot();
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
public final void queueEvent(String event, @Nullable Object[] arguments) {
computer.queueEvent(event, arguments);
}
public int getRedstoneOutput(ComputerSide side) {
public final void queueEvent(String event) {
queueEvent(event, null);
}
public final int getRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0;
}
public void setRedstoneInput(ComputerSide side, int level, int bundledState) {
public final void setRedstoneInput(ComputerSide side, int level, int bundledState) {
computer.getRedstone().setInput(side, level, bundledState);
}
public int getBundledRedstoneOutput(ComputerSide side) {
public final int getBundledRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalBundledOutput(side) : 0;
}
public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
public final void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
computer.getEnvironment().setPeripheral(side, peripheral);
}
@Nullable
public IPeripheral getPeripheral(ComputerSide side) {
public final IPeripheral getPeripheral(ComputerSide side) {
return computer.getEnvironment().getPeripheral(side);
}
public void setLabel(@Nullable String label) {
public final void setLabel(@Nullable String label) {
computer.setLabel(label);
}
@Override
public double getTimeOfDay() {
public final double getTimeOfDay() {
return (level.getDayTime() + 6000) % 24000 / 1000.0;
}
@Override
public int getDay() {
public final int getDay() {
return (int) ((level.getDayTime() + 6000) / 24000) + 1;
}
@Override
public MetricsObserver getMetrics() {
public final MetricsObserver getMetrics() {
return metrics;
}
public WorkMonitor getMainThreadMonitor() {
public final WorkMonitor getMainThreadMonitor() {
return computer.getMainThreadMonitor();
}
@Override
public @Nullable WritableMount createRootMount() {
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit);
public final WritableMount createRootMount() {
var capacity = storageCapacity <= 0 ? ConfigSpec.computerSpaceLimit.get() : storageCapacity;
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), capacity);
}
public static Properties properties(int computerID, ComputerFamily family) {
return new Properties(computerID, family);
}
public static final class Properties {
private final int computerID;
private @Nullable String label;
private final ComputerFamily family;
private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH;
private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT;
private long storageCapacity = -1;
private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
private Properties(int computerID, ComputerFamily family) {
this.computerID = computerID;
this.family = family;
}
public Properties label(@Nullable String label) {
this.label = label;
return this;
}
public Properties terminalSize(int width, int height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("Terminal size must be positive");
this.terminalWidth = width;
this.terminalHeight = height;
return this;
}
/**
* Override the storage capacity for this computer.
*
* @param capacity The capacity for this computer's drive, or {@code -1} to use the default.
* @return {@code this}, for chaining.
*/
public Properties storageCapacity(long capacity) {
storageCapacity = capacity;
return this;
}
public <T> Properties addComponent(ComputerComponent<T> component, T value) {
if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
components.put(component, value);
return this;
}
}
}

View File

@@ -9,8 +9,9 @@ import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.StorageCapacity;
import net.minecraft.ChatFormatting;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.component.DataComponents;
@@ -53,6 +54,9 @@ public class AbstractComputerItem extends BlockItem implements IMedia {
@Override
public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
var id = stack.get(ModRegistry.DataComponents.COMPUTER_ID.get());
return id != null ? ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id.id(), Config.computerSpaceLimit) : null;
if (id == null) return null;
var capacity = StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), ConfigSpec.computerSpaceLimit);
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id.id(), capacity);
}
}

View File

@@ -7,6 +7,8 @@ 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.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
@@ -21,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.UUID;
@@ -48,21 +51,33 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
this.owner = owner;
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
owner.getComputer().queueEvent(event, arguments);
}
@Override
public void keyDown(int key, boolean repeat) {
keysDown.add(key);
owner.getComputer().keyDown(key, repeat);
ComputerEvents.keyDown(owner.getComputer(), key, repeat);
}
@Override
public void keyUp(int key) {
keysDown.remove(key);
owner.getComputer().keyUp(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.remaining(), max = buffer.limit(); i < max; i++) {
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
}
return true;
}
@Override
@@ -71,7 +86,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = button;
owner.getComputer().mouseClick(button, x, y);
ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
}
@Override
@@ -80,7 +95,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = -1;
owner.getComputer().mouseUp(button, x, y);
ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
}
@Override
@@ -89,7 +104,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = button;
owner.getComputer().mouseDrag(button, x, y);
ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
}
@Override
@@ -97,7 +112,12 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseX = x;
lastMouseY = y;
owner.getComputer().mouseScroll(direction, x, y);
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
}
@Override
public void terminate() {
owner.getComputer().queueEvent("terminate");
}
@Override
@@ -169,9 +189,9 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
public void close() {
var computer = owner.getComputer();
var keys = keysDown.iterator();
while (keys.hasNext()) computer.keyUp(keys.nextInt());
while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt());
if (lastMouseDown != -1) computer.mouseUp(lastMouseDown, lastMouseX, lastMouseY);
if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY);
keysDown.clear();
lastMouseDown = -1;

View File

@@ -12,8 +12,6 @@ import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
* @see ConfigSpec The definition of our config values.
*/
public final class Config {
public static int computerSpaceLimit = 1000 * 1000;
public static int floppySpaceLimit = 125 * 1000;
public static int uploadMaxSize = 512 * 1024; // 512 KB
public static boolean commandRequireCreative = true;
@@ -32,14 +30,14 @@ public final class Config {
public static int advancedTurtleFuelLimit = 100000;
public static boolean turtlesCanPush = true;
public static int computerTermWidth = 51;
public static int computerTermHeight = 19;
public static final int DEFAULT_COMPUTER_TERM_WIDTH = 51;
public static final int DEFAULT_COMPUTER_TERM_HEIGHT = 19;
public static final int turtleTermWidth = 39;
public static final int turtleTermHeight = 13;
public static final int TURTLE_TERM_WIDTH = 39;
public static final int TURTLE_TERM_HEIGHT = 13;
public static int pocketTermWidth = 26;
public static int pocketTermHeight = 20;
public static final int DEFAULT_POCKET_TERM_WIDTH = 26;
public static final int DEFAULT_POCKET_TERM_HEIGHT = 20;
public static int monitorWidth = 8;
public static int monitorHeight = 6;

View File

@@ -97,11 +97,11 @@ public final class ConfigSpec {
{ // General computers
computerSpaceLimit = builder
.comment("The disk space limit for computers and turtles, in bytes.")
.define("computer_space_limit", Config.computerSpaceLimit);
.define("computer_space_limit", 1000 * 1000);
floppySpaceLimit = builder
.comment("The disk space limit for floppy disks, in bytes.")
.define("floppy_space_limit", Config.floppySpaceLimit);
.define("floppy_space_limit", 125 * 1000);
uploadMaxSize = builder
.comment("""
@@ -344,13 +344,13 @@ public final class ConfigSpec {
.push("term_sizes");
builder.comment("Terminal size of computers.").push("computer");
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 1, 255);
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.computerTermHeight, 1, 255);
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.DEFAULT_COMPUTER_TERM_WIDTH, 1, 255);
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.DEFAULT_COMPUTER_TERM_HEIGHT, 1, 255);
builder.pop();
builder.comment("Terminal size of pocket computers.").push("pocket_computer");
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 1, 255);
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.pocketTermHeight, 1, 255);
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.DEFAULT_POCKET_TERM_WIDTH, 1, 255);
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.DEFAULT_POCKET_TERM_HEIGHT, 1, 255);
builder.pop();
builder.comment("Maximum size of monitors (in blocks).").push("monitor");
@@ -384,8 +384,6 @@ public final class ConfigSpec {
public static void syncServer(@Nullable Path path) {
// General
Config.computerSpaceLimit = computerSpaceLimit.get();
Config.floppySpaceLimit = floppySpaceLimit.get();
Config.uploadMaxSize = uploadMaxSize.get();
CoreConfig.maximumFilesOpen = maximumFilesOpen.get();
CoreConfig.defaultComputerSettings = defaultComputerSettings.get();
@@ -437,10 +435,6 @@ public final class ConfigSpec {
Config.turtlesCanPush = turtlesCanPush.get();
// Terminal size
Config.computerTermWidth = computerTermWidth.get();
Config.computerTermHeight = computerTermHeight.get();
Config.pocketTermWidth = pocketTermWidth.get();
Config.pocketTermHeight = pocketTermHeight.get();
Config.monitorWidth = monitorWidth.get();
Config.monitorHeight = monitorHeight.get();
}

View File

@@ -6,6 +6,8 @@ package dan200.computercraft.shared.lectern;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.stats.Stats;
@@ -20,8 +22,12 @@ import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
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.phys.BlockHitResult;
import org.jetbrains.annotations.Nullable;
/**
* Extends {@link LecternBlock} with support for {@linkplain PrintoutItem printouts}.
@@ -48,7 +54,7 @@ public class CustomLecternBlock extends LecternBlock {
* @return Whether the item was placed or not.
*/
public static InteractionResult tryPlaceItem(Player player, Level level, BlockPos pos, BlockState blockState, ItemStack item) {
if (item.getItem() instanceof PrintoutItem) {
if (item.getItem() instanceof PrintoutItem || item.getItem() instanceof PocketComputerItem) {
if (!level.isClientSide) replaceLectern(player, level, pos, blockState, item);
return InteractionResult.sidedSuccess(level.isClientSide);
}
@@ -152,7 +158,7 @@ public class CustomLecternBlock extends LecternBlock {
clearLectern(level, pos, state);
} else {
// Otherwise open the screen.
player.openMenu(lectern);
lectern.openMenu(player);
}
player.awardStat(Stats.INTERACT_WITH_LECTERN);
@@ -160,4 +166,11 @@ public class CustomLecternBlock extends LecternBlock {
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
return level.isClientSide ? null : BlockEntityHelpers.createTickerHelper(type, ModRegistry.BlockEntities.LECTERN.get(), serverTicker);
}
private static final BlockEntityTicker<CustomLecternBlockEntity> serverTicker = (level, pos, state, lectern) -> lectern.tick();
}

View File

@@ -10,28 +10,26 @@ 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.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.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.util.Mth;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
import java.util.AbstractList;
import java.util.List;
@@ -41,7 +39,7 @@ import java.util.List;
*
* @see LecternBlockEntity
*/
public final class CustomLecternBlockEntity extends BlockEntity implements MenuProvider {
public final class CustomLecternBlockEntity extends BlockEntity {
private static final String NBT_ITEM = "Item";
private static final String NBT_PAGE = "Page";
@@ -83,6 +81,12 @@ public final class CustomLecternBlockEntity extends BlockEntity implements MenuP
}
}
void tick() {
if (item.getItem() instanceof PocketComputerItem pocket) {
pocket.tick(item, new PocketHolder.LecternHolder(this), false);
}
}
/**
* Set the current page, emitting a redstone pulse if needed.
*
@@ -125,24 +129,17 @@ public final class CustomLecternBlockEntity extends BlockEntity implements MenuP
return tag;
}
@Nullable
@Override
public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
void openMenu(Player player) {
var item = getItem();
if (item.getItem() instanceof PrintoutItem) {
return new PrintoutMenu(
containerId, new LecternContainer(), 0,
p -> Container.stillValidBlockEntity(this, player, Container.DEFAULT_DISTANCE_BUFFER),
player.openMenu(new SimpleMenuProvider((id, inventory, entity) -> new PrintoutMenu(
id, new LecternContainer(), 0,
p -> Container.stillValidBlockEntity(this, p, Container.DEFAULT_DISTANCE_BUFFER),
new PrintoutContainerData()
);
), getItem().getDisplayName()));
} else if (item.getItem() instanceof PocketComputerItem pocket) {
pocket.open(player, item, new PocketHolder.LecternHolder(this), true);
}
return null;
}
@Override
public Component getDisplayName() {
return getItem().getDisplayName();
}
/**

View File

@@ -10,8 +10,9 @@ import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.util.NonNegativeId;
import dan200.computercraft.shared.util.StorageCapacity;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
@@ -64,7 +65,8 @@ public class DiskItem extends Item implements IMedia {
@Override
public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
var diskID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.DISK_ID.get(), "disk");
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "disk/" + diskID, Config.floppySpaceLimit);
var capacity = StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), ConfigSpec.floppySpaceLimit);
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "disk/" + diskID, capacity);
}
public static int getDiskID(ItemStack stack) {

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.shared.media.items;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.media.PrintoutContents;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.ModRegistry;
import io.netty.buffer.ByteBuf;
@@ -16,6 +17,7 @@ import net.minecraft.network.codec.StreamCodec;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* The contents of a printout.
@@ -25,7 +27,7 @@ import java.util.List;
* @see PrintoutItem
* @see dan200.computercraft.shared.ModRegistry.DataComponents#PRINTOUT
*/
public record PrintoutData(String title, List<Line> lines) {
public record PrintoutData(String title, List<Line> lines) implements PrintoutContents {
public static final int LINE_LENGTH = 25;
public static final int LINES_PER_PAGE = 21;
public static final int MAX_PAGES = 16;
@@ -107,4 +109,14 @@ public record PrintoutData(String title, List<Line> lines) {
public int pages() {
return Math.ceilDiv(lines.size(), LINES_PER_PAGE);
}
@Override
public String getTitle() {
return title();
}
@Override
public Stream<String> getTextLines() {
return lines().stream().map(Line::text);
}
}

View File

@@ -26,9 +26,9 @@ public final class NetworkMessages {
private static final List<CustomPacketPayload.TypeAndCodec<RegistryFriendlyByteBuf, ? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
public static final CustomPacketPayload.Type<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound("computer_action", ComputerActionServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<QueueEventServerMessage> QUEUE_EVENT = register(serverMessages, "queue_event", QueueEventServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<KeyEventServerMessage> KEY_EVENT = registerServerbound("key_event", KeyEventServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<MouseEventServerMessage> MOUSE_EVENT = registerServerbound("mouse_event", MouseEventServerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<PasteEventComputerMessage> PASTE_EVENT = registerServerbound("paste_event", PasteEventComputerMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<UploadFileMessage> UPLOAD_FILE = register(serverMessages, "upload_file", UploadFileMessage.STREAM_CODEC);
public static final CustomPacketPayload.Type<ChatTableClientMessage> CHAT_TABLE = registerClientbound("chat_table", ChatTableClientMessage.STREAM_CODEC);

View File

@@ -83,25 +83,36 @@ public class MoreStreamCodecs {
};
/**
* Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
* Read a {@link ByteBuffer}, with a limit of the number of bytes to read.
*
* @param limit The maximum length of the received buffer.
* @return A stream codec that reads {@link ByteBuffer}s.
* @see #BYTE_BUFFER
*/
public static final StreamCodec<ByteBuf, ByteBuffer> BYTE_BUFFER = new StreamCodec<>() {
@Override
public ByteBuffer decode(ByteBuf buf) {
var toRead = VarInt.read(buf);
if (toRead > buf.readableBytes()) {
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed");
public static StreamCodec<ByteBuf, ByteBuffer> byteBuffer(int limit) {
return new StreamCodec<>() {
@Override
public ByteBuffer decode(ByteBuf buf) {
var toRead = VarInt.read(buf);
if (toRead > buf.readableBytes() || toRead >= limit) {
throw new DecoderException("ByteArray with size " + toRead + " is bigger than allowed");
}
var bytes = new byte[toRead];
buf.readBytes(bytes);
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
}
var bytes = new byte[toRead];
buf.readBytes(bytes);
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
}
@Override
public void encode(ByteBuf buf, ByteBuffer buffer) {
VarInt.write(buf, buffer.remaining());
buf.writeBytes(buffer.duplicate());
}
};
}
@Override
public void encode(ByteBuf buf, ByteBuffer buffer) {
VarInt.write(buf, buffer.remaining());
buf.writeBytes(buffer.duplicate());
}
};
/**
* Equivalent to {@link ByteBufCodecs#BYTE_ARRAY}, but into an immutable {@link ByteBuffer}.
*/
public static final StreamCodec<ByteBuf, ByteBuffer> BYTE_BUFFER = byteBuffer(Integer.MAX_VALUE);
}

View File

@@ -8,9 +8,7 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
@@ -20,16 +18,6 @@ import net.minecraft.world.inventory.MenuType;
public interface ContainerData {
void toBytes(RegistryFriendlyByteBuf buf);
/**
* Open a menu for a specific player using this data.
*
* @param player The player to open the menu for.
* @param menu The underlying menu provider.
*/
default void open(Player player, MenuProvider menu) {
PlatformHelper.get().openMenu(player, menu, this);
}
static <C extends AbstractContainerMenu, T extends ContainerData> MenuType<C> toType(StreamCodec<RegistryFriendlyByteBuf, T> codec, Factory<C, T> factory) {
return PlatformHelper.get().createMenuType(codec, factory);
}

View File

@@ -37,6 +37,7 @@ 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();
@@ -49,6 +50,7 @@ public final class ComputerActionServerMessage extends ComputerServerMessage {
}
public enum Action {
TERMINATE,
TURN_ON,
SHUTDOWN,
REBOOT

View File

@@ -25,8 +25,8 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw
@Override
public void handle(ServerNetworkContext context) {
Player player = context.getSender();
if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu) {
handle(context, (ComputerMenu) player.containerMenu);
if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
handle(context, menu);
}
}

View File

@@ -44,6 +44,7 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
case UP -> input.keyUp(key);
case DOWN -> input.keyDown(key, false);
case REPEAT -> input.keyDown(key, true);
case CHAR -> input.charTyped((byte) key);
}
}
@@ -53,6 +54,6 @@ public final class KeyEventServerMessage extends ComputerServerMessage {
}
public enum Action {
DOWN, REPEAT, UP
DOWN, REPEAT, UP, CHAR
}
}

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network.server;
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;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.inventory.AbstractContainerMenu;
import java.nio.ByteBuffer;
/**
* Paste a string on a {@link ServerComputer}.
*
* @see ServerInputHandler#paste(ByteBuffer)
*/
public class PasteEventComputerMessage extends ComputerServerMessage {
public static final StreamCodec<RegistryFriendlyByteBuf, PasteEventComputerMessage> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.VAR_INT, PasteEventComputerMessage::containerId,
MoreStreamCodecs.byteBuffer(StringUtil.MAX_PASTE_LENGTH), c -> c.text,
PasteEventComputerMessage::new
);
private final ByteBuffer text;
public PasteEventComputerMessage(AbstractContainerMenu menu, ByteBuffer text) {
this(menu.containerId, text);
}
private PasteEventComputerMessage(int id, ByteBuffer text) {
super(id);
this.text = text;
}
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
container.getInput().paste(text);
}
@Override
public CustomPacketPayload.Type<PasteEventComputerMessage> type() {
return NetworkMessages.PASTE_EVENT;
}
}

View File

@@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network.server;
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.util.NBTUtil;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
/**
* Queue an event on a {@link ServerComputer}.
*
* @see ServerInputHandler#queueEvent(String)
*/
public final class QueueEventServerMessage extends ComputerServerMessage {
public static final StreamCodec<RegistryFriendlyByteBuf, QueueEventServerMessage> STREAM_CODEC = StreamCodec.ofMember(QueueEventServerMessage::write, QueueEventServerMessage::new);
private final String event;
private final @Nullable Object[] args;
public QueueEventServerMessage(AbstractContainerMenu menu, String event, @Nullable Object[] args) {
super(menu.containerId);
this.event = event;
this.args = args;
}
private QueueEventServerMessage(FriendlyByteBuf buf) {
super(buf.readVarInt());
event = buf.readUtf(Short.MAX_VALUE);
var args = buf.readNbt();
this.args = args == null ? null : NBTUtil.decodeObjects(args);
}
private void write(RegistryFriendlyByteBuf buf) {
buf.writeVarInt(containerId());
buf.writeUtf(event);
buf.writeNbt(args == null ? null : NBTUtil.encodeObjects(args));
}
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
container.getInput().queueEvent(event, args);
}
@Override
public CustomPacketPayload.Type<QueueEventServerMessage> type() {
return NetworkMessages.QUEUE_EVENT;
}
}

View File

@@ -18,6 +18,7 @@ import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
@@ -237,7 +238,7 @@ public class MonitorBlockEntity extends BlockEntity {
getLevel().setBlock(getBlockPos(), getBlockState()
.setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
yIndex < height - 1, yIndex > 0,
xIndex > 0, xIndex < width - 1)), 2);
xIndex > 0, xIndex < width - 1)), Block.UPDATE_CLIENTS);
}
// region Sizing and placement stuff

View File

@@ -17,6 +17,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
@@ -24,10 +25,14 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.tags.TagKey;
import net.minecraft.world.*;
import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.DyeColor;
@@ -109,10 +114,11 @@ public interface PlatformHelper {
* Open a container using a specific {@link ContainerData}.
*
* @param player The player to open the menu for.
* @param owner The underlying menu provider.
* @param menu The menu data.
* @param title The title for this menu.
* @param menu The underlying menu constructor.
* @param data The menu data.
*/
void openMenu(Player player, MenuProvider owner, ContainerData menu);
void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data);
/**
* Invalidate components on a block enitty.

View File

@@ -8,7 +8,7 @@ import dan200.computercraft.api.pocket.IPocketAccess;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
@@ -41,8 +41,8 @@ public final class PocketBrain implements IPocketAccess {
private int colour = -1;
private int lightColour = -1;
public PocketBrain(PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
this.computer = new PocketServerComputer(this, holder, computerID, label, family);
public PocketBrain(PocketHolder holder, @Nullable UpgradeData<IPocketUpgrade> upgrade, ServerComputer.Properties properties) {
this.computer = new PocketServerComputer(this, holder, properties);
this.holder = holder;
this.position = holder.pos();
this.upgrade = upgrade;

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
@@ -51,6 +53,15 @@ public sealed interface PocketHolder {
*/
void setChanged();
/**
* Whether the terminal is visible to all players in range, and so should be broadcast to everyone.
*
* @return Whether to send the terminal.
*/
default boolean isTerminalAlwaysVisible() {
return false;
}
/**
* An {@link Entity} holding a pocket computer.
*/
@@ -112,4 +123,41 @@ public sealed interface PocketHolder {
entity.setItem(entity.getItem().copy());
}
}
/**
* A pocket computer in a {@link CustomLecternBlockEntity}.
*
* @param lectern The lectern holding this item.
*/
record LecternHolder(CustomLecternBlockEntity lectern) implements PocketHolder {
@Override
public ServerLevel level() {
return (ServerLevel) lectern.getLevel();
}
@Override
public Vec3 pos() {
return Vec3.atCenterOf(lectern.getBlockPos());
}
@Override
public BlockPos blockPos() {
return lectern.getBlockPos();
}
@Override
public boolean isValid(ServerComputer computer) {
return !lectern().isRemoved() && PocketComputerItem.isServerComputer(computer, lectern.getItem());
}
@Override
public void setChanged() {
BlockEntityHelpers.updateBlock(lectern());
}
@Override
public boolean isTerminalAlwaysVisible() {
return true;
}
}
}

View File

@@ -5,15 +5,13 @@
package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos;
@@ -41,10 +39,10 @@ public final class PocketServerComputer extends ServerComputer {
private Set<ServerPlayer> tracking = Set.of();
PocketServerComputer(PocketBrain brain, PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family) {
super(
holder.level(), holder.blockPos(), computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight,
ComponentMap.builder().add(ComputerComponents.POCKET, brain).build()
PocketServerComputer(PocketBrain brain, PocketHolder holder, ServerComputer.Properties properties) {
super(holder.level(), holder.blockPos(), properties
.terminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get())
.addComponent(ComputerComponents.POCKET, brain)
);
this.brain = brain;
}
@@ -74,7 +72,7 @@ public final class PocketServerComputer extends ServerComputer {
// Broadcast the state to new players.
var added = newTracking.stream().filter(x -> !tracking.contains(x)).toList();
if (!added.isEmpty()) {
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added);
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, brain.holder().isTerminalAlwaysVisible()), added);
}
}
@@ -85,9 +83,15 @@ public final class PocketServerComputer extends ServerComputer {
protected void onTerminalChanged() {
super.onTerminalChanged();
if (brain.holder() instanceof PocketHolder.PlayerHolder holder && holder.isValid(this)) {
// Broadcast the terminal to the current player.
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), holder.entity());
var holder = brain.holder() instanceof PocketHolder.PlayerHolder h && h.isValid(this) ? h.entity() : null;
if (brain.holder().isTerminalAlwaysVisible() && !tracking.isEmpty()) {
// If the terminal is always visible, send it to all players *and* the holder.
var packet = new PocketComputerDataMessage(this, true);
ServerNetworking.sendToPlayers(packet, tracking);
if (holder != null && !tracking.contains(holder)) ServerNetworking.sendToPlayer(packet, holder);
} else if (holder != null) {
// Otherwise just send it to the holder.
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), holder);
}
}

View File

@@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.pocket.inventory;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class PocketComputerMenuProvider implements MenuProvider {
private final ServerComputer computer;
private final Component name;
private final PocketComputerItem item;
private final InteractionHand hand;
private final boolean isTypingOnly;
public PocketComputerMenuProvider(ServerComputer computer, ItemStack stack, PocketComputerItem item, InteractionHand hand, boolean isTypingOnly) {
this.computer = computer;
name = stack.getHoverName();
this.item = item;
this.hand = hand;
this.isTypingOnly = isTypingOnly;
}
@Override
public Component getDisplayName() {
return name;
}
@Nullable
@Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player entity) {
return new ComputerMenuWithoutInventory(
isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(), id, inventory,
p -> {
var stack = p.getItemInHand(hand);
return stack.getItem() == item && PocketComputerItem.getServerComputer(assertNonNull(entity.level().getServer()), stack) == computer;
},
computer
);
}
}

View File

@@ -17,17 +17,15 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerComputerRegistry;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.items.ServerComputerReference;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.container.ComputerContainerData;
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.core.PocketServerComputer;
import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider;
import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.IDAssigner;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.util.NonNegativeId;
import dan200.computercraft.shared.util.*;
import net.minecraft.ChatFormatting;
import net.minecraft.core.HolderLookup;
import net.minecraft.network.chat.Component;
@@ -60,12 +58,20 @@ public class PocketComputerItem extends Item implements IMedia {
/**
* Tick a pocket computer.
*
* @param stack The current pocket computer stack.
* @param holder The entity holding the pocket item.
* @param brain The pocket computer brain.
* @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.
*/
private void tick(ItemStack stack, PocketHolder holder, PocketBrain brain) {
brain.updateHolder(holder);
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();
}
// Update pocket upgrade
var upgrade = brain.getUpgrade();
@@ -111,11 +117,7 @@ public class PocketComputerItem extends Item implements IMedia {
if (slot < 0) return;
// If we're in the inventory, create a computer and keep it alive.
var holder = new PocketHolder.PlayerHolder(player, slot);
var brain = getOrCreateBrain((ServerLevel) world, holder, stack);
brain.computer().keepAlive();
tick(stack, holder, brain);
tick(stack, new PocketHolder.PlayerHolder(player, slot), false);
}
@ForgeOverride
@@ -125,8 +127,7 @@ public class PocketComputerItem extends Item implements IMedia {
// If we're an item entity, tick an already existing computer (as to update the position), but do not keep the
// computer alive.
var computer = getServerComputer(level.getServer(), stack);
if (computer != null) tick(stack, new PocketHolder.ItemEntityHolder(entity), computer.getBrain());
tick(stack, new PocketHolder.ItemEntityHolder(entity), true);
return false;
}
@@ -143,20 +144,39 @@ public class PocketComputerItem extends Item implements IMedia {
var stop = false;
var upgrade = getUpgrade(stack);
if (upgrade != null) {
brain.updateHolder(holder);
stop = upgrade.onRightClick(world, brain, computer.getPeripheral(ComputerSide.BACK));
// Sync back just in case. We don't need to setChanged, as we'll return the item anyway.
updateItem(stack, brain);
}
if (!stop) {
var isTypingOnly = hand == InteractionHand.OFF_HAND;
new ComputerContainerData(computer, stack).open(player, new PocketComputerMenuProvider(computer, stack, this, hand, isTypingOnly));
}
if (!stop) openImpl(player, stack, holder, hand == InteractionHand.OFF_HAND, computer);
}
return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack);
}
/**
* 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) {
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),
computer
), new ComputerContainerData(computer, stack));
}
@Override
public Component getName(ItemStack stack) {
var baseString = getDescriptionId(stack);
@@ -192,17 +212,25 @@ public class PocketComputerItem extends Item implements IMedia {
var registry = ServerContext.get(level.getServer()).registry();
{
var computer = getServerComputer(registry, stack);
if (computer != null) return computer.getBrain();
if (computer != null) {
var brain = computer.getBrain();
brain.updateHolder(holder);
return brain;
}
}
var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER);
var brain = new PocketBrain(holder, computerID, getLabel(stack), getFamily(), getUpgradeWithData(stack));
var brain = new PocketBrain(
holder, getUpgradeWithData(stack),
ServerComputer.properties(computerID, getFamily())
.label(getLabel(stack))
.storageCapacity(StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), -1))
);
var computer = brain.computer();
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register()));
// Only turn on when initially creating the computer, rather than each tick.
if (isMarkedOn(stack) && holder instanceof PocketHolder.PlayerHolder) computer.turnOn();
if (isMarkedOn(stack)) computer.turnOn();
updateItem(stack, brain);
@@ -262,10 +290,10 @@ public class PocketComputerItem extends Item implements IMedia {
@Override
public @Nullable Mount createDataMount(ItemStack stack, ServerLevel level) {
var id = stack.get(ModRegistry.DataComponents.COMPUTER_ID.get());
if (id != null) {
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id.id(), Config.computerSpaceLimit);
}
return null;
if (id == null) return null;
var capacity = StorageCapacity.getOrDefault(stack.get(ModRegistry.DataComponents.STORAGE_CAPACITY.get()), ConfigSpec.computerSpaceLimit);
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + id.id(), capacity);
}
private static boolean isMarkedOn(ItemStack stack) {

View File

@@ -24,7 +24,6 @@ import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
@@ -80,10 +79,11 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
@Override
protected ServerComputer createComputer(int id) {
var computer = new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.turtleTermWidth, Config.turtleTermHeight,
ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build()
var computer = new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
.label(getLabel())
.terminalSize(Config.TURTLE_TERM_WIDTH, Config.TURTLE_TERM_HEIGHT)
.storageCapacity(storageCapacity)
.addComponent(ComputerComponents.TURTLE, brain)
);
brain.setupComputer(computer);
return computer;

View File

@@ -40,6 +40,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
@@ -256,7 +257,7 @@ public class TurtleBrain implements TurtleAccessInternal {
try {
// We use Block.UPDATE_CLIENTS here to ensure that neighbour updates caused in Block.updateNeighbourShapes
// are sent to the client. We want to avoid doing a full block update until the turtle state is copied over.
if (world.setBlock(pos, newState, 2)) {
if (world.setBlock(pos, newState, Block.UPDATE_CLIENTS)) {
var block = world.getBlockState(pos).getBlock();
if (block == oldBlock.getBlock()) {
var newTile = world.getBlockEntity(pos);
@@ -669,7 +670,7 @@ public class TurtleBrain implements TurtleAccessInternal {
}
var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
var list = world.getEntitiesOfClass(Entity.class, aabb, TurtleBrain::canPush);
var list = world.getEntities((Entity) null, aabb, TurtleBrain::canPush);
if (!list.isEmpty()) {
double pushStep = 1.0f / ANIM_DURATION;
var pushStepX = moveDir.getStepX() * pushStep;

View File

@@ -22,11 +22,10 @@ public class TurtleDetectCommand implements TurtleCommand {
var direction = this.direction.toWorldDir(turtle);
// Check if thing in front is air or not
var world = turtle.getLevel();
var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction);
var level = turtle.getLevel();
var pos = turtle.getPosition().relative(direction);
return !WorldUtil.isLiquidBlock(world, newPosition) && !world.isEmptyBlock(newPosition)
return !WorldUtil.isEmptyBlock(level.getBlockState(pos))
? TurtleCommandResult.success()
: TurtleCommandResult.failure();
}

View File

@@ -13,9 +13,9 @@ import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
public class TurtleMoveCommand implements TurtleCommand {
private final MoveDirection direction;
@@ -30,57 +30,32 @@ public class TurtleMoveCommand implements TurtleCommand {
var direction = this.direction.toWorldDir(turtle);
// Check if we can move
var oldWorld = (ServerLevel) turtle.getLevel();
var level = (ServerLevel) turtle.getLevel();
var oldPosition = turtle.getPosition();
var newPosition = oldPosition.relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, oldPosition, direction);
var canEnterResult = canEnter(turtlePlayer, oldWorld, newPosition);
if (!canEnterResult.isSuccess()) {
return canEnterResult;
}
var canEnterResult = canEnter(turtlePlayer, level, newPosition);
if (!canEnterResult.isSuccess()) return canEnterResult;
// Check existing block is air or replaceable
var state = oldWorld.getBlockState(newPosition);
if (!oldWorld.isEmptyBlock(newPosition) &&
!WorldUtil.isLiquidBlock(oldWorld, newPosition) &&
!state.canBeReplaced()) {
// Check existing block is air or replaceable.
var existingState = level.getBlockState(newPosition);
if (!(WorldUtil.isEmptyBlock(existingState) || existingState.canBeReplaced())) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check there isn't anything in the way
var collision = state.getCollisionShape(oldWorld, oldPosition).move(
newPosition.getX(),
newPosition.getY(),
newPosition.getZ()
);
if (!oldWorld.isUnobstructed(null, collision)) {
if (!Config.turtlesCanPush || this.direction == MoveDirection.UP || this.direction == MoveDirection.DOWN) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check there is space for all the pushable entities to be pushed
var list = oldWorld.getEntitiesOfClass(Entity.class, getBox(collision), x -> x != null && x.isAlive() && x.blocksBuilding);
for (var entity : list) {
var pushedBB = entity.getBoundingBox().move(
direction.getStepX(),
direction.getStepY(),
direction.getStepZ()
);
if (!oldWorld.isUnobstructed(null, Shapes.create(pushedBB))) {
return TurtleCommandResult.failure("Movement obstructed");
}
}
// Check there isn't an entity in the way.
var turtleShape = level.getBlockState(oldPosition).getCollisionShape(level, oldPosition)
.move(newPosition.getX(), newPosition.getY(), newPosition.getZ());
if (!level.isUnobstructed(null, turtleShape) && !canPushEntities(level, turtleShape.bounds())) {
return TurtleCommandResult.failure("Movement obstructed");
}
// Check fuel level
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) {
return TurtleCommandResult.failure("Out of fuel");
}
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) return TurtleCommandResult.failure("Out of fuel");
// Move
if (!turtle.teleportTo(oldWorld, newPosition)) return TurtleCommandResult.failure("Movement failed");
if (!turtle.teleportTo(level, newPosition)) return TurtleCommandResult.failure("Movement failed");
// Consume fuel
turtle.consumeFuel(1);
@@ -114,9 +89,20 @@ public class TurtleMoveCommand implements TurtleCommand {
return TurtleCommandResult.success();
}
private static AABB getBox(VoxelShape shape) {
return shape.isEmpty() ? EMPTY_BOX : shape.bounds();
}
private static final AABB EMPTY_BOX = new AABB(0, 0, 0, 0, 0, 0);
/**
* Determine if all entities in the given bounds can be pushed by the turtle.
*
* @param level The current level.
* @param bounds The bounding box.
* @return Whether all entities can be pushed.
*/
private boolean canPushEntities(Level level, AABB bounds) {
if (!Config.turtlesCanPush) return false;
// Check there is space for all the pushable entities to be pushed
return level.getEntities((Entity) null, bounds, e -> e.isAlive()
&& !e.isSpectator() && e.blocksBuilding && e.getPistonPushReaction() == PushReaction.IGNORE
).isEmpty();
}
}

View File

@@ -39,7 +39,7 @@ public final class TurtleInventoryCrafting {
}
}
var input = CraftingInput.ofPositioned(WIDTH, HEIGHT, new AbstractList<>() {
List<ItemStack> items = new AbstractList<>() {
@Override
public ItemStack get(int index) {
var x = xStart + index % WIDTH;
@@ -53,9 +53,10 @@ public final class TurtleInventoryCrafting {
public int size() {
return WIDTH * HEIGHT;
}
});
var recipe = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, input.input(), level).orElse(null);
return recipe == null ? null : new FoundRecipe(recipe.value(), input.input(), input.left() + xStart, input.top() + yStart);
};
var input = CraftingInput.of(WIDTH, HEIGHT, items);
var recipe = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, input, level).orElse(null);
return recipe == null ? null : new FoundRecipe(recipe.value(), items, xStart, yStart);
}
@Nullable
@@ -76,18 +77,24 @@ public final class TurtleInventoryCrafting {
if (maxCount == 0) return List.of();
var recipe = candidate.recipe();
var input = candidate.input();
var xStart = candidate.xStart();
var yStart = candidate.yStart();
var items = candidate.items();
var results = new ArrayList<ItemStack>();
for (var i = 0; i < maxCount && recipe.matches(input, level); i++) {
for (var i = 0; i < maxCount; i++) {
var offsetInput = CraftingInput.ofPositioned(WIDTH, HEIGHT, items);
var input = offsetInput.input();
if (!recipe.matches(input, level)) break;
var result = recipe.assemble(input, level.registryAccess());
if (result.isEmpty()) break;
results.add(result);
result.onCraftedBySystem(level);
// Remove items from the inventory, and add back the remainders.
var xStart = candidate.xStart() + offsetInput.left();
var yStart = candidate.yStart() + offsetInput.top();
var remainders = recipe.getRemainingItems(input);
for (var y = 0; y < input.height(); y++) {
for (var x = 0; x < input.width(); x++) {
@@ -118,6 +125,6 @@ public final class TurtleInventoryCrafting {
return Collections.unmodifiableList(results);
}
private record FoundRecipe(Recipe<CraftingInput> recipe, CraftingInput input, int xStart, int yStart) {
private record FoundRecipe(Recipe<CraftingInput> recipe, List<ItemStack> items, int xStart, int yStart) {
}
}

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.core.metrics.MetricsObserver;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* An immutable map of components.
*/
public final class ComponentMap {
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
private static final ComponentMap EMPTY = new ComponentMap(Map.of());
private final Map<ComputerComponent<?>, Object> components;
private ComponentMap(Map<ComputerComponent<?>, Object> components) {
this.components = components;
}
@SuppressWarnings("unchecked")
public <T> @Nullable T get(ComputerComponent<T> component) {
return (T) components.get(component);
}
public static ComponentMap empty() {
return EMPTY;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
private Builder() {
}
public <T> Builder add(ComputerComponent<T> component, T value) {
addImpl(component, value);
return this;
}
public Builder add(ComponentMap components) {
for (var component : components.components.entrySet()) addImpl(component.getKey(), component.getValue());
return this;
}
private void addImpl(ComputerComponent<?> component, Object value) {
if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
components.put(component, value);
}
public ComponentMap build() {
return new ComponentMap(Map.copyOf(components));
}
}
}

View File

@@ -44,125 +44,33 @@ public final class NBTUtil {
.ifPresent(x -> destination.put(key, x));
}
private static @Nullable Tag toNBTTag(@Nullable Object object) {
if (object == null) return null;
if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0));
if (object instanceof Number) return DoubleTag.valueOf(((Number) object).doubleValue());
if (object instanceof String) return StringTag.valueOf(object.toString());
if (object instanceof Map<?, ?> m) {
var nbt = new CompoundTag();
var i = 0;
for (Map.Entry<?, ?> entry : m.entrySet()) {
var key = toNBTTag(entry.getKey());
var value = toNBTTag(entry.getKey());
if (key != null && value != null) {
nbt.put("k" + i, key);
nbt.put("v" + i, value);
i++;
}
}
nbt.putInt("len", m.size());
return nbt;
}
return null;
}
public static @Nullable CompoundTag encodeObjects(@Nullable Object[] objects) {
if (objects == null || objects.length == 0) return null;
var nbt = new CompoundTag();
nbt.putInt("len", objects.length);
for (var i = 0; i < objects.length; i++) {
var child = toNBTTag(objects[i]);
if (child != null) nbt.put(Integer.toString(i), child);
}
return nbt;
}
private static @Nullable Object fromNBTTag(@Nullable Tag tag) {
if (tag == null) return null;
switch (tag.getId()) {
case Tag.TAG_BYTE:
return ((ByteTag) tag).getAsByte() > 0;
case Tag.TAG_DOUBLE:
return ((DoubleTag) tag).getAsDouble();
default:
case Tag.TAG_STRING:
return tag.getAsString();
case Tag.TAG_COMPOUND: {
var c = (CompoundTag) tag;
var len = c.getInt("len");
Map<Object, Object> map = new HashMap<>(len);
for (var i = 0; i < len; i++) {
var key = fromNBTTag(c.get("k" + i));
var value = fromNBTTag(c.get("v" + i));
if (key != null && value != null) map.put(key, value);
}
return map;
}
}
}
public static @Nullable Object toLua(@Nullable Tag tag) {
if (tag == null) return null;
switch (tag.getId()) {
case Tag.TAG_BYTE:
case Tag.TAG_SHORT:
case Tag.TAG_INT:
case Tag.TAG_LONG:
return ((NumericTag) tag).getAsLong();
case Tag.TAG_FLOAT:
case Tag.TAG_DOUBLE:
return ((NumericTag) tag).getAsDouble();
case Tag.TAG_STRING: // String
return tag.getAsString();
case Tag.TAG_COMPOUND: { // Compound
return switch (tag.getId()) {
case Tag.TAG_BYTE, Tag.TAG_SHORT, Tag.TAG_INT, Tag.TAG_LONG -> ((NumericTag) tag).getAsLong();
case Tag.TAG_FLOAT, Tag.TAG_DOUBLE -> ((NumericTag) tag).getAsDouble();
case Tag.TAG_STRING -> tag.getAsString();
case Tag.TAG_COMPOUND -> {
var compound = (CompoundTag) tag;
Map<String, Object> map = new HashMap<>(compound.size());
for (var key : compound.getAllKeys()) {
var value = toLua(compound.get(key));
if (value != null) map.put(key, value);
}
return map;
yield map;
}
case Tag.TAG_LIST: {
var list = (ListTag) tag;
List<Object> map = new ArrayList<>(list.size());
for (var value : list) map.add(toLua(value));
return map;
}
case Tag.TAG_BYTE_ARRAY: {
case Tag.TAG_LIST -> ((ListTag) tag).stream().map(NBTUtil::toLua).toList();
case Tag.TAG_BYTE_ARRAY -> {
var array = ((ByteArrayTag) tag).getAsByteArray();
List<Byte> map = new ArrayList<>(array.length);
for (var b : array) map.add(b);
return map;
yield map;
}
case Tag.TAG_INT_ARRAY: {
var array = ((IntArrayTag) tag).getAsIntArray();
List<Integer> map = new ArrayList<>(array.length);
for (var j : array) map.add(j);
return map;
}
default:
return null;
}
}
public static @Nullable Object[] decodeObjects(CompoundTag tag) {
var len = tag.getInt("len");
if (len <= 0) return null;
var objects = new Object[len];
for (var i = 0; i < len; i++) {
var key = Integer.toString(i);
if (tag.contains(key)) {
objects[i] = fromNBTTag(tag.get(key));
}
}
return objects;
case Tag.TAG_INT_ARRAY -> Arrays.stream(((IntArrayTag) tag).getAsIntArray()).boxed().toList();
case Tag.TAG_LONG_ARRAY -> Arrays.stream(((LongArrayTag) tag).getAsLongArray()).boxed().toList();
default -> null;
};
}
@Nullable

View File

@@ -26,7 +26,7 @@ import javax.annotation.Nullable;
public record NonNegativeId(int id) {
public static final Codec<NonNegativeId> CODEC = ExtraCodecs.NON_NEGATIVE_INT.xmap(NonNegativeId::new, NonNegativeId::id);
public static final StreamCodec<ByteBuf, NonNegativeId> STREAM_CODEC = ByteBufCodecs.INT.map(NonNegativeId::new, NonNegativeId::id);
public static final StreamCodec<ByteBuf, NonNegativeId> STREAM_CODEC = ByteBufCodecs.VAR_INT.map(NonNegativeId::new, NonNegativeId::id);
public NonNegativeId {
if (id < 0) throw new IllegalArgumentException("ID must be >= 0");

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.ConfigSpec;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import javax.annotation.Nullable;
import java.util.function.Supplier;
/**
* A data component that sets the storage capacity of a computer or disk.
* <p>
* This component is not present by default, and consumers should fall back to a globally-configured config value
* (e.g. {@link ConfigSpec#computerSpaceLimit}, {@link ConfigSpec#floppySpaceLimit}).
*
* @param capacity The capacity of this medium.
* @see ServerComputer.Properties#storageCapacity(long)
* @see ModRegistry.DataComponents#STORAGE_CAPACITY
*/
public record StorageCapacity(long capacity) {
public static final Codec<StorageCapacity> CODEC = Codec.LONG.validate(x ->
x > 0 ? DataResult.success(x) : DataResult.error(() -> "Capacity must be positive: " + x)
).xmap(StorageCapacity::new, StorageCapacity::capacity);
public static final StreamCodec<ByteBuf, StorageCapacity> STREAM_CODEC = ByteBufCodecs.VAR_LONG.map(StorageCapacity::new, StorageCapacity::capacity);
public StorageCapacity {
if (capacity <= 0) throw new IllegalArgumentException("Capacity must be > 0");
}
/**
* Get the configured capacity, or return a default value.
*
* @param capacity The capacity to get.
* @param fallback The value to fall back to. This is typically a config value.
* @return The capacity for this computer or disk.
*/
public static long getOrDefault(@Nullable StorageCapacity capacity, Supplier<Integer> fallback) {
return capacity == null ? fallback.get() : capacity.capacity();
}
/**
* Get the configured capacity, or return a default value.
*
* @param capacity The capacity to get.
* @param fallback The value to fall back to. This is typically {@code -1}.
* @return The capacity for this computer or disk.
*/
public static long getOrDefault(@Nullable StorageCapacity capacity, long fallback) {
return capacity == null ? fallback : capacity.capacity();
}
}

View File

@@ -28,9 +28,14 @@ import net.minecraft.world.phys.shapes.VoxelShape;
import javax.annotation.Nullable;
public final class WorldUtil {
@SuppressWarnings("deprecation")
public static boolean isLiquidBlock(Level world, BlockPos pos) {
if (!world.isInWorldBounds(pos)) return false;
return world.getBlockState(pos).liquid();
return world.isInWorldBounds(pos) && world.getBlockState(pos).liquid();
}
@SuppressWarnings("deprecation")
public static boolean isEmptyBlock(BlockState state) {
return state.isAir() || state.liquid();
}
public static boolean isVecInside(VoxelShape shape, Vec3 vec) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -18,6 +18,7 @@ import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.network.chat.Component;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey;
@@ -26,10 +27,10 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.world.Container;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
@@ -77,7 +78,7 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
}
@Override
public void openMenu(Player player, MenuProvider owner, ContainerData menu) {
public void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data) {
throw new UnsupportedOperationException("Cannot open menu inside tests");
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.gametest;
import com.mojang.datafixers.DataFixer;
import dan200.computercraft.gametest.core.TestHooks;
import net.minecraft.gametest.framework.GameTestServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.net.Proxy;
import java.util.concurrent.locks.LockSupport;
@Mixin(GameTestServer.class)
abstract class GameTestServerMixin extends MinecraftServer {
GameTestServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
}
/**
* Overwrite {@link GameTestServer#waitUntilNextTick()} to wait for all computers to finish executing.
* <p>
* This is a little dangerous (breaks async behaviour of computers), but it forces tests to be deterministic.
*
* @reason See above. This is only in the test mod, so no risk of collision.
* @author SquidDev.
*/
@Overwrite
@Override
public void waitUntilNextTick() {
while (true) {
runAllTasks();
if (!haveTestsStarted() || TestHooks.areComputersIdle(this)) break;
LockSupport.parkNanos(100_000);
}
}
@Shadow
private boolean haveTestsStarted() {
throw new AssertionError("Stub.");
}
}

View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.gametest
import dan200.computercraft.gametest.api.assertNoPeripheral
import dan200.computercraft.gametest.api.assertPeripheral
import dan200.computercraft.gametest.api.immediate
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.platform.ComponentAccess
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import java.util.*
/**
* Checks that we expose [ComponentAccess] for various blocks/block entities
*/
class Component_Test {
@GameTest(template = "default")
fun Peripheral(context: GameTestHelper) = context.immediate {
val pos = BlockPos(2, 2, 2)
// We fetch peripherals from the NORTH, as that is the default direction for modems. This is a bit of a hack,
// but avoids having to override the block state.
val side = Direction.NORTH
for ((block, type) in mapOf(
// Computers
ModRegistry.Blocks.COMPUTER_NORMAL to Optional.of("computer"),
ModRegistry.Blocks.COMPUTER_ADVANCED to Optional.of("computer"),
ModRegistry.Blocks.COMPUTER_COMMAND to Optional.empty(),
// Turtles
ModRegistry.Blocks.TURTLE_NORMAL to Optional.of("turtle"),
ModRegistry.Blocks.TURTLE_ADVANCED to Optional.of("turtle"),
// Peripherals
ModRegistry.Blocks.SPEAKER to Optional.of("speaker"),
ModRegistry.Blocks.DISK_DRIVE to Optional.of("drive"),
ModRegistry.Blocks.PRINTER to Optional.of("printer"),
ModRegistry.Blocks.MONITOR_NORMAL to Optional.of("monitor"),
ModRegistry.Blocks.MONITOR_ADVANCED to Optional.of("monitor"),
ModRegistry.Blocks.WIRELESS_MODEM_NORMAL to Optional.of("modem"),
ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED to Optional.of("modem"),
ModRegistry.Blocks.WIRED_MODEM_FULL to Optional.of("modem"),
ModRegistry.Blocks.REDSTONE_RELAY to Optional.of("redstone_relay"),
)) {
context.setBlock(pos, block.get())
if (type.isPresent) {
context.assertPeripheral(pos, side, type.get())
} else {
context.assertNoPeripheral(pos, side)
}
}
}
}

View File

@@ -95,17 +95,6 @@ class Computer_Test {
}
}
/**
* Check computers and turtles expose peripherals.
*/
@GameTest
fun Computer_peripheral(context: GameTestHelper) = context.sequence {
thenExecute {
context.assertPeripheral(BlockPos(3, 2, 2), type = "computer")
context.assertPeripheral(BlockPos(1, 2, 2), type = "turtle")
}
}
/**
* Check chest peripherals are reattached with a new size.
*/

View File

@@ -658,6 +658,27 @@ class Turtle_Test {
}
}
/**
* Test turtles can push entities.
*/
@GameTest
fun Move_push_entity(helper: GameTestHelper) = helper.sequence {
thenOnComputer { turtle.up().await().assertArrayEquals(true) }
thenIdle(9)
thenExecute {
// The turtle has moved up
helper.assertBlockPresent(ModRegistry.Blocks.TURTLE_NORMAL.get(), BlockPos(2, 3, 2))
// As has the villager
val pos = BlockPos(2, 4, 2)
helper.assertEntityPresent(EntityType.VILLAGER, pos)
val villager = helper.getEntity(EntityType.VILLAGER)
val expectedY = helper.absolutePos(pos).y - 0.125
if (villager.y < expectedY) helper.fail("Expected villager at y>=$expectedY, but at ${villager.y}", pos)
}
}
/**
* Test a turtle can attack an entity and capture its drops.
*/
@@ -814,6 +835,29 @@ class Turtle_Test {
}
}
/**
* `turtle.craft` works on shapeless recipes
*
* @see [#2094](https://github.com/cc-tweaked/CC-Tweaked/issues/2094)
*/
@GameTest
fun Craft_shapeless(helper: GameTestHelper) = helper.sequence {
thenExecute {
val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get())
assertTrue(TurtleCraftCommand(64).execute(turtle.access).isSuccess, "Crafting succeeded")
helper.assertContainerExactly(
BlockPos(2, 2, 2),
listOf(
ItemStack(Items.ENDER_EYE, 1), ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY,
ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY,
ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY,
ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY,
),
)
}
}
/**
* `turtle.craft` leaves a remainder
*

View File

@@ -128,6 +128,14 @@ fun GameTestHelper.sequence(run: GameTestSequence.() -> Unit) {
sequence.thenSucceed()
}
/**
* Run a function immediately, and then succeed.
*/
fun GameTestHelper.immediate(run: () -> Unit) {
run()
succeed()
}
/**
* A custom instance of [GameTestAssertPosException] which allows for longer error messages.
*/
@@ -237,15 +245,17 @@ private fun GameTestHelper.getPeripheralAt(pos: BlockPos, direction: Direction):
fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) {
val peripheral = getPeripheralAt(pos, direction)
val block = getBlockState(pos).block.name.string
when {
peripheral == null -> fail("No peripheral at position", pos)
peripheral.type != type -> fail("Peripheral is of type ${peripheral.type}, expected $type", pos)
peripheral == null -> fail("No peripheral for '$block'", pos)
peripheral.type != type -> fail("Peripheral for '$block' is of type ${peripheral.type}, expected $type", pos)
}
}
fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) {
val peripheral = getPeripheralAt(pos, direction)
if (peripheral != null) fail("Expected no peripheral, got a ${peripheral.type}", pos)
val block = getBlockState(pos).block.name
if (peripheral != null) fail("Expected no peripheral for '$block', got a ${peripheral.type}", pos)
}
fun GameTestHelper.assertExactlyItems(vararg expected: ItemStack, message: String? = null) {

View File

@@ -36,6 +36,10 @@ object ManagedComputers : ILuaMachine.Factory {
private val LOGGER = LoggerFactory.getLogger(ManagedComputers::class.java)
private val computers: MutableMap<String, Queue<suspend LuaTaskContext.() -> Unit>> = mutableMapOf()
internal fun reset() {
computers.clear()
}
internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor {
val monitor = Monitor(test, label)
computers.computeIfAbsent(label) { ConcurrentLinkedDeque() }.add {

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.gametest.core
import dan200.computercraft.api.ComputerCraftAPI
import dan200.computercraft.core.ComputerContext
import dan200.computercraft.core.computer.computerthread.ComputerThread
import dan200.computercraft.gametest.*
import dan200.computercraft.gametest.api.ClientGameTest
import dan200.computercraft.gametest.api.TestTags
@@ -24,6 +26,8 @@ import net.minecraft.world.phys.Vec3
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
@@ -80,6 +84,8 @@ object TestHooks {
StructureUtils.clearSpaceForStructure(StructureUtils.getStructureBoundingBox(structure), level)
}
ManagedComputers.reset()
// Delete server context and add one with a mutable machine factory. This allows us to set the factory for
// specific test batches without having to reset all computers.
for (computer in ServerContext.get(server).registry().computers) {
@@ -91,7 +97,11 @@ object TestHooks {
CCTestCommand.importFiles(server)
}
@JvmStatic
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
private val testClasses = listOf(
Component_Test::class.java,
Computer_Test::class.java,
CraftOs_Test::class.java,
Details_Test::class.java,
@@ -128,14 +138,6 @@ object TestHooks {
}
}
private val isCi = System.getenv("CI") != null
/**
* Adjust the timeout of a test. This makes it 1.5 times longer when run under CI, as CI servers are less powerful
* than our own.
*/
private fun adjustTimeout(timeout: Int): Int = if (isCi) timeout + (timeout / 2) else timeout
private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
val className = testClass.simpleName.lowercase()
val testName = className + "." + method.name.lowercase()
@@ -147,7 +149,7 @@ object TestHooks {
TestFunction(
testInfo.batch, testName, testInfo.template.ifEmpty { testName },
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
adjustTimeout(testInfo.timeoutTicks),
testInfo.timeoutTicks,
testInfo.setupTicks,
testInfo.required, testInfo.manualOnly,
testInfo.attempts,
@@ -167,7 +169,7 @@ object TestHooks {
testName,
testName,
testInfo.template.ifEmpty { testName },
adjustTimeout(testInfo.timeoutTicks),
testInfo.timeoutTicks,
0,
true,
) { value -> safeInvoke(method, value) },
@@ -215,3 +217,31 @@ object TestHooks {
return false
}
}
/**
* Nasty reflection to determine if computers are fully idle.
*
* This is horribly nasty, and should not be used as a model for any production code!
*
* @see [ComputerThread.isFullyIdle]
* @see [dan200.computercraft.mixin.gametest.GameTestServerMixin]
*/
private object ComputerThreadReflection {
private val lookup = MethodHandles.lookup()
@JvmField
val computerContext: MethodHandle = lookup.unreflectGetter(
ServerContext::class.java.getDeclaredField("context").also { it.isAccessible = true },
)
@JvmField
val isFullyIdle: MethodHandle = lookup.unreflect(
ComputerThread::class.java.getDeclaredMethod("isFullyIdle").also { it.isAccessible = true },
)
fun isFullyIdle(context: ServerContext): Boolean {
val computerContext = computerContext.invokeExact(context) as ComputerContext
val computerThread = computerContext.computerScheduler() as ComputerThread
return isFullyIdle.invokeExact(computerThread) as Boolean
}
}

View File

@@ -35,7 +35,7 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
* directory exists.
*/
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) {
class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
override fun save(file: File) {
try {
Files.createDirectories(file.toPath().parent)

View File

@@ -11,6 +11,7 @@
"GameTestInfoAccessor",
"GameTestSequenceAccessor",
"GameTestSequenceMixin",
"GameTestServerMixin",
"StructureTemplateManagerMixin",
"TestCommandAccessor"
],

View File

@@ -1,5 +1,5 @@
{
DataVersion: 2975,
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
@@ -34,17 +34,17 @@
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft:turtle_advanced{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_advanced"}},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:ender_pearl"}, {Count: 1b, Slot: 1b, id: "minecraft:blaze_powder"}], Label: "turtle_test.craft_shapeless", LeftUpgrade: "minecraft:crafting_table", LeftUpgradeNbt: {}, On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "computercraft:computer_advanced{facing:north,state:off}", nbt: {On: 0b, id: "computercraft:computer_advanced"}},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
@@ -132,7 +132,6 @@
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:turtle_advanced{facing:south,waterlogged:false}",
"computercraft:computer_advanced{facing:north,state:off}"
"computercraft:turtle_normal{facing:north,waterlogged:false}"
]
}

View File

@@ -0,0 +1,140 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:west,waterlogged:false}", nbt: {ComputerId: 1, Label: "turtle_test.move_push_entity", Fuel: 80, Items: [], On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 2], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [
{blockPos: [2, 1, 2], pos: [2.5d, 1.875d, 2.5d], nbt: {AbsorptionAmount: 0.0f, Age: 0, Air: 300s, ArmorDropChances: [0.085f, 0.085f, 0.085f, 0.085f], ArmorItems: [{}, {}, {}, {}], Attributes: [{Base: 0.5d, Name: "minecraft:generic.movement_speed"}, {Base: 48.0d, Modifiers: [{Amount: -0.01165046535152748d, Name: "Random spawn bonus", Operation: 1, UUID: [I; 1412502412, 1522745411, -1211155694, 2103054347]}], Name: "minecraft:generic.follow_range"}], Brain: {memories: {}}, CanPickUpLoot: 1b, DeathTime: 0s, FallDistance: 0.0f, FallFlying: 0b, Fire: -1s, FoodLevel: 0b, ForcedAge: 0, Gossips: [], HandDropChances: [0.085f, 0.085f], HandItems: [{}, {}], Health: 20.0f, HurtByTimestamp: 0, HurtTime: 0s, Inventory: [], Invulnerable: 0b, LastGossipDecay: 52357L, LastRestock: 0L, LeftHanded: 0b, Motion: [0.0d, -0.0784000015258789d, 0.0d], OnGround: 1b, PersistenceRequired: 0b, PortalCooldown: 0, Pos: [-33.5d, 58.875d, -21.5d], RestocksToday: 0, Rotation: [-102.704926f, 0.0f], UUID: [I; 164071932, -867285780, -1817215456, -2129864016], VillagerData: {level: 1, profession: "minecraft:none", type: "minecraft:desert"}, Xp: 0, id: "minecraft:villager"}}
],
palette: [
"minecraft:polished_andesite",
"minecraft:white_stained_glass",
"minecraft:air",
"computercraft:turtle_normal{facing:west,waterlogged:false}"
]
}

View File

@@ -40,6 +40,8 @@ dependencies {
}
tasks.processResources {
inputs.property("gitHash", cct.gitHash)
var props = mapOf("gitContributors" to cct.gitContributors.get().joinToString("\n"))
filesMatching("data/computercraft/lua/rom/help/credits.md") { expand(props) }
}

View File

@@ -69,7 +69,7 @@ public class WebsocketHandle {
* Send a websocket message to the connected server.
*
* @param message The message to send.
* @param binary Whether this message should be treated as a
* @param binary Whether this message should be treated as a binary message.
* @throws LuaException If the message is too large.
* @throws LuaException If the websocket has been closed.
* @cc.changed 1.81.0 Added argument for binary mode.

View File

@@ -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 {
public class Computer implements ComputerEvents.Receiver {
private static final int START_DELAY = 50;
// Various properties of the computer
@@ -114,6 +114,7 @@ public class Computer {
executor.queueStop(false, true);
}
@Override
public void queueEvent(String event, @Nullable Object[] args) {
executor.queueEvent(event, args);
}

View File

@@ -0,0 +1,72 @@
// 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 javax.annotation.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[] arguments);
}
}

View File

@@ -433,6 +433,16 @@ public final class ComputerThread implements ComputerScheduler {
return computerQueueSize() > idleWorkers.get();
}
/**
* Determine if no work is queued, and all workers are idle.
*
* @return If the threads are fully idle.
*/
@VisibleForTesting
boolean isFullyIdle() {
return computerQueueSize() == 0 && idleWorkers.get() >= workerCount();
}
private void workerFinished(WorkerThread worker) {
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
// worker finishes normally.

View File

@@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.ILuaFunction;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.lua.errorinfo.ErrorInfoLib;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.util.LuaUtil;
@@ -77,6 +78,7 @@ public class CobaltLuaMachine implements ILuaMachine {
var globals = state.globals();
CoreLibraries.debugGlobals(state);
Bit32Lib.add(state, globals);
ErrorInfoLib.add(state);
globals.rawset("_HOST", ValueFactory.valueOf(environment.hostString()));
globals.rawset("_CC_DEFAULT_SETTINGS", ValueFactory.valueOf(CoreConfig.defaultComputerSettings));

View File

@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2009-2011 Luaj.org, 2015-2020 SquidDev
//
// SPDX-License-Identifier: MIT
package dan200.computercraft.core.lua.errorinfo;
import org.squiddev.cobalt.Prototype;
import static org.squiddev.cobalt.Lua.*;
/**
* Extracted parts of Cobalt's {@link org.squiddev.cobalt.debug.DebugHelpers}.
*/
final class DebugHelpers {
private DebugHelpers() {
}
private static int filterPc(int pc, int jumpTarget) {
return pc < jumpTarget ? -1 : pc;
}
/**
* Find the PC where a register was last set.
* <p>
* This makes some assumptions about the structure of the bytecode, namely that there are no back edges within the
* CFG. As a result, this is only valid for temporary values, and not locals.
*
* @param pt The function prototype.
* @param lastPc The PC to work back from.
* @param reg The register.
* @return The last instruction where the register was set, or {@code -1} if not defined.
*/
static int findSetReg(Prototype pt, int lastPc, int reg) {
var lastInsn = -1; // Last instruction that changed "reg";
var jumpTarget = 0; // Any code before this address is conditional
for (var pc = 0; pc < lastPc; pc++) {
var i = pt.code[pc];
var op = GET_OPCODE(i);
var a = GETARG_A(i);
switch (op) {
case OP_LOADNIL -> {
var b = GETARG_B(i);
if (a <= reg && reg <= a + b) lastInsn = filterPc(pc, jumpTarget);
}
case OP_TFORCALL -> {
if (a >= a + 2) lastInsn = filterPc(pc, jumpTarget);
}
case OP_CALL, OP_TAILCALL -> {
if (reg >= a) lastInsn = filterPc(pc, jumpTarget);
}
case OP_JMP -> {
var dest = pc + 1 + GETARG_sBx(i);
// If jump is forward and doesn't skip lastPc, update jump target
if (pc < dest && dest <= lastPc && dest > jumpTarget) jumpTarget = dest;
}
default -> {
if (testAMode(op) && reg == a) lastInsn = filterPc(pc, jumpTarget);
}
}
}
return lastInsn;
}
}

View File

@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.lua.errorinfo;
import com.google.common.annotations.VisibleForTesting;
import org.squiddev.cobalt.*;
import org.squiddev.cobalt.debug.DebugFrame;
import org.squiddev.cobalt.function.LuaFunction;
import org.squiddev.cobalt.function.RegisteredFunction;
import javax.annotation.Nullable;
import java.util.Objects;
import static org.squiddev.cobalt.Lua.*;
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_ANY_HOOK;
/**
* Provides additional info about an error.
* <p>
* This is currently an internal and deeply unstable module. It's not clear if doing this via bytecode (rather than an
* AST) is the correct approach and/or, what the correct design is.
*/
public class ErrorInfoLib {
private static final int MAX_DEPTH = 8;
private static final RegisteredFunction[] functions = new RegisteredFunction[]{
RegisteredFunction.ofV("info_for_nil", ErrorInfoLib::getInfoForNil),
};
public static void add(LuaState state) throws LuaError {
state.registry().getSubTable(Constants.LOADED).rawset("cc.internal.error_info", RegisteredFunction.bind(functions));
}
private static Varargs getInfoForNil(LuaState state, Varargs args) throws LuaError {
var thread = args.arg(1).checkThread();
var level = args.arg(2).checkInteger();
var context = getInfoForNil(state, thread, level);
return context == null ? Constants.NIL : ValueFactory.varargsOf(
ValueFactory.valueOf(context.op()), ValueFactory.valueOf(context.source().isGlobal()),
context.source().table(), context.source().key()
);
}
/**
* Get some additional information about an {@code attempt to $OP (a nil value)} error. This often occurs as a
* result of a misspelled local, global or table index, and so we attempt to detect those cases.
*
* @param state The current Lua state.
* @param thread The thread which has errored.
* @param level The level where the error occurred. We currently expect this to always be 0.
* @return Some additional information about the error, where available.
*/
@VisibleForTesting
static @Nullable NilInfo getInfoForNil(LuaState state, LuaThread thread, int level) {
var frame = thread.getDebugState().getFrame(level);
if (frame == null || frame.closure == null || (frame.flags & FLAG_ANY_HOOK) != 0) return null;
var prototype = frame.closure.getPrototype();
var pc = frame.pc;
var insn = prototype.code[pc];
// Find what operation we're doing that errored.
return switch (GET_OPCODE(insn)) {
case OP_CALL, OP_TAILCALL ->
NilInfo.of("call", resolveValueSource(state, frame, prototype, pc, GETARG_A(insn), 0));
case OP_GETTABLE, OP_SETTABLE, OP_SELF ->
NilInfo.of("index", resolveValueSource(state, frame, prototype, pc, GETARG_A(insn), 0));
default -> null;
};
}
/**
* Information about an {@code attempt to $OP (a nil value)} error.
*
* @param op The operation we tried to perform.
* @param source The expression that resulted in a nil value.
*/
@VisibleForTesting
record NilInfo(String op, ValueSource source) {
public static @Nullable NilInfo of(String op, @Nullable ValueSource values) {
return values == null ? null : new NilInfo(op, values);
}
}
/**
* A partially-reconstructed Lua expression. This currently only is used for table indexing ({@code table[key]}.
*
* @param isGlobal Whether this is a global table access. This is a best-effort guess, and does not distinguish between
* {@code foo} and {@code _ENV.foo}.
* @param table The table being indexed.
* @param key The key we tried to index.
*/
@VisibleForTesting
record ValueSource(boolean isGlobal, LuaValue table, LuaString key) {
}
/**
* Attempt to partially reconstruct a Lua expression from the current debug state.
*
* @param state The current Lua state.
* @param frame The current debug frame.
* @param prototype The current function.
* @param pc The current program counter.
* @param register The register where this value was stored.
* @param depth The current depth. Starts at 0, and aborts once reaching {@link #MAX_DEPTH}.
* @return The reconstructed expression, or {@code null} if not available.
*/
@SuppressWarnings("NullTernary")
private static @Nullable ValueSource resolveValueSource(LuaState state, DebugFrame frame, Prototype prototype, int pc, int register, int depth) {
if (depth > MAX_DEPTH) return null;
if (prototype.getLocalName(register + 1, pc) != null) return null;
// Find where this register was set. If unknown, then abort.
pc = DebugHelpers.findSetReg(prototype, pc, register);
if (pc == -1) return null;
var insn = prototype.code[pc];
return switch (GET_OPCODE(insn)) {
case OP_MOVE -> {
var a = GETARG_A(insn);
var b = GETARG_B(insn); // move from `b' to `a'
yield b < a ? resolveValueSource(state, frame, prototype, pc, register, depth + 1) : null; // Resolve 'b' .
}
case OP_GETTABUP, OP_GETTABLE, OP_SELF -> {
var tableIndex = GETARG_B(insn);
var keyIndex = GETARG_C(insn);
// We're only interested in expressions of the form "foo.bar". Showing a "did you mean" hint for
// "foo[i]" isn't very useful!
if (!ISK(keyIndex)) yield null;
var key = prototype.constants[INDEXK(keyIndex)];
if (key.type() != Constants.TSTRING) yield null;
var table = GET_OPCODE(insn) == OP_GETTABUP
? frame.closure.getUpvalue(tableIndex).getValue()
: evaluate(state, frame, prototype, pc, tableIndex, depth);
if (table == null) yield null;
var isGlobal = GET_OPCODE(insn) == OP_GETTABUP && Objects.equals(prototype.getUpvalueName(tableIndex), Constants.ENV);
yield new ValueSource(isGlobal, table, (LuaString) key);
}
default -> null;
};
}
/**
* Attempt to reconstruct the value of a register.
*
* @param state The current Lua state.
* @param frame The current debug frame.
* @param prototype The current function
* @param pc The PC to evaluate at.
* @param register The register to evaluate.
* @param depth The current depth. Starts at 0, and aborts once reaching {@link #MAX_DEPTH}.
* @return The reconstructed value, or {@code null} if unavailable.
*/
@SuppressWarnings("NullTernary")
private static @Nullable LuaValue evaluate(LuaState state, DebugFrame frame, Prototype prototype, int pc, int register, int depth) {
if (depth >= MAX_DEPTH) return null;
// If this is a local, then return its contents.
if (prototype.getLocalName(register + 1, pc) != null) return frame.stack[register];
// Otherwise find where this register was set. If unknown, then abort.
pc = DebugHelpers.findSetReg(prototype, pc, register);
if (pc == -1) return null;
var insn = prototype.code[pc];
var opcode = GET_OPCODE(insn);
return switch (opcode) {
case OP_MOVE -> {
var a = GETARG_A(insn);
var b = GETARG_B(insn); // move from `b' to `a'
yield b < a ? evaluate(state, frame, prototype, pc, register, depth + 1) : null; // Resolve 'b'.
}
// Load constants
case OP_LOADK -> prototype.constants[GETARG_Bx(insn)];
case OP_LOADKX -> prototype.constants[GETARG_Ax(prototype.code[pc + 1])];
case OP_LOADBOOL -> GETARG_B(insn) == 0 ? Constants.FALSE : Constants.TRUE;
case OP_LOADNIL -> Constants.NIL;
// Upvalues and tables.
case OP_GETUPVAL -> frame.closure.getUpvalue(GETARG_B(insn)).getValue();
case OP_GETTABLE, OP_GETTABUP -> {
var table = opcode == OP_GETTABUP
? frame.closure.getUpvalue(GETARG_B(insn)).getValue()
: evaluate(state, frame, prototype, pc, GETARG_B(insn), depth + 1);
if (table == null) yield null;
var key = evaluateK(state, frame, prototype, pc, GETARG_C(insn), depth + 1);
yield key == null ? null : safeIndex(state, table, key);
}
default -> null;
};
}
private static @Nullable LuaValue evaluateK(LuaState state, DebugFrame frame, Prototype prototype, int pc, int registerOrConstant, int depth) {
return ISK(registerOrConstant) ? prototype.constants[INDEXK(registerOrConstant)] : evaluate(state, frame, prototype, pc, registerOrConstant, depth + 1);
}
private static @Nullable LuaValue safeIndex(LuaState state, LuaValue table, LuaValue key) {
var loop = 0;
do {
LuaValue metatable;
if (table instanceof LuaTable tbl) {
var res = tbl.rawget(key);
if (!res.isNil() || (metatable = tbl.metatag(state, CachedMetamethod.INDEX)).isNil()) return res;
} else if ((metatable = table.metatag(state, CachedMetamethod.INDEX)).isNil()) {
return null;
}
if (metatable instanceof LuaFunction) return null;
table = metatable;
}
while (++loop < Constants.MAXTAGLOOP);
return null;
}
}

View File

@@ -4,52 +4,118 @@
package dan200.computercraft.core.util;
import dan200.computercraft.core.computer.ComputerEvents;
import java.nio.ByteBuffer;
public final class StringUtil {
public static final int MAX_PASTE_LENGTH = 512;
private StringUtil() {
}
private static boolean isAllowed(char c) {
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255);
}
private static String removeSpecialCharacters(String text, int length) {
var builder = new StringBuilder(length);
for (var i = 0; i < length; i++) {
var c = text.charAt(i);
builder.append(isAllowed(c) ? c : '?');
/**
* Convert a Unicode character to a terminal one.
*
* @param chr The Unicode character.
* @return The terminal character. This is either in the range [0, 255] (if a valid character) or {@code -1} if
* it cannot be mapped to CC's charset.
*/
public static int unicodeToTerminal(int chr) {
// ASCII and latin1 map to themselves
if (chr == 0 || chr == '\t' || chr == '\n' || chr == '\r' || (chr >= ' ' && chr <= '~') || (chr >= 160 && chr <= 255)) {
return chr;
}
return builder.toString();
// Teletext block mosaics are *fairly* contiguous.
if (chr >= 0x1FB00 && chr <= 0x1FB13) return chr + (129 - 0x1fb00);
if (chr >= 0x1FB14 && chr <= 0x1FB1D) return chr + (150 - 0x1fb14);
// Everything else is just a manual lookup. For now, we just use a big switch statement, which we spin into a
// separate function to hopefully avoid inlining it here.
return unicodeToCraftOsFallback(chr);
}
public static String normaliseLabel(String text) {
return removeSpecialCharacters(text, Math.min(32, text.length()));
private static int unicodeToCraftOsFallback(int c) {
return switch (c) {
case 0x263A -> 1;
case 0x263B -> 2;
case 0x2665 -> 3;
case 0x2666 -> 4;
case 0x2663 -> 5;
case 0x2660 -> 6;
case 0x2022 -> 7;
case 0x25D8 -> 8;
case 0x2642 -> 11;
case 0x2640 -> 12;
case 0x266A -> 14;
case 0x266B -> 15;
case 0x25BA -> 16;
case 0x25C4 -> 17;
case 0x2195 -> 18;
case 0x203C -> 19;
case 0x25AC -> 22;
case 0x21A8 -> 23;
case 0x2191 -> 24;
case 0x2193 -> 25;
case 0x2192 -> 26;
case 0x2190 -> 27;
case 0x221F -> 28;
case 0x2194 -> 29;
case 0x25B2 -> 30;
case 0x25BC -> 31;
case 0x1FB99 -> 127;
case 0x258C -> 149;
default -> -1;
};
}
/**
* Normalise a string from the clipboard, suitable for pasting into a computer.
* Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte)
* "char" event}.
*
* @param chr The character to check.
* @return Whether this character can be typed.
*/
public static boolean isTypableChar(int chr) {
return chr >= 0 && chr <= 255 && chr != 0 && chr != '\r' && chr != '\n';
}
private static boolean isAllowedInLabel(char c) {
// Limit to ASCII and latin1, excluding '§' (Minecraft's formatting character).
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 255 && c != 167);
}
public static String normaliseLabel(String text) {
var length = Math.min(32, text.length());
var builder = new StringBuilder(length);
for (var i = 0; i < length; i++) {
var c = text.charAt(i);
builder.append(isAllowedInLabel(c) ? c : '?');
}
return builder.toString();
}
/**
* Convert a Java string to a Lua one (using the terminal charset), suitable for pasting into a computer.
* <p>
* This removes special characters and strips to the first line of text.
*
* @param clipboard The text from the clipboard.
* @return The normalised clipboard text.
* @return The encoded clipboard text.
*/
public static String normaliseClipboardString(String clipboard) {
// Clip to the first occurrence of \r or \n
var newLineIndex1 = clipboard.indexOf('\r');
var newLineIndex2 = clipboard.indexOf('\n');
public static ByteBuffer getClipboardString(String clipboard) {
var output = new byte[Math.min(MAX_PASTE_LENGTH, clipboard.length())];
var idx = 0;
int length;
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
length = Math.min(newLineIndex1, newLineIndex2);
} else if (newLineIndex1 >= 0) {
length = newLineIndex1;
} else if (newLineIndex2 >= 0) {
length = newLineIndex2;
} else {
length = clipboard.length();
var iterator = clipboard.codePoints().iterator();
while (iterator.hasNext() && idx <= output.length) {
var chr = unicodeToTerminal(iterator.next());
if (chr < 0) continue; // Strip out unconvertible characters
if (!isTypableChar(chr)) break; // Stop at untypable ones.
output[idx++] = (byte) chr;
}
return removeSpecialCharacters(clipboard, Math.min(length, 512));
return ByteBuffer.wrap(output, 0, idx).asReadOnlyBuffer();
}
}

Some files were not shown because too many files have changed in this diff Show More