1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-16 22:47:38 +00:00

Compare commits

..

13 Commits

Author SHA1 Message Date
Jonathan Coates
aaf8c248a8 Bump CC:T to 1.106.1 2023-07-08 09:37:43 +01:00
Jonathan Coates
df26cd267a Fix conflicts with other mods replacing reach distance 2023-07-08 09:35:06 +01:00
Jonathan Coates
8914b78816 Also block the CGNAT range (100.64.0.0/10) 2023-07-08 09:27:09 +01:00
Jonathan Coates
9ea7f45fa7 Fix class cast exception with ObjectSource
We were generating methods with the original object, rather than the
extra one.

Updated our tests to actually catch this. Unfortunately the only places
we use this interface is in HTTP responses and transferred files,
neither of which show up in the Lua-side tests.
2023-07-07 18:05:02 +01:00
Jonathan Coates
d351bc33c6 Bump CC:T to 1.106.0 2023-07-07 00:16:48 +01:00
Jonathan Coates
5d71770931 Several command permission fixes
- Attach permission checks to the first argument (so the literal
   command name) rather than the last argument. This fixes commands
   showing up when they shouldn't.

 - HelpingArgumentBuilder now inherits permissions of its leaf nodes.
   This only really impacts the "track" subcommand.

 - Don't autocomplete the computer selector for the "queue" subcommand.
   As everyone has permission for this command, it's possible to find
   all computer ids and labels in the world.

   I'm in mixed minds about this, but don't think this is an exploit -
   computer ids/labels are sent to in-range players so shouldn't be
   considered secret - but worth patching none-the-less.
2023-07-06 23:16:22 +01:00
Jonathan Coates
4bbde8c50c Tighten up the $private HTTP rule
- Block multicast and the fd00::/8 address ranges.
 - Block several cloud metadata providers which sit outside the standard
   address ranges.
2023-07-06 23:16:22 +01:00
Jonathan Coates
cc8c1f38e7 Small documentation improvements
- Document that settings.set doesn't persist values. I think this
   closes #1512 - haven't heard back from them.

 - Add missing close reasons to the websocket_closed event. Closes #1493.

 - Mention what values are preserved by os.queueEvent. This is just the
   same as modem.transmit. Closes #1490.
2023-07-06 23:03:22 +01:00
Jonathan Coates
cab9c9772a Add some tests for new turtle tool functionality
- Normalise upgrade keys, to be "allowEnchantments" and
   "consumeDurability". We were previously inconsistent with
   allow/allows and consumes.
 - Add tests for durability and enchantments of pickaxes.
 - Fix a couple of issues with the original upgrade NBT being modified.
 - Now store the item's tag under a separate key rather than on the
   root. This makes syncing the NBT between the two much nicer.
2023-07-06 22:28:56 +01:00
Jonathan Coates
e337a63712 Bump FG and Loom
Also split up CI steps a little, so that it's more clear what failed.
2023-07-05 20:58:15 +01:00
Weblate
efa92b741b Translations for Russian (ru_ru)
Translations for French

Co-authored-by: chesiren <chesiren63@gmail.com>
Co-authored-by: ego-rick <vsevidcev@gmail.com>
2023-07-04 23:03:03 +00:00
Jonathan Coates
a91ac6f214 Make turtle tools a little more flexible
Turtle tools now accept two additional JSON fields

 - allowEnchantments: Whether items with enchantments (or any
   non-standard NBT) can be equipped.
 - consumesDurability: Whether durability will be consumed. This can be
   "never" (the current and default behaviour), "always", and
   "when_enchanted".

Closes #1501.
2023-07-03 22:14:31 +01:00
PenguinEncounter
943a9406b1 Fix turtle.refuel example (#1510) 2023-07-02 17:19:09 +00:00
52 changed files with 1106 additions and 727 deletions

View File

@@ -8,16 +8,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone repository
- name: 📥 Clone repository
uses: actions/checkout@v3
- name: Set up Java
- name: 📥 Set up Java
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'temurin'
- name: Setup Gradle
- name: 📥 Setup Gradle
uses: gradle/gradle-build-action@v2
with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}
@@ -27,42 +27,45 @@ jobs:
mkdir -p ~/.gradle
echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties
- name: Build with Gradle
- name: ⚒️ Build
run: ./gradlew assemble || ./gradlew assemble
- name: Download assets for game tests
- name: 💡 Lint
uses: pre-commit/action@v3.0.0
- name: 🧪 Run tests
run: ./gradlew test validateMixinNames checkChangelog
- name: 📥 Download assets for game tests
run: ./gradlew downloadAssets || ./gradlew downloadAssets
- name: Run tests and linters
run: ./gradlew build
- name: 🧪 Run integration tests
run: ./gradlew runGametest
- name: Run client tests
- name: 🧪 Run client tests
run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests.
# These are a little flaky on GH actions: its useful to run them, but don't break the build.
continue-on-error: true
- name: Prepare Jars
- name: 🧪 Parse test reports
run: ./tools/parse-reports.py
if: ${{ failure() }}
- name: 📦 Prepare Jars
run: |
# Find the main jar and append the git hash onto it.
mkdir -p jars
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
- name: Upload Jar
- name: 📤 Upload Jar
uses: actions/upload-artifact@v3
with:
name: CC-Tweaked
path: ./jars
- name: Upload coverage
- name: 📤 Upload coverage
uses: codecov/codecov-action@v3
- name: Parse test reports
run: ./tools/parse-reports.py
if: ${{ failure() }}
- name: Run linters
uses: pre-commit/action@v3.0.0
build-core:
strategy:
fail-fast: false

View File

@@ -6,6 +6,7 @@ Upstream-Contact: Jonathan Coates <git@squiddev.cc>
Files:
projects/common/src/main/resources/assets/computercraft/sounds.json
projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg
projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/*
projects/common/src/testMod/resources/data/cctest/structures/*
projects/fabric/src/generated/*
projects/forge/src/generated/*

View File

@@ -59,7 +59,6 @@ repositories {
includeGroup("org.squiddev")
includeGroup("cc.tweaked")
// Things we mirror
includeGroup("alexiil.mc.lib")
includeGroup("dev.architectury")
includeGroup("maven.modrinth")
includeGroup("me.shedaniel")

View File

@@ -49,7 +49,6 @@ fun JavaExec.copyToFull(spec: JavaExec) {
* Copy additional [BaseExecSpec] options which aren't handled by [ProcessForkOptions.copyTo].
*/
fun BaseExecSpec.copyToExec(spec: BaseExecSpec) {
spec.workingDir = workingDir
spec.isIgnoreExitValue = isIgnoreExitValue
if (standardInput != null) spec.standardInput = standardInput
if (standardOutput != null) spec.standardOutput = standardOutput

View File

@@ -30,41 +30,22 @@ internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config:
val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java)
// Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts.
fun lazyTokens(): MutableMap<String, Supplier<String>> {
return RunConfigGenerator.configureTokensLazy(
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
originalTask.get().minecraftArtifacts.files,
originalTask.get().runtimeClasspathArtifacts.files,
)
}
val lazyTokens = RunConfigGenerator.configureTokensLazy(
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
originalTask.get().minecraftArtifacts,
originalTask.get().runtimeClasspathArtifacts,
)
spec.argumentProviders.add(
CommandLineArgumentProvider {
RunConfigGenerator.getArgsStream(config, lazyTokens(), false).toList()
RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()
},
)
spec.jvmArgumentProviders.add(
CommandLineArgumentProvider {
val lazyTokens = lazyTokens()
(if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } +
config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" }
},
)
// We can't configure environment variables lazily, so we do these now with a more minimal lazyTokens set.
val lazyTokens = mutableMapOf<String, Supplier<String>>()
for ((k, v) in config.tokens) lazyTokens[k] = Supplier<String> { v }
for ((k, v) in config.lazyTokens) lazyTokens[k] = v
lazyTokens.compute(
"source_roots",
{ _, sourceRoots ->
Supplier<String> {
val modClasses = RunConfigGenerator.mapModClassesToGradle(project, config)
(when (sourceRoots) {
null -> modClasses
else -> Stream.concat<String>(sourceRoots.get().split(File.pathSeparator).stream(), modClasses)
}).distinct().collect(Collectors.joining(File.pathSeparator))
}
},
)
for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value))
}

View File

@@ -13,6 +13,15 @@ The @{websocket_closed} event is fired when an open WebSocket connection is clos
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the WebSocket that was closed.
3. <span class="type">@{string}|@{nil}</span>: The [server-provided reason][close_reason]
the websocket was closed. This will be @{nil} if the connection was closed
abnormally.
4. <span class="type">@{number}|@{nil}</span>: The [connection close code][close_code],
indicating why the socket was closed. This will be @{nil} if the connection
was closed abnormally.
[close_reason]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 "The WebSocket Connection Close Reason, RFC 6455"
[close_code]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 "The WebSocket Connection Close Code, RFC 6455"
## Example
Prints a message when a WebSocket is closed (this may take a minute):

View File

@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties
isUnstable=false
modVersion=1.105.0
modVersion=1.106.1
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.19.4

View File

@@ -35,7 +35,6 @@ slf4j = "1.7.36"
# Minecraft mods
iris = "1.5.2+1.19.4"
jei = "13.1.0.11"
libmultipart = "0.10.0"
modmenu = "6.1.0-rc.1"
oculus = "1.2.5"
rei = "10.0.578"
@@ -54,8 +53,8 @@ checkstyle = "10.3.4"
curseForgeGradle = "1.0.14"
errorProne-core = "2.18.0"
errorProne-plugin = "3.0.1"
fabric-loom = "1.2.7"
forgeGradle = "6.0.6"
fabric-loom = "1.3.7"
forgeGradle = "6.0.8"
githubRelease = "2.2.12"
ideaExt = "1.1.6"
illuaminate = "0.1.0-28-ga7efd71"
@@ -97,7 +96,6 @@ iris = { module = "maven.modrinth:iris", version.ref = "iris" }
jei-api = { module = "mezz.jei:jei-1.19.4-common-api", version.ref = "jei" }
jei-fabric = { module = "mezz.jei:jei-1.19.4-fabric", version.ref = "jei" }
jei-forge = { module = "mezz.jei:jei-1.19.4-forge", version.ref = "jei" }
libmultipart = { module = "alexiil.mc.lib:libmultipart-all", version.ref = "libmultipart" }
mixin = { module = "org.spongepowered:mixin", version.ref = "mixin" }
modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" }
oculus = { module = "maven.modrinth:oculus", version.ref = "oculus" }
@@ -154,7 +152,7 @@ externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"]
externalMods-forge-compile = ["oculus", "jei-api"]
externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric = ["nightConfig-core", "nightConfig-toml"]
externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin", "libmultipart"]
externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin"]
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
# Testing

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.turtle;
import net.minecraft.util.StringRepresentable;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
/**
* Indicates if an equipped turtle item will consume durability.
*
* @see TurtleUpgradeDataProvider.ToolBuilder#consumeDurability(TurtleToolDurability)
*/
public enum TurtleToolDurability implements StringRepresentable {
/**
* The equipped tool always consumes durability when using.
*/
ALWAYS("always"),
/**
* The equipped tool consumes durability if it is {@linkplain ItemStack#isEnchanted() enchanted} or has
* {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
*/
WHEN_ENCHANTED("when_enchanted"),
/**
* The equipped tool never consumes durability. Tools which have been damaged cannot be used as upgrades.
*/
NEVER("never");
private final String serialisedName;
/**
* The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s.
*/
public static final StringRepresentable.EnumCodec<TurtleToolDurability> CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values);
TurtleToolDurability(String serialisedName) {
this.serialisedName = serialisedName;
}
@Override
public String getSerializedName() {
return serialisedName;
}
}

View File

@@ -13,8 +13,10 @@ import net.minecraft.data.DataGenerator;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import javax.annotation.Nullable;
@@ -61,6 +63,8 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
private @Nullable Item craftingItem;
private @Nullable Float damageMultiplier = null;
private @Nullable TagKey<Block> breakable;
private boolean allowEnchantments = false;
private TurtleToolDurability consumeDurability = TurtleToolDurability.NEVER;
ToolBuilder(ResourceLocation id, TurtleUpgradeSerialiser<?> serialiser, Item toolItem) {
this.id = id;
@@ -104,6 +108,28 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
return this;
}
/**
* Indicate that this upgrade allows items which have been {@linkplain ItemStack#isEnchanted() enchanted} or have
* {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
*
* @return The tool builder, for further use.
*/
public ToolBuilder allowEnchantments() {
allowEnchantments = true;
return this;
}
/**
* Set when the tool will consume durability.
*
* @param durability The durability predicate.
* @return The tool builder, for further use.
*/
public ToolBuilder consumeDurability(TurtleToolDurability durability) {
consumeDurability = durability;
return this;
}
/**
* Provide a list of breakable blocks. If not given, the tool can break all blocks. If given, only blocks
* in this tag, those in {@link ComputerCraftTags.Blocks#TURTLE_ALWAYS_BREAKABLE} and "insta-mine" ones can
@@ -132,6 +158,10 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
}
if (damageMultiplier != null) s.addProperty("damageMultiplier", damageMultiplier);
if (breakable != null) s.addProperty("breakable", breakable.location().toString());
if (allowEnchantments) s.addProperty("allowEnchantments", true);
if (consumeDurability != TurtleToolDurability.NEVER) {
s.addProperty("consumeDurability", consumeDurability.getSerializedName());
}
}));
}
}

View File

@@ -6,9 +6,12 @@ package dan200.computercraft.shared.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -169,7 +172,10 @@ public final class CommandComputerCraft {
.then(command("queue")
.requires(UserLevel.ANYONE)
.arg("computer", manyComputers())
.arg(
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
.suggests((context, builder) -> Suggestions.empty())
)
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
.executes((ctx, args) -> {
var computers = getComputersArgument(ctx, "computer");

View File

@@ -49,6 +49,29 @@ public enum UserLevel implements Predicate<CommandSourceStack> {
return source.hasPermission(toLevel());
}
/**
* Take the union of two {@link UserLevel}s.
* <p>
* This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a b).test(s)}.
*
* @param left The first user level to take the union of.
* @param right The second user level to take the union of.
* @return The union of two levels.
*/
public static UserLevel union(UserLevel left, UserLevel right) {
if (left == right) return left;
// x ANYONE = ANYONE
if (left == ANYONE || right == ANYONE) return ANYONE;
// x OWNER = OWNER
if (left == OWNER) return right;
if (right == OWNER) return left;
// At this point, we have x != y and x, y { OP, OWNER_OP }.
return OWNER_OP;
}
private static boolean isOwner(CommandSourceStack source) {
var server = source.getServer();
var sender = source.getEntity();

View File

@@ -48,11 +48,15 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
return this;
}
public CommandBuilder<S> arg(String name, ArgumentType<?> type) {
args.add(RequiredArgumentBuilder.argument(name, type));
public CommandBuilder<S> arg(ArgumentBuilder<S, ?> arg) {
args.add(arg);
return this;
}
public CommandBuilder<S> arg(String name, ArgumentType<?> type) {
return arg(RequiredArgumentBuilder.argument(name, type));
}
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, List<T> empty) {
return argMany(name, type, () -> empty);
}
@@ -74,7 +78,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
return command -> {
// The node for no arguments
var tail = tail(ctx -> command.run(ctx, empty.get()));
var tail = setupTail(ctx -> command.run(ctx, empty.get()));
// The node for one or more arguments
ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder
@@ -83,7 +87,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
// Chain all of them together!
tail.then(moreArg);
return link(tail);
return buildTail(tail);
};
}
@@ -94,20 +98,16 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
@Override
public CommandNode<S> executes(Command<S> command) {
if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder");
return link(tail(command));
return buildTail(setupTail(command));
}
private ArgumentBuilder<S, ?> tail(Command<S> command) {
var defaultTail = args.get(args.size() - 1);
defaultTail.executes(command);
if (requires != null) defaultTail.requires(requires);
return defaultTail;
private ArgumentBuilder<S, ?> setupTail(Command<S> command) {
return args.get(args.size() - 1).executes(command);
}
private CommandNode<S> link(ArgumentBuilder<S, ?> tail) {
private CommandNode<S> buildTail(ArgumentBuilder<S, ?> tail) {
for (var i = args.size() - 2; i >= 0; i--) tail = args.get(i).then(tail);
if (requires != null) tail.requires(requires);
return tail.build();
}
}

View File

@@ -10,6 +10,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import dan200.computercraft.shared.command.UserLevel;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.ClickEvent;
@@ -18,6 +19,8 @@ import net.minecraft.network.chat.Component;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
@@ -37,6 +40,29 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
return new HelpingArgumentBuilder(literal);
}
@Override
public LiteralArgumentBuilder<CommandSourceStack> requires(Predicate<CommandSourceStack> requirement) {
throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder");
}
@Override
public Predicate<CommandSourceStack> getRequirement() {
// The requirement of this node is the union of all child's requirements.
var requirements = Stream.concat(
children.stream().map(ArgumentBuilder::getRequirement),
getArguments().stream().map(CommandNode::getRequirement)
).toList();
// If all requirements are a UserLevel, take the union of those instead.
var userLevel = UserLevel.OWNER;
for (var requirement : requirements) {
if (!(requirement instanceof UserLevel level)) return x -> requirements.stream().anyMatch(y -> y.test(x));
userLevel = UserLevel.union(userLevel, level);
}
return userLevel;
}
@Override
public LiteralArgumentBuilder<CommandSourceStack> executes(final Command<CommandSourceStack> command) {
throw new IllegalStateException("Cannot use executes on a HelpingArgumentBuilder");
@@ -80,9 +106,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
helpCommand.node = node;
// Set up a /... help command
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help")
.requires(x -> getArguments().stream().anyMatch(y -> y.getRequirement().test(x)))
.executes(helpCommand);
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help").executes(helpCommand);
// Add all normal command children to this and the help node
for (var child : getArguments()) {

View File

@@ -11,7 +11,7 @@ import it.unimi.dsi.fastutil.ints.IntSet;
import javax.annotation.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModemState {
public class ModemState {
private final @Nullable Runnable onChanged;
private final AtomicBoolean changed = new AtomicBoolean(true);
@@ -69,30 +69,4 @@ public final class ModemState {
setOpen(false);
}
}
/**
* Copy this modem state, returning a new instance. The new instance will have the same set of open channels but no
* on-change listener.
*
* @return The new modem state.
*/
public ModemState copy() {
return copy(null);
}
/**
* Copy this modem state, returning a new instance. The new instance will have the same set of open channels and a
* different on-change listener.
*
* @param onChanged The on-change listener.
* @return The new modem state.
*/
public ModemState copy(@Nullable Runnable onChanged) {
synchronized (channels) {
var clone = onChanged == null ? new ModemState() : new ModemState(onChanged);
clone.channels.addAll(channels);
clone.open = open;
return clone;
}
}
}

View File

@@ -18,7 +18,7 @@ import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
public final class WirelessModemBlockEntity extends BlockEntity {
public class WirelessModemBlockEntity extends BlockEntity {
private static class Peripheral extends WirelessModemPeripheral {
private final WirelessModemBlockEntity entity;
@@ -96,10 +96,6 @@ public final class WirelessModemBlockEntity extends BlockEntity {
}
}
public ModemState getModemState() {
return modem.getModemState();
}
@Nullable
public IPeripheral getPeripheral(@Nullable Direction direction) {
return direction == null || getDirection() == direction ? modem : null;

View File

@@ -555,7 +555,7 @@ public class TurtleAPI implements ILuaAPI {
* @cc.usage Refuel a turtle from the currently selected slot.
* <pre>{@code
* local level = turtle.getFuelLevel()
* if new_level == "unlimited" then error("Turtle does not need fuel", 0) end
* if level == "unlimited" then error("Turtle does not need fuel", 0) end
*
* local ok, err = turtle.refuel()
* if ok then

View File

@@ -15,7 +15,6 @@ import dan200.computercraft.api.turtle.TurtleCommand;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -35,7 +34,6 @@ import net.minecraft.tags.FluidTags;
import net.minecraft.world.Container;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.material.PushReaction;
@@ -465,23 +463,6 @@ public class TurtleBrain implements TurtleAccessInternal {
}
}
public @Nullable DyeColor getDyeColour() {
if (colourHex == -1) return null;
var colour = Colour.fromHex(colourHex);
return colour == null ? null : DyeColor.byId(15 - colour.ordinal());
}
public void setDyeColour(@Nullable DyeColor dyeColour) {
var newColour = -1;
if (dyeColour != null) {
newColour = Colour.values()[15 - dyeColour.getId()].getHex();
}
if (colourHex != newColour) {
colourHex = newColour;
BlockEntityHelpers.updateBlock(owner);
}
}
@Override
public void setColour(int colour) {
if (colour >= 0 && colour <= 0xFFFFFF) {

View File

@@ -74,19 +74,7 @@ public class TurtlePlaceCommand implements TurtleCommand {
}
}
public static boolean deployCopiedItem(
ItemStack stack, ITurtleAccess turtle, Direction direction, @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) {
// Create a fake player, and orient it appropriately
var playerPosition = turtle.getPosition().relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, playerPosition, direction);
turtlePlayer.loadInventory(stack);
var result = deploy(stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage);
turtlePlayer.player().getInventory().clearContent();
return result;
}
private static boolean deploy(
public static boolean deploy(
ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction,
@Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) {

View File

@@ -59,7 +59,7 @@ class UpgradeContainer implements Container {
private ItemStack setUpgradeStack(int slot, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getUpgradeItem();
lastUpgrade.set(slot, upgrade);
lastUpgrade.set(slot, UpgradeData.copyOf(upgrade));
lastStack.set(slot, stack);
return stack;
}

View File

@@ -17,14 +17,19 @@ import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.MobType;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
@@ -33,61 +38,127 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.EntityHitResult;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.function.Function;
import static net.minecraft.nbt.Tag.TAG_COMPOUND;
import static net.minecraft.nbt.Tag.TAG_LIST;
public class TurtleTool extends AbstractTurtleUpgrade {
protected static final TurtleCommandResult UNBREAKABLE = TurtleCommandResult.failure("Cannot break unbreakable block");
protected static final TurtleCommandResult INEFFECTIVE = TurtleCommandResult.failure("Cannot break block with this tool");
private static final TurtleCommandResult UNBREAKABLE = TurtleCommandResult.failure("Cannot break unbreakable block");
private static final TurtleCommandResult INEFFECTIVE = TurtleCommandResult.failure("Cannot break block with this tool");
private static final String TAG_ITEM_TAG = "Tag";
final ItemStack item;
final float damageMulitiplier;
@Nullable
final TagKey<Block> breakable;
final boolean allowEnchantments;
final TurtleToolDurability consumeDurability;
final @Nullable TagKey<Block> breakable;
public TurtleTool(ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier, @Nullable TagKey<Block> breakable) {
public TurtleTool(
ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier,
boolean allowEnchantments, TurtleToolDurability consumeDurability, @Nullable TagKey<Block> breakable
) {
super(id, TurtleUpgradeType.TOOL, adjective, new ItemStack(craftItem));
item = toolItem;
this.damageMulitiplier = damageMulitiplier;
this.allowEnchantments = allowEnchantments;
this.consumeDurability = consumeDurability;
this.breakable = breakable;
}
@Override
public boolean isItemSuitable(ItemStack stack) {
var tag = stack.getTag();
if (tag == null || tag.isEmpty()) return true;
// Check we've not got anything vaguely interesting on the item. We allow other mods to add their
// own NBT, with the understanding such details will be lost to the mist of time.
if (stack.isDamaged() || stack.isEnchanted()) return false;
if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
return false;
}
if (consumeDurability == TurtleToolDurability.NEVER && stack.isDamaged()) return false;
if (!allowEnchantments && isEnchanted(stack)) return false;
return true;
}
private static boolean isEnchanted(ItemStack stack) {
return !stack.isEmpty() && isEnchanted(stack.getTag());
}
private static boolean isEnchanted(@Nullable CompoundTag tag) {
if (tag == null || tag.isEmpty()) return false;
return (tag.contains(ItemStack.TAG_ENCH, TAG_LIST) && !tag.getList(ItemStack.TAG_ENCH, TAG_COMPOUND).isEmpty())
|| (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty());
}
@Override
public CompoundTag getUpgradeData(ItemStack stack) {
// Just use the current item's tag.
var upgradeData = super.getUpgradeData(stack);
// Store the item's current tag.
var itemTag = stack.getTag();
return itemTag == null ? new CompoundTag() : itemTag;
if (itemTag != null) upgradeData.put(TAG_ITEM_TAG, itemTag);
return upgradeData;
}
@Override
public ItemStack getUpgradeItem(CompoundTag upgradeData) {
// Copy upgrade data back to the item.
var item = super.getUpgradeItem(upgradeData);
if (!upgradeData.isEmpty()) item.setTag(upgradeData);
var item = super.getUpgradeItem(upgradeData).copy();
item.setTag(upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) ? upgradeData.getCompound(TAG_ITEM_TAG).copy() : null);
return item;
}
private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) {
return getUpgradeItem(turtle.getUpgradeNBTData(side));
}
private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) {
var upgradeData = turtle.getUpgradeNBTData(side);
var useDurability = switch (consumeDurability) {
case NEVER -> false;
case WHEN_ENCHANTED ->
upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) && isEnchanted(upgradeData.getCompound(TAG_ITEM_TAG));
case ALWAYS -> true;
};
if (!useDurability) return;
// If the tool has broken, remove the upgrade!
if (stack.isEmpty()) {
turtle.setUpgradeWithData(side, null);
return;
}
// If the tool has changed, no clue what's going on.
if (stack.getItem() != item.getItem()) return;
var itemTag = stack.getTag();
// Early return if the item hasn't changed to avoid redundant syncs with the client.
if (Objects.equals(itemTag, upgradeData.get(TAG_ITEM_TAG))) return;
if (itemTag == null) {
upgradeData.remove(TAG_ITEM_TAG);
} else {
upgradeData.put(TAG_ITEM_TAG, itemTag);
}
turtle.updateUpgradeNBTData(side);
}
private <T> T withEquippedItem(ITurtleAccess turtle, TurtleSide side, Direction direction, Function<TurtlePlayer, T> action) {
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtle.getPosition(), direction);
turtlePlayer.loadInventory(getToolStack(turtle, side));
var result = action.apply(turtlePlayer);
setToolStack(turtle, side, turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND));
turtlePlayer.player().getInventory().clearContent();
return result;
}
@Override
public TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, TurtleVerb verb, Direction direction) {
return switch (verb) {
case ATTACK -> attack(turtle, direction);
case DIG -> dig(turtle, direction);
case ATTACK -> attack(turtle, side, direction);
case DIG -> dig(turtle, side, direction);
};
}
@@ -102,16 +173,14 @@ public class TurtleTool extends AbstractTurtleUpgrade {
}
/**
* Attack an entity. This is a <em>very</em> cut down version of {@link Player#attack(Entity)}, which doesn't handle
* enchantments, knockback, etc... Unfortunately we can't call attack directly as damage calculations are rather
* different (and we don't want to play sounds/particles).
* Attack an entity.
*
* @param turtle The current turtle.
* @param side The side the tool is on.
* @param direction The direction we're attacking in.
* @return Whether an attack occurred.
* @see Player#attack(Entity)
*/
private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
private TurtleCommandResult attack(ITurtleAccess turtle, TurtleSide side, Direction direction) {
// Create a fake player, and orient it appropriately
var world = turtle.getLevel();
var position = turtle.getPosition();
@@ -123,10 +192,11 @@ public class TurtleTool extends AbstractTurtleUpgrade {
var turtlePos = player.position();
var rayDir = player.getViewVector(1.0f);
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
var attacked = false;
if (hit instanceof EntityHitResult entityHit) {
// Load up the turtle's inventory
var stackCopy = item.copy();
turtlePlayer.loadInventory(stackCopy);
var stack = getToolStack(turtle, side);
turtlePlayer.loadInventory(stack);
var hitEntity = entityHit.getEntity();
@@ -134,62 +204,120 @@ public class TurtleTool extends AbstractTurtleUpgrade {
DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle));
// Attack the entity
var attacked = false;
var result = PlatformHelper.get().canAttackEntity(player, hitEntity);
if (result.consumesAction()) {
attacked = true;
} else if (result == InteractionResult.PASS && hitEntity.isAttackable() && !hitEntity.skipAttackInteraction(player)) {
var damage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
if (damage > 0.0f) {
var source = player.damageSources().playerAttack(player);
if (hitEntity instanceof ArmorStand) {
// Special case for armor stands: attack twice to guarantee destroy
hitEntity.hurt(source, damage);
if (hitEntity.isAlive()) hitEntity.hurt(source, damage);
attacked = true;
} else {
if (hitEntity.hurt(source, damage)) attacked = true;
}
}
attacked = attack(player, direction, hitEntity);
}
// Stop claiming drops
TurtleUtil.stopConsuming(turtle);
// Put everything we collected into the turtles inventory, then return
// Put everything we collected into the turtles inventory.
setToolStack(turtle, side, player.getItemInHand(InteractionHand.MAIN_HAND));
player.getInventory().clearContent();
if (attacked) return TurtleCommandResult.success();
}
return TurtleCommandResult.failure("Nothing to attack here");
return attacked ? TurtleCommandResult.success() : TurtleCommandResult.failure("Nothing to attack here");
}
private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) {
if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) {
return TurtleCommandResult.success();
/**
* Attack an entity. This is a copy of {@link Player#attack(Entity)}, with some unwanted features removed (sweeping
* edge). This is a little limited.
* <p>
* Ideally we'd use attack directly (if other mods mixin to that method, we won't support their features).
* Unfortunately,that doesn't give us any feedback to whether the attack occurred or not (and we don't want to play
* sounds/particles).
*
* @param player The fake player doing the attacking.
* @param direction The direction the turtle is attacking.
* @param entity The entity to attack.
* @return Whether we attacked or not.
* @see Player#attack(Entity)
*/
private boolean attack(ServerPlayer player, Direction direction, Entity entity) {
var baseDamage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
var bonusDamage = EnchantmentHelper.getDamageBonus(
player.getItemInHand(InteractionHand.MAIN_HAND), entity instanceof LivingEntity target ? target.getMobType() : MobType.UNDEFINED
);
var damage = baseDamage + bonusDamage;
if (damage <= 0) return false;
var knockBack = EnchantmentHelper.getKnockbackBonus(player);
// We follow the logic in Player.attack of setting the entity on fire before attacking, so it's burning when it
// (possibly) dies.
var fireAspect = EnchantmentHelper.getFireAspect(player);
var onFire = false;
if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) {
onFire = true;
target.setSecondsOnFire(1);
}
var level = (ServerLevel) turtle.getLevel();
var turtlePosition = turtle.getPosition();
var source = player.damageSources().playerAttack(player);
if (!entity.hurt(source, damage)) {
// If we failed to damage the entity, undo us setting the entity on fire.
if (onFire) entity.clearFire();
return false;
}
var blockPosition = turtlePosition.relative(direction);
// Special case for armor stands: attack twice to guarantee destroy
if (entity.isAlive() && entity instanceof ArmorStand) entity.hurt(source, damage);
// Apply knockback
if (knockBack > 0) {
if (entity instanceof LivingEntity target) {
target.knockback(knockBack * 0.5, -direction.getStepX(), -direction.getStepZ());
} else {
entity.push(direction.getStepX() * knockBack * 0.5, 0.1, direction.getStepZ() * knockBack * 0.5);
}
}
// Apply remaining enchantments
if (entity instanceof LivingEntity target) EnchantmentHelper.doPostHurtEffects(target, player);
EnchantmentHelper.doPostDamageEffects(player, entity);
// Damage the original item stack.
if (entity instanceof LivingEntity target) {
player.getItemInHand(InteractionHand.MAIN_HAND).hurtEnemy(target, player);
}
// Apply fire aspect
if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) {
target.setSecondsOnFire(4 * fireAspect);
}
return true;
}
private TurtleCommandResult dig(ITurtleAccess turtle, TurtleSide side, Direction direction) {
var level = (ServerLevel) turtle.getLevel();
var blockPosition = turtle.getPosition().relative(direction);
if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) {
return TurtleCommandResult.failure("Nothing to dig here");
}
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction);
turtlePlayer.loadInventory(item.copy());
return withEquippedItem(turtle, side, direction, turtlePlayer -> {
var stack = turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND);
// Check if we can break the block
var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable;
// Right-click the block when using a shovel/hoe.
if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deploy(stack, turtle, turtlePlayer, direction, null, null)) {
return TurtleCommandResult.success();
}
DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle));
var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition);
TurtleUtil.stopConsuming(turtle);
// Check if we can break the block
var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable;
// Check spawn protection
return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block");
// And break it!
DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle));
var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition);
TurtleUtil.stopConsuming(turtle);
return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block");
});
}
private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) {

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.shared.turtle.upgrades;
import com.google.gson.JsonObject;
import dan200.computercraft.api.turtle.TurtleToolDurability;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.shared.platform.RegistryWrappers;
@@ -28,6 +29,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
var toolItem = GsonHelper.getAsItem(object, "item");
var craftingItem = GsonHelper.getAsItem(object, "craftingItem", toolItem);
var damageMultiplier = GsonHelper.getAsFloat(object, "damageMultiplier", 3.0f);
var allowEnchantments = GsonHelper.getAsBoolean(object, "allowEnchantments", false);
var consumeDurability = TurtleToolDurability.CODEC.byName(GsonHelper.getAsString(object, "consumeDurability", null), TurtleToolDurability.NEVER);
TagKey<Block> breakable = null;
if (object.has("breakable")) {
@@ -35,7 +38,7 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
breakable = TagKey.create(Registries.BLOCK, tag);
}
return new TurtleTool(id, adjective, craftingItem, new ItemStack(toolItem), damageMultiplier, breakable);
return new TurtleTool(id, adjective, craftingItem, new ItemStack(toolItem), damageMultiplier, allowEnchantments, consumeDurability, breakable);
}
@Override
@@ -46,9 +49,11 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
// damageMultiplier and breakable aren't used by the client, but we need to construct the upgrade exactly
// as otherwise syncing on an SP world will overwrite the (shared) upgrade registry with an invalid upgrade!
var damageMultiplier = buffer.readFloat();
var allowsEnchantments = buffer.readBoolean();
var consumesDurability = buffer.readEnum(TurtleToolDurability.class);
var breakable = buffer.readBoolean() ? TagKey.create(Registries.BLOCK, buffer.readResourceLocation()) : null;
return new TurtleTool(id, adjective, craftingItem, toolItem, damageMultiplier, breakable);
return new TurtleTool(id, adjective, craftingItem, toolItem, damageMultiplier, allowsEnchantments, consumesDurability, breakable);
}
@Override
@@ -57,6 +62,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
RegistryWrappers.writeId(buffer, RegistryWrappers.ITEMS, upgrade.getCraftingItem().getItem());
buffer.writeItem(upgrade.item);
buffer.writeFloat(upgrade.damageMulitiplier);
buffer.writeBoolean(upgrade.allowEnchantments);
buffer.writeEnum(upgrade.consumeDurability);
buffer.writeBoolean(upgrade.breakable != null);
if (upgrade.breakable != null) buffer.writeResourceLocation(upgrade.breakable.location());
}

View File

@@ -217,5 +217,15 @@
"gui.computercraft.config.http.enabled.tooltip": "Active l'API \"http\" sur les ordinateurs. Cela désactive également les programmes \"pastebin\" et \"wget\",\nsur lesquels de nombreux utilisateurs comptent. Il est recommandé de laisser cette option activée et\nd'utiliser l'option de configuration \"rules\" pour imposer un contrôle plus précis.",
"gui.computercraft.config.peripheral.modem_range.tooltip": "La portée des modems sans fil à basse altitude par temps dégagé, en mètres.\nPlage : 0 ~ 100000",
"gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "La limite de la quantité de données du moniteur pouvant être envoyées *par tick*. Note:\n - La bande passante est mesurée avant la compression, donc les données envoyées\n au client sont plus petites.\n - Cela ignore le nombre de joueurs auxquels un paquet est envoyé. La mise à jour d'un\n moniteur pour un joueur consomme la même limite de bande passante que l'envoi à 20.\n - Un moniteur de taille normale envoie ~25ko de données. Ainsi, la valeur par défaut (1Mo) permet \n à environ 40 moniteurs d'être mis à jour en un seul tick.\nMettre à 0 pour désactiver.\nPlage: > 0",
"gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué."
"gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué.",
"gui.computercraft.config.http.proxy.host.tooltip": "Le nom d'hôte ou l'adresse IP du serveur proxy.",
"gui.computercraft.config.http.proxy.tooltip": "Tunnelise les requêtes HTTP et websocket via un serveur proxy. Affecte uniquement\nles règles HTTP avec \"use_proxy\" défini sur true (désactivé par défaut).\nSi l'authentification est requise pour le proxy, créez un fichier \"computercraft-proxy.pw\"\ndans le même dossier que \"computercraft-server.toml\", contenant le\nnom d'utilisateur et mot de passe séparés par deux-points, par ex. \"monutilisateur:monmotdepasse\". Pour\nProxy SOCKS4, seul le nom d'utilisateur est requis.",
"gui.computercraft.config.upload_max_size.tooltip": "La taille limite de téléversement de fichier, en octets. Doit être compris entre 1 Kio et 16 Mio.\nGardez à l'esprit que les téléversements sont traités en un seul clic - les fichiers volumineux ou\nde mauvaises performances réseau peuvent bloquer le thread du réseau. Et attention à l'espace disque !\nPlage : 1024 ~ 16777216",
"gui.computercraft.config.http.proxy": "Proxy",
"gui.computercraft.config.http.proxy.host": "Nom d'hôte",
"gui.computercraft.config.http.proxy.port": "Port",
"gui.computercraft.config.http.proxy.port.tooltip": "Le port du serveur proxy.\nPlage : 1 ~ 65536",
"gui.computercraft.config.http.proxy.type": "Type de proxy",
"gui.computercraft.config.http.proxy.type.tooltip": "Le type de proxy à utiliser.\nValeurs autorisées : HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.config.upload_max_size": "Taille limite de téléversement de fichiers (octets)"
}

View File

@@ -177,5 +177,23 @@
"gui.computercraft.config.turtle.need_fuel": "Включить механику топлива",
"gui.computercraft.config.turtle.normal_fuel_limit": "Лимит топлива Черепашек",
"gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "Лимит топлива для Черепашек.\nОграничение: > 0",
"gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками."
"gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками.",
"gui.computercraft.config.http.proxy.port": "Порт",
"gui.computercraft.config.http.proxy.port.tooltip": "Порт прокси-сервера.\nДиапазон: 1 ~ 65536",
"gui.computercraft.config.http.proxy.host": "Имя хоста",
"gui.computercraft.config.http.proxy": "Proxy",
"gui.computercraft.config.http.proxy.host.tooltip": "Имя хоста или IP-адрес прокси-сервера.",
"gui.computercraft.config.http.proxy.tooltip": "Туннелирует HTTP-запросы и запросы websocket через прокси-сервер. Влияет только на HTTP\nправила с параметром \"use_proxy\" в значении true (отключено по умолчанию).\nЕсли для прокси-сервера требуется аутентификация, создайте \"computercraft-proxy.pw\"\nфайл в том же каталоге, что и \"computercraft-server.toml\", содержащий имя\nпользователя и пароль, разделенные двоеточием, например \"myuser:mypassword\". Для\nпрокси-серверов SOCKS4 требуется только имя пользователя.",
"gui.computercraft.config.http.proxy.type": "Тип прокси-сервера",
"gui.computercraft.config.http.proxy.type.tooltip": "Тип используемого прокси-сервера.\nДопустимые значения: HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.upload.no_response.msg": "Ваш компьютер не использовал переданные вами файлы. Возможно, вам потребуется запустить программу %s и повторить попытку.",
"tracking_field.computercraft.max": "%s (максимальное)",
"tracking_field.computercraft.count": "%s (количество)",
"gui.computercraft.config.http.rules": "Разрешающие/запрещающие правила",
"gui.computercraft.config.http.websocket_enabled": "Включить веб-сокеты",
"gui.computercraft.config.http.websocket_enabled.tooltip": "Включить использование http веб-сокетов. Для этого необходимо, чтобы параметр «http_enable» был true.",
"gui.computercraft.config.log_computer_errors": "Регистрировать ошибки компьютера",
"gui.computercraft.config.log_computer_errors.tooltip": "Регистрировать исключения, вызванные периферийными устройствами и другими объектами Lua. Это облегчает\nдля авторам модов устранение проблем, но может привести к спаму в логах, если люди будут использовать\nглючные методы.",
"gui.computercraft.config.maximum_open_files": "Максимальное количество файлов, открытых на одном компьютере",
"gui.computercraft.config.http.rules.tooltip": "Список правил, которые контролируют поведение «http» API для определенных доменов или\nIP-адресов. Каждое правило представляет собой элемент с «узлом» для сопоставления и набором\nсвойств. Правила оцениваются по порядку, то есть более ранние правила перевешивают\nболее поздние.\nХост может быть доменным именем (\"pastebin.com\"), wildcard-сертификатом (\"*.pastebin.com\") или\nнотацией CIDR (\"127.0.0.0/8\").\nЕсли правил нет, домен блокируется."
}

View File

@@ -7,9 +7,13 @@ package dan200.computercraft.gametest
import dan200.computercraft.api.detail.BasicItemDetailProvider
import dan200.computercraft.api.detail.VanillaDetailRegistries
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.api.turtle.ITurtleUpgrade
import dan200.computercraft.api.turtle.TurtleSide
import dan200.computercraft.api.upgrades.UpgradeData
import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.gametest.api.*
import dan200.computercraft.gametest.core.TestHooks
import dan200.computercraft.impl.TurtleUpgrades
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
import dan200.computercraft.shared.ModRegistry
@@ -30,6 +34,7 @@ import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.item.PrimedTnt
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.item.enchantment.Enchantments
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.FenceBlock
import net.minecraft.world.level.block.state.properties.BlockStateProperties
@@ -174,6 +179,85 @@ class Turtle_Test {
}
}
/**
* Digging using a pickaxe with `{"consumesDurability": "always"}`, consumes durability.
*/
@GameTest
fun Dig_consume_durability(helper: GameTestHelper) = helper.sequence {
thenOnComputer { turtle.dig(Optional.empty()).await() }
thenExecute {
helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 3))
helper.assertContainerExactly(BlockPos(2, 2, 2), listOf(ItemStack(Items.COBBLESTONE)))
val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get()).access
val upgrade = turtle.getUpgrade(TurtleSide.LEFT)
assertEquals(TurtleUpgrades.instance().get("cctest:wooden_pickaxe"), upgrade, "Upgrade is a wooden pickaxe")
val item = ItemStack(Items.WOODEN_PICKAXE)
item.damageValue = 1
helper.assertUpgradeItem(item, turtle.getUpgradeWithData(TurtleSide.LEFT)!!)
}
}
/**
* Digging using a pickaxe with `{"consumesDurability": "always"}` and no durability removes the tool.
*/
@GameTest
fun Dig_breaks_tool(helper: GameTestHelper) = helper.sequence {
thenOnComputer { turtle.dig(Optional.empty()).await() }
thenExecute {
helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 3))
helper.assertContainerExactly(BlockPos(2, 2, 2), listOf(ItemStack(Items.COBBLESTONE)))
val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get()).access
val upgrade = turtle.getUpgrade(TurtleSide.LEFT)
assertEquals(null, upgrade, "Upgrade broke")
helper.assertUpgradeItem(
ItemStack(Items.WOODEN_PICKAXE),
UpgradeData.ofDefault(TurtleUpgrades.instance().get("cctest:wooden_pickaxe")),
)
}
}
/**
* Digging using a silk-touch enchanted pickaxe with `{"consumesDurability": "when_enchanted"}`, consumes durability
* uses silk touch.
*/
@GameTest
fun Dig_enchanted_consume_durability(helper: GameTestHelper) = helper.sequence {
thenOnComputer { turtle.dig(Optional.empty()).await() }
thenExecute {
helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 3))
helper.assertContainerExactly(BlockPos(2, 2, 2), listOf(ItemStack(Items.STONE)))
val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get()).access
val upgrade = turtle.getUpgrade(TurtleSide.LEFT)
assertEquals(
TurtleUpgrades.instance().get("cctest:netherite_pickaxe"),
upgrade,
"Upgrade is a netherite pickaxe",
)
val item = ItemStack(Items.NETHERITE_PICKAXE)
item.damageValue = 1
item.enchant(Enchantments.SILK_TOUCH, 1)
item.setRepairCost(1)
helper.assertUpgradeItem(item, turtle.getUpgradeWithData(TurtleSide.LEFT)!!)
}
}
private fun GameTestHelper.assertUpgradeItem(expected: ItemStack, upgrade: UpgradeData<ITurtleUpgrade>) {
if (!ItemStack.matches(expected, upgrade.upgradeItem)) {
fail("Invalid upgrade item\n Expected => ${expected.tag}\n Actual => ${upgrade.upgradeItem.tag}")
}
if (!ItemStack.matches(ItemStack(expected.item), upgrade.upgrade.craftingItem)) {
fail("Original upgrade item has changed (is now ${upgrade.upgrade.craftingItem})")
}
}
/**
* Checks turtles can place monitors
*

View File

@@ -0,0 +1,6 @@
{
"type": "computercraft:tool",
"item": "minecraft:netherite_pickaxe",
"allowEnchantments": true,
"consumeDurability": "when_enchanted"
}

View File

@@ -0,0 +1,5 @@
{
"type": "computercraft:tool",
"item": "minecraft:wooden_pickaxe",
"consumeDurability": "always"
}

View File

@@ -0,0 +1,138 @@
{
DataVersion: 3218,
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:air"},
{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: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 80, Items: [], Label: "turtle_test.dig_breaks_tool", LeftUpgrade: "cctest:wooden_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 58}}, On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:stone"},
{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: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{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: [],
palette: [
"minecraft:polished_andesite",
"minecraft:stone",
"minecraft:air",
"computercraft:turtle_normal{facing:south,waterlogged:false}"
]
}

View File

@@ -0,0 +1,138 @@
{
DataVersion: 3218,
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:air"},
{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: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 80, Items: [], Label: "turtle_test.dig_consume_durability", LeftUpgrade: "cctest:wooden_pickaxe", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:stone"},
{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: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{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: [],
palette: [
"minecraft:polished_andesite",
"minecraft:stone",
"minecraft:air",
"computercraft:turtle_normal{facing:south,waterlogged:false}"
]
}

View File

@@ -0,0 +1,138 @@
{
DataVersion: 3337,
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:air"},
{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: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 80, Items: [], Label: "turtle_test.dig_enchanted_consume_durability", LeftUpgrade: "cctest:netherite_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0, Enchantments: [{id: "minecraft:silk_touch", lvl: 1s}], RepairCost: 1}}, On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:stone"},
{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: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{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:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{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: [],
palette: [
"minecraft:polished_andesite",
"minecraft:stone",
"minecraft:air",
"computercraft:turtle_normal{facing:south,waterlogged:false}"
]
}

View File

@@ -132,7 +132,8 @@ public class OSAPI implements ILuaAPI {
* @param name The name of the event to queue.
* @param args The parameters of the event.
* @cc.tparam string name The name of the event to queue.
* @cc.param ... The parameters of the event.
* @cc.param ... The parameters of the event. These can be any primitive type (boolean, number, string) as well as
* tables. Other types (like functions), as well as metatables, will not be preserved.
* @cc.see os.pullEvent To pull the event queued
*/
@LuaFunction

View File

@@ -6,9 +6,14 @@ package dan200.computercraft.core.apis.http.options;
import com.google.common.net.InetAddresses;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A predicate on an address. Matches against a domain and an ip address.
@@ -107,12 +112,48 @@ interface AddressPredicate {
final class PrivatePattern implements AddressPredicate {
static final PrivatePattern INSTANCE = new PrivatePattern();
private static final Set<InetAddress> additionalAddresses = Arrays.stream(new String[]{
// Block various cloud providers internal IPs.
"192.0.0.192", // Oracle
}).map(InetAddresses::forString).collect(Collectors.toUnmodifiableSet());
@Override
public boolean matches(InetAddress socketAddress) {
return socketAddress.isAnyLocalAddress()
|| socketAddress.isLoopbackAddress()
|| socketAddress.isLinkLocalAddress()
|| socketAddress.isSiteLocalAddress();
return
socketAddress.isAnyLocalAddress() // 0.0.0.0, ::0
|| socketAddress.isLoopbackAddress() // 127.0.0.0/8, ::1
|| socketAddress.isLinkLocalAddress() // 169.254.0.0/16, fe80::/10
|| socketAddress.isSiteLocalAddress() // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fec0::/10
|| socketAddress.isMulticastAddress() // 224.0.0.0/4, ff00::/8
|| isUniqueLocalAddress(socketAddress) // fd00::/8
|| isCarrierGradeNatAddress(socketAddress) // 100.64.0.0/10
|| additionalAddresses.contains(socketAddress);
}
/**
* Determine if an IP address lives inside the ULA address range.
*
* @param address The IP address to test.
* @return Whether this address sits in the ULA address range.
* @see <a href="https://en.wikipedia.org/wiki/Unique_local_address">Unique local address on Wikipedia</a>
*/
private boolean isUniqueLocalAddress(InetAddress address) {
// ULA is actually defined as fc00::/7 (so both fc00::/8 and fd00::/8). However, only the latter is actually
// defined right now, so let's be conservative.
return address instanceof Inet6Address && (address.getAddress()[0] & 0xff) == 0xfd;
}
/**
* Determine if an IP address lives within the CGNAT address range (100.64.0.0/10).
*
* @param address The IP address to test.
* @return Whether this address sits in the CGNAT address range.
* @see <a href="https://en.wikipedia.org/wiki/Carrier-grade_NAT">Carrier-grade NAT on Wikipedia</a>
*/
private boolean isCarrierGradeNatAddress(InetAddress address) {
if (!(address instanceof Inet4Address)) return false;
var bytes = address.getAddress();
return bytes[0] == 100 && ((bytes[1] & 0xFF) >= 64 && (bytes[1] & 0xFF) <= 127);
}
}

View File

@@ -76,7 +76,7 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
for (var extra : source.getExtra()) {
var extraMethods = getMethods(extra.getClass());
if (!extraMethods.isEmpty()) hasMethods = true;
for (var method : extraMethods) consumer.accept(object, method.name(), method.method(), method);
for (var method : extraMethods) consumer.accept(extra, method.name(), method.method(), method);
}
}

View File

@@ -2,13 +2,32 @@
--
-- SPDX-License-Identifier: LicenseRef-CCPL
--- Read and write configuration options for CraftOS and your programs.
--
-- By default, the settings API will load its configuration from the
-- `/.settings` file. One can then use @{settings.save} to update the file.
--
-- @module settings
-- @since 1.78
--[[- Read and write configuration options for CraftOS and your programs.
When a computer starts, it reads the current value of settings from the
`/.settings` file. These values then may be @{settings.get|read} or
@{settings.set|modified}.
:::caution
Calling @{settings.set} does _not_ update the settings file by default. You
_must_ call @{settings.save} to persist values.
:::
@module settings
@since 1.78
@usage Define an basic setting `123` and read its value.
settings.define("my.setting", {
description = "An example setting",
default = 123,
type = number,
})
print("my.setting = " .. settings.get("my.setting")) -- 123
You can then use the `set` program to change its value (e.g. `set my.setting 456`),
and then re-run the `example` program to check it has changed.
]]
local expect = dofile("rom/modules/main/cc/expect.lua")
local type, expect, field = type, expect.expect, expect.field
@@ -92,13 +111,19 @@ local function set_value(name, new)
end
end
--- Set the value of a setting.
--
-- @tparam string name The name of the setting to set
-- @param value The setting's value. This cannot be `nil`, and must be
-- serialisable by @{textutils.serialize}.
-- @throws If this value cannot be serialised
-- @see settings.unset
--[[- Set the value of a setting.
:::caution
Calling @{settings.set} does _not_ update the settings file by default. You
_must_ call @{settings.save} to persist values.
:::
@tparam string name The name of the setting to set
@param value The setting's value. This cannot be `nil`, and must be
serialisable by @{textutils.serialize}.
@throws If this value cannot be serialised
@see settings.unset
]]
function set(name, value)
expect(1, name, "string")
expect(2, value, "number", "string", "boolean", "table")

View File

@@ -1,3 +1,29 @@
# New features in CC: Tweaked 1.106.1
Several bug fixes:
* Block the CGNAT range (100.64.0.0/10) by default.
* Fix conflicts with other mods replacing reach distance.
# New features in CC: Tweaked 1.106.0
* Numerous documentation improvements (MCJack123, znepb, penguinencounter).
* Port `fs.find` to Lua. This also allows using `?` as a wildcard.
* Computers cursors now glow in the dark.
* Allow changing turtle upgrades from the GUI.
* Add option to serialize Unicode strings to JSON (MCJack123).
* Small optimisations to the `window` API.
* Turtle upgrades can now preserve NBT from upgrade item stack and when broken.
* Add support for tool enchantments and durability via datapacks. This is disabled for the built-in tools.
Several bug fixes:
* Fix turtles rendering incorrectly when upside down.
* Fix misplaced calls to IArguments.escapes.
* Lua REPL no longer accepts `)(` as a valid expression.
* Fix several inconsistencies with `require`/`package.path` in the Lua REPL (Wojbie).
* Fix turtle being able to place water buckets outside its reach distance.
* Fix private several IP address ranges not being blocked by the `$private` rule.
* Improve permission checks in the `/computercraft` command.
# New features in CC: Tweaked 1.105.0
* Optimise JSON string parsing.

View File

@@ -1,25 +1,7 @@
New features in CC: Tweaked 1.105.0
* Optimise JSON string parsing.
* Add `colors.fromBlit` (Erb3).
* Upload file size limit is now configurable (khankul).
* Wired cables no longer have a distance limit.
* Java methods now coerce values to strings consistently with Lua.
* Add custom timeout support to the HTTP API.
* Support custom proxies for HTTP requests (Lemmmy).
* The `speaker` program now errors when playing HTML files.
* `edit` now shows an error message when editing read-only files.
* Update Ukranian translation (SirEdvin).
New features in CC: Tweaked 1.106.1
Several bug fixes:
* Allow GPS hosts to only be 1 block apart.
* Fix "Turn On"/"Turn Off" buttons being inverted in the computer GUI (Erb3).
* Fix arrow keys not working in the printout UI.
* Several documentation fixes (zyxkad, Lupus590, Commandcracker).
* Fix monitor renderer debug text always being visible on Forge.
* Fix crash when another mod changes the LoggerContext.
* Fix the `monitor_renderer` option not being present in Fabric config files.
* Pasting on MacOS/OSX now uses Cmd+V rather than Ctrl+V.
* Fix turtles placing blocks upside down when at y<0.
* Block the CGNAT range (100.64.0.0/10) by default.
* Fix conflicts with other mods replacing reach distance.
Type "help changelog" to see the full version history.

View File

@@ -31,12 +31,30 @@ public class AddressRuleTest {
@ValueSource(strings = {
"0.0.0.0", "[::]",
"localhost", "127.0.0.1.nip.io", "127.0.0.1", "[::1]",
"172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1"
"172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1",
// Multicast
"224.0.0.1", "ff02::1",
// CGNAT
"100.64.0.0", "100.127.255.255",
// Cloud metadata providers
"100.100.100.200", // Alibaba
"192.0.0.192", // Oracle
"fd00:ec2::254", // AWS
"169.254.169.254", // AWS, Digital Ocean, GCP, etc..
})
public void blocksLocalDomains(String domain) {
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.DENY);
}
@ParameterizedTest
@ValueSource(strings = {
// Ensure either side of the CGNAT range is allowed.
"100.63.255.255", "100.128.0.0"
})
public void allowsNonLocalDomains(String domain) {
assertEquals(apply(CoreConfig.httpRules, domain, 80).action, Action.ALLOW);
}
private Options apply(Iterable<AddressRule> rules, String host, int port) {
return AddressRule.apply(rules, host, new InetSocketAddress(host, port));
}

View File

@@ -58,7 +58,7 @@ public class MethodTest {
@Test
public void testExtra() {
ComputerBootstrap.run("assert(extra.go, 'go')\nassert(extra.go2, 'go2')",
ComputerBootstrap.run("assert(extra.go() == nil, 'go')\nassert(extra.go2() == 456, 'go2')",
x -> x.addApi(new ExtraObject()),
50);
}
@@ -163,7 +163,8 @@ public class MethodTest {
}
@LuaFunction
public final void go2() {
public final int go2() {
return 456;
}
@Override

View File

@@ -28,7 +28,6 @@ fun addRemappedConfiguration(name: String) {
val ourSourceSet = sourceSets.register(name) {
// Try to make this source set as much of a non-entity as possible.
listOf(allSource, java, resources, kotlin).forEach { it.setSrcDirs(emptyList<File>()) }
runtimeClasspath += sourceSets["client"].runtimeClasspath
}
val capitalName = name.replaceFirstChar { it.titlecase(Locale.ROOT) }
loom.addRemapConfiguration("mod$capitalName") {
@@ -46,7 +45,6 @@ fun addRemappedConfiguration(name: String) {
addRemappedConfiguration("testWithSodium")
addRemappedConfiguration("testWithIris")
addRemappedConfiguration("integrations")
dependencies {
modImplementation(libs.bundles.externalMods.fabric)
@@ -62,7 +60,6 @@ dependencies {
"modTestWithSodium"(libs.sodium)
"modTestWithIris"(libs.iris)
"modTestWithIris"(libs.sodium)
"modIntegrations"(libs.libmultipart)
include(libs.cobalt)
include(libs.jzlib)
@@ -169,14 +166,6 @@ loom {
property("fabric-api.gametest.report-file", project.buildDir.resolve("test-results/runGametest.xml").absolutePath)
runDir("run/gametest")
}
register("clientWithIntegrations") {
configName = "Client (+integrations)"
runDir("run/integration")
client()
source(sourceSets["integrations"])
}
}
}

View File

@@ -4,16 +4,11 @@
package dan200.computercraft.client;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.integration.libmultipart.LibMultiPartIntegrationClient;
import dan200.computercraft.client.model.EmissiveComputerModel;
import dan200.computercraft.client.model.turtle.TurtleModelLoader;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.integration.LoadedMods;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.platform.NetworkHandler;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@@ -22,7 +17,6 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
import net.fabricmc.fabric.api.event.client.player.ClientPickBlockGatherCallback;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.world.item.ItemStack;
@@ -74,10 +68,5 @@ public class ComputerCraftClient {
return cable.getCloneItemStack(state, hit, level, pos, player);
});
// Load config file
((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml"));
if (LoadedMods.LIB_MULTI_PART) LibMultiPartIntegrationClient.init();
}
}

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.integration.libmultipart;
import alexiil.mc.lib.multipart.api.render.PartStaticModelRegisterEvent;
import dan200.computercraft.shared.integration.libmultipart.BlockStateModelKey;
import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration;
import net.minecraft.client.Minecraft;
/**
* Client-side support for LibMultiPart.
*
* @see LibMultiPartIntegration
*/
public class LibMultiPartIntegrationClient {
public static void init() {
PartStaticModelRegisterEvent.EVENT.register(renderer -> {
var baker = Minecraft.getInstance().getBlockRenderer();
renderer.register(BlockStateModelKey.class, (key, ctx) ->
ctx.bakedModelConsumer().accept(baker.getBlockModel(key.state()), key.state()));
});
}
}

View File

@@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin;
import dan200.computercraft.shared.integration.LoadedMods;
import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.BlockPlaceContext;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Adds multipart support to {@link BlockItem}.
*/
@Mixin(BlockItem.class)
class BlockItemMixin extends Item {
BlockItemMixin(Properties properties) {
super(properties);
}
@Inject(method = "place", at = @At(value = "HEAD"), cancellable = true)
private void placeMultipart(BlockPlaceContext context, CallbackInfoReturnable<InteractionResult> cir) {
if (!LoadedMods.LIB_MULTI_PART) return;
// If we have custom handling for this item, and the default logic would not work, then we run our libmultipart
// hook.
var factory = LibMultiPartIntegration.getCreatorForItem(this);
if (factory != null && !context.canPlace()) cir.setReturnValue(factory.placePart(context));
}
}

View File

@@ -9,24 +9,38 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Item.class)
class ItemMixin {
/**
* Replace the reach distance in {@link Item#getPlayerPOVHitResult(Level, Player, ClipContext.Fluid)}.
*
* @param reach The original reach distance.
* @param level The current level.
* @param player The current player.
* @return The new reach distance.
* @param level The current level.
* @param player The current player.
* @param fluidMode The current clip-context fluid mode.
* @param cir Callback info to store the new reach distance.
* @see FakePlayer#getBlockReach()
*/
@ModifyConstant(method = "getPlayerPOVHitResult", constant = @Constant(doubleValue = 5))
@Inject(method = "getPlayerPOVHitResult", at = @At("HEAD"), cancellable = true)
@SuppressWarnings("UnusedMethod")
private static double getReachDistance(double reach, Level level, Player player) {
return player instanceof FakePlayer fp ? fp.getBlockReach() : reach;
private static void getReachDistance(Level level, Player player, ClipContext.Fluid fluidMode, CallbackInfoReturnable<BlockHitResult> cir) {
// It would theoretically be cleaner to use @ModifyConstant here, but as it's treated as a @Redirect, it doesn't
// compose with other mods. Instead, we replace the method when working with our fake player.
if (player instanceof FakePlayer fp) cir.setReturnValue(getHitResult(level, fp, fluidMode));
}
@Unique
private static BlockHitResult getHitResult(Level level, FakePlayer player, ClipContext.Fluid fluidMode) {
var start = player.getEyePosition();
var reach = player.getBlockReach();
var direction = player.getViewVector(1.0f);
var end = start.add(direction.x() * reach, direction.y() * reach, direction.z() * reach);
return level.clip(new ClipContext(start, end, ClipContext.Block.OUTLINE, fluidMode, player));
}
}

View File

@@ -12,8 +12,6 @@ import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.details.FluidDetails;
import dan200.computercraft.shared.integration.LoadedMods;
import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration;
import dan200.computercraft.shared.network.client.UpgradesLoadedMessage;
import dan200.computercraft.shared.peripheral.commandblock.CommandBlockPeripheral;
import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods;
@@ -32,6 +30,7 @@ import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup;
import net.fabricmc.fabric.api.loot.v2.LootTableEvents;
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.PreparableReloadListener;
@@ -101,11 +100,11 @@ public class ComputerCraft {
CommonHooks.onDatapackReload((name, listener) -> ResourceManagerHelper.get(PackType.SERVER_DATA).registerReloadListener(new ReloadListener(name, listener)));
((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml"));
FabricDetailRegistries.FLUID_VARIANT.addProvider(FluidDetails::fill);
ComputerCraftAPI.registerGenericSource(new InventoryMethods());
if (LoadedMods.LIB_MULTI_PART) LibMultiPartIntegration.init();
}
private record ReloadListener(String name, PreparableReloadListener listener)

View File

@@ -1,24 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration;
import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration;
import net.fabricmc.loader.api.FabricLoader;
/**
* Constants indicating whether various mods are loaded or not. These are stored as static final fields, to avoid
* repeated lookups and allow the JIT to inline them to constants.
*/
public final class LoadedMods {
/**
* Whether LibMultiPart is loaded.
*
* @see LibMultiPartIntegration
*/
public static final boolean LIB_MULTI_PART = FabricLoader.getInstance().isModLoaded(LibMultiPartIntegration.MOD_ID);
private LoadedMods() {
}
}

View File

@@ -1,33 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.libmultipart;
import alexiil.mc.lib.multipart.api.render.PartModelKey;
import net.minecraft.world.level.block.state.BlockState;
/**
* A {@link PartModelKey} which just renders a basic {@link BlockState}.
*/
public final class BlockStateModelKey extends PartModelKey {
private final BlockState state;
public BlockStateModelKey(BlockState state) {
this.state = state;
}
public BlockState state() {
return state;
}
@Override
public boolean equals(Object o) {
return o instanceof BlockStateModelKey other && state == other.state;
}
@Override
public int hashCode() {
return state.hashCode();
}
}

View File

@@ -1,73 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.libmultipart;
import alexiil.mc.lib.attributes.*;
import alexiil.mc.lib.multipart.api.NativeMultipart;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.node.wired.WiredElementLookup;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.PeripheralLookup;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.integration.libmultipart.parts.WirelessModemPart;
import net.minecraft.world.item.Item;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Integration for <a href="https://github.com/AlexIIL/LibMultiPart/">LibMultiPart</a>.
* <p>
* This adds multipart versions of modems and cables.
*/
public final class LibMultiPartIntegration {
public static final String MOD_ID = "libmultipart";
public static final Attribute<IPeripheral> PERIPHERAL = Attributes.create(IPeripheral.class);
public static final Attribute<WiredElement> WIRED_ELEMENT = Attributes.create(WiredElement.class);
private static final Map<Item, PlacementMultipartCreator> itemPlacers = new HashMap<>();
private LibMultiPartIntegration() {
}
public static void init() {
// Register an adapter from Fabric block lookup to attributes. This would be very inefficient by default, so
// we only do it for blocks which explicitly implement the attribute interfaces.
PeripheralLookup.get().registerFallback((world, pos, state, blockEntity, context) ->
state.getBlock() instanceof AttributeProvider || blockEntity instanceof AttributeProviderBlockEntity
? PERIPHERAL.getFirstOrNull(world, pos, SearchOptions.inDirection(context.getOpposite()))
: null);
WiredElementLookup.get().registerFallback((world, pos, state, blockEntity, context) ->
state.getBlock() instanceof AttributeProvider || blockEntity instanceof AttributeProviderBlockEntity
? WIRED_ELEMENT.getFirstOrNull(world, pos, SearchOptions.inDirection(context.getOpposite()))
: null);
registerWirelessModem(WirelessModemPart.makeDefinition(ModRegistry.Blocks.WIRELESS_MODEM_NORMAL, false));
registerWirelessModem(WirelessModemPart.makeDefinition(ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED, true));
}
private static void registerWirelessModem(WirelessModemPart.Definition definition) {
definition.register();
NativeMultipart.LOOKUP.registerForBlocks((world, pos, state, blockEntity, context) -> (level, blockPos, blockState) ->
List.of(holder -> definition.convert(holder, state, blockEntity)), definition.block());
itemPlacers.put(definition.block().asItem(), definition);
}
/**
* Get the corresponding {@link PlacementMultipartCreator} for an item.
*
* @param item The item we're trying to place.
* @return The placement-aware multipart creator, or {@code null}.
*/
public static @Nullable PlacementMultipartCreator getCreatorForItem(Item item) {
return itemPlacers.get(item);
}
}

View File

@@ -1,64 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.libmultipart;
import alexiil.mc.lib.multipart.api.AbstractPart;
import alexiil.mc.lib.multipart.api.MultipartContainer.MultipartCreator;
import alexiil.mc.lib.multipart.api.MultipartHolder;
import alexiil.mc.lib.multipart.api.MultipartUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.gameevent.GameEvent;
/**
* Creates a {@linkplain AbstractPart multipart} based on a {@link BlockPlaceContext}.
*
* @see MultipartCreator
*/
public interface PlacementMultipartCreator {
/**
* Create a new part.
*
* @param holder The holder which is creating this part.
* @param context The current block placement context.
* @return The newly created part.
*/
AbstractPart create(MultipartHolder holder, BlockPlaceContext context);
/**
* Attempt to place this part into the world.
* <p>
* This largely mirrors the logic in {@link BlockItem#place(BlockPlaceContext)}, but using
* {@link MultipartUtil#offerNewPart(Level, BlockPos, MultipartCreator)} to place the new part instead.
*
* @param context The current placement context.
* @return Whether the part was placed or not.
*/
default InteractionResult placePart(BlockPlaceContext context) {
var level = context.getLevel();
var position = context.getClickedPos();
var offer = MultipartUtil.offerNewPart(level, position, holder -> create(holder, context));
if (offer == null) return InteractionResult.PASS;
// Be careful to only apply this server-side. Multiparts send a bunch of network packets when created, which we
// obviously don't want to do on the server!
if (!level.isClientSide) offer.apply();
// Approximate the block state from the placed part, and then fire all the appropriate events.
var stack = context.getItemInHand();
var blockState = ((BlockItem) stack.getItem()).getBlock().defaultBlockState();
var player = context.getPlayer();
var sound = blockState.getSoundType();
level.playSound(player, position, sound.getPlaceSound(), SoundSource.BLOCKS, (sound.getVolume() + 1f) / 2f, sound.getPitch() * 0.8f);
level.gameEvent(GameEvent.BLOCK_PLACE, position, GameEvent.Context.of(player, blockState));
if (player == null || !player.getAbilities().instabuild) stack.shrink(1);
return InteractionResult.sidedSuccess(level.isClientSide);
}
}

View File

@@ -1,194 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.libmultipart.parts;
import alexiil.mc.lib.attributes.AttributeList;
import alexiil.mc.lib.multipart.api.AbstractPart;
import alexiil.mc.lib.multipart.api.MultipartEventBus;
import alexiil.mc.lib.multipart.api.MultipartHolder;
import alexiil.mc.lib.multipart.api.PartDefinition;
import alexiil.mc.lib.multipart.api.event.PartTickEvent;
import alexiil.mc.lib.multipart.api.render.PartModelKey;
import alexiil.mc.lib.net.IMsgReadCtx;
import alexiil.mc.lib.net.IMsgWriteCtx;
import alexiil.mc.lib.net.InvalidInputDataException;
import alexiil.mc.lib.net.NetByteBuf;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.integration.libmultipart.BlockStateModelKey;
import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration;
import dan200.computercraft.shared.integration.libmultipart.PlacementMultipartCreator;
import dan200.computercraft.shared.peripheral.modem.ModemShapes;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockEntity;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral;
import dan200.computercraft.shared.platform.RegistryEntry;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.VoxelShape;
import javax.annotation.Nullable;
/**
* A {@linkplain AbstractPart multipart} for wireless modems.
*
* @see WirelessModemBlock
* @see WirelessModemBlockEntity
*/
public final class WirelessModemPart extends AbstractPart {
private final WirelessModemBlock modemBlock;
private final boolean advanced;
private final Direction direction;
private final Peripheral modem;
private boolean on;
private WirelessModemPart(
PartDefinition definition, MultipartHolder holder, WirelessModemBlock modemBlock, boolean advanced,
Direction direction, @Nullable ModemState state
) {
super(definition, holder);
this.modemBlock = modemBlock;
this.advanced = advanced;
this.direction = direction;
modem = new Peripheral(this, state);
}
public static Definition makeDefinition(RegistryEntry<WirelessModemBlock> modem, boolean advanced) {
return new Definition(modem, advanced);
}
@Override
public void onAdded(MultipartEventBus bus) {
if (container.getMultipartWorld().isClientSide) return;
bus.addListener(this, PartTickEvent.class, event -> {
if (modem.getModemState().pollChanged()) sendNetworkUpdate(this, NET_RENDER_DATA);
});
}
@Override
public void addAllAttributes(AttributeList<?> list) {
super.addAllAttributes(list);
if (list.attribute == LibMultiPartIntegration.PERIPHERAL && list.getSearchDirection() == direction.getOpposite()) {
list.offer(modem);
}
}
@Override
public void writeCreationData(NetByteBuf buffer, IMsgWriteCtx ctx) {
super.writeCreationData(buffer, ctx);
buffer.writeEnum(direction);
}
@Override
public CompoundTag toTag() {
var tag = super.toTag();
tag.putString("direction", direction.getSerializedName());
return tag;
}
@Override
public void writeRenderData(NetByteBuf buffer, IMsgWriteCtx ctx) {
super.writeRenderData(buffer, ctx);
buffer.writeBoolean(on = modem.getModemState().isOpen());
}
@Override
public void readRenderData(NetByteBuf buffer, IMsgReadCtx ctx) throws InvalidInputDataException {
super.readRenderData(buffer, ctx);
on = buffer.readBoolean();
redrawIfChanged();
}
@Override
public VoxelShape getShape() {
return ModemShapes.getBounds(direction);
}
@Nullable
@Override
public PartModelKey getModelKey() {
return new BlockStateModelKey(
modemBlock.defaultBlockState()
.setValue(WirelessModemBlock.FACING, direction)
.setValue(WirelessModemBlock.ON, on)
);
}
private static class Peripheral extends WirelessModemPeripheral {
private final WirelessModemPart part;
Peripheral(WirelessModemPart part, @Nullable ModemState state) {
// state will be non-null when converting an existing modem. This allows us to preserve the open channels.
super(state == null ? new ModemState() : state.copy(), part.advanced);
this.part = part;
}
@Override
public Level getLevel() {
return part.container.getMultipartWorld();
}
@Override
public Vec3 getPosition() {
return Vec3.atLowerCornerOf(part.container.getMultipartPos().relative(part.direction));
}
@Override
public boolean equals(@Nullable IPeripheral other) {
return this == other || (other instanceof Peripheral && part == ((Peripheral) other).part);
}
@Override
public Object getTarget() {
return part;
}
}
public static final class Definition extends PartDefinition implements PlacementMultipartCreator {
private final RegistryEntry<WirelessModemBlock> modem;
private final boolean advanced;
private Definition(RegistryEntry<WirelessModemBlock> modem, boolean advanced) {
super(
modem.id(),
(def, holder, tag) -> {
var direction = Direction.CODEC.byName(tag.getString("direction"), Direction.NORTH);
return new WirelessModemPart(def, holder, modem.get(), advanced, direction, null);
},
(def, holder, buffer, context) -> {
var direction = buffer.readEnum(Direction.class);
return new WirelessModemPart(def, holder, modem.get(), advanced, direction, null);
}
);
this.modem = modem;
this.advanced = advanced;
}
public Block block() {
return modem.get();
}
public AbstractPart convert(MultipartHolder holder, BlockState state, @Nullable BlockEntity blockEntity) {
return new WirelessModemPart(
this, holder, modem.get(), advanced,
state.getValue(WirelessModemBlock.FACING),
blockEntity instanceof WirelessModemBlockEntity modemBlockEntity ? modemBlockEntity.getModemState() : null
);
}
@Override
public AbstractPart create(MultipartHolder holder, BlockPlaceContext context) {
return new WirelessModemPart(this, holder, modem.get(), advanced, context.getClickedFace().getOpposite(), null);
}
}
}

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.shared.platform;
import com.mojang.authlib.GameProfile;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
@@ -39,4 +40,9 @@ public final class FakePlayer extends net.fabricmc.fabric.api.entity.FakePlayer
public double getBlockReach() {
return MAX_REACH;
}
@Override
public boolean broadcastToPlayer(ServerPlayer player) {
return false;
}
}

View File

@@ -8,7 +8,6 @@
},
"mixins": [
"ArgumentTypeInfosAccessor",
"BlockItemMixin",
"ChunkMapMixin",
"EntityMixin",
"ExplosionDamageCalculatorMixin",

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.shared.platform;
import com.mojang.authlib.GameProfile;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityDimensions;
@@ -57,4 +58,9 @@ class FakePlayerExt extends FakePlayer {
public double getEntityReach() {
return MAX_REACH;
}
@Override
public boolean broadcastToPlayer(ServerPlayer player) {
return false;
}
}