From 286f969f940bc4b929c72ca93d6322b82992c32e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 23 Mar 2024 10:58:46 +0000 Subject: [PATCH 01/19] Remove computers from both lookups when they timeout In 5d8c46c7e61117ede5d6315efc81a4ae498f34ba, we switched to using UUIDs for looking up computers (rather than an integer ID). However, for compatibility in some of the command code, we need to maintain the old integer lookup map. Most of the code was updated to handle this, *except* the code to remove a computer from the registry. This meant that we'd fail to remove a computer from the UUID lookup map, so computers ended up in a phantom state where they were destroyed, but still accessible. This is not an issue on 1.20.4, because the legacy int lookup map was removed. Fixes #1760 --- .../shared/computer/core/ServerComputerRegistry.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java index 34f76b19d..326562c7a 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java @@ -42,13 +42,14 @@ public class ServerComputerRegistry { } void update() { - var it = getComputers().iterator(); + var it = computersByInstanceId.values().iterator(); while (it.hasNext()) { var computer = it.next(); if (computer.hasTimedOut()) { computer.unload(); computer.onRemoved(); it.remove(); + computersByInstanceUuid.remove(computer.getInstanceUUID()); } else { computer.tickServer(); } From 777aa34bb059cf457fe85ed0f65c82d914c5c36b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 23 Mar 2024 11:09:42 +0000 Subject: [PATCH 02/19] Bump CC:T to 1.110.1 --- gradle.properties | 2 +- .../data/computercraft/lua/rom/help/changelog.md | 6 ++++++ .../data/computercraft/lua/rom/help/whatsnew.md | 15 +++------------ 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/gradle.properties b/gradle.properties index a0129c816..b49c90155 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error # Mod properties isUnstable=false -modVersion=1.110.0 +modVersion=1.110.1 # Minecraft properties: We want to configure this here so we can read it in settings.gradle mcVersion=1.20.1 diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md index 9cb7717a4..a5ea53369 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md @@ -1,3 +1,9 @@ +# New features in CC: Tweaked 1.110.1 + +Several bug fixes: +* Fix computers not turning on after they're unloaded/not-ticked for a while. +* Fix networking cables sometimes not connecting on Forge. + # New features in CC: Tweaked 1.110.0 * Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md index 4afa55617..860667454 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md @@ -1,16 +1,7 @@ -New features in CC: Tweaked 1.110.0 - -* Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command. -* Remove custom breaking progress of modems on Forge. +New features in CC: Tweaked 1.110.1 Several bug fixes: -* Fix client and server DFPWM transcoders getting out of sync. -* Fix `turtle.suck` reporting incorrect error when failing to suck items. -* Fix pocket computers displaying state (blinking, modem light) for the wrong computer. -* Fix crash when wrapping an invalid BE as a generic peripheral. -* Chest peripherals now reattach when a chest is converted into a double chest. -* Fix `speaker` program not resolving files relative to the current directory. -* Skip main-thread tasks if the peripheral is detached. -* Fix internal Lua VM errors if yielding inside `__tostring`. +* Fix computers not turning on after they're unloaded/not-ticked for a while. +* Fix networking cables sometimes not connecting on Forge. Type "help changelog" to see the full version history. From ae767eb5be1cfa551c1689a7638273d0c5b00f7f Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 24 Mar 2024 11:10:36 +0000 Subject: [PATCH 03/19] Improve error when no path is passed to "speaker" Co-authored-by: Matthew W --- .../data/computercraft/lua/rom/programs/fun/speaker.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua index 883b7eaff..928c403dd 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua @@ -43,6 +43,10 @@ if cmd == "stop" then for _, speaker in pairs(get_speakers(name)) do speaker.stop() end elseif cmd == "play" then local _, file, name = ... + if not file then + error("Usage: speaker play [speaker]", 0) + end + local speaker = get_speakers(name)[1] local handle, err From e154b0db2a69b1b48190f3f97549a7acb708d742 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 24 Mar 2024 12:20:53 +0000 Subject: [PATCH 04/19] Fix speaker.playSound overwriting current sound playSound should return false if we've already played a sound this tick, rather than overwriting it. --- .github/workflows/main-ci.yml | 12 +- .github/workflows/make-doc.yml | 6 +- .../peripheral/speaker/SpeakerPeripheral.java | 2 +- .../computercraft/gametest/Speaker_Test.kt | 32 ++++ .../gametest/api/TestExtensions.kt | 13 ++ .../computercraft/gametest/core/TestHooks.kt | 1 + ...er_test.fails_to_play_multiple_sounds.snbt | 138 ++++++++++++++++++ 7 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/speaker_test.fails_to_play_multiple_sounds.snbt diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index d94091eb9..5d1cf8a92 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -9,16 +9,16 @@ jobs: steps: - name: 📥 Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 📥 Set up Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' - name: 📥 Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} @@ -82,16 +82,16 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} diff --git a/.github/workflows/make-doc.yml b/.github/workflows/make-doc.yml index 5b22ee6f2..4da2a788a 100644 --- a/.github/workflows/make-doc.yml +++ b/.github/workflows/make-doc.yml @@ -13,16 +13,16 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java index 9791b9977..df995fb6d 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -260,7 +260,7 @@ public abstract class SpeakerPeripheral implements IPeripheral { } synchronized (lock) { - if (dfpwmState != null && dfpwmState.isPlaying()) return false; + if (pendingSound != null | (dfpwmState != null && dfpwmState.isPlaying())) return false; dfpwmState = null; pendingSound = new PendingSound<>(identifier, volume, pitch); return true; diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt new file mode 100644 index 000000000..be8cab748 --- /dev/null +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.gametest + +import dan200.computercraft.gametest.api.sequence +import dan200.computercraft.gametest.api.thenOnComputer +import dan200.computercraft.gametest.api.tryMultipleTimes +import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral +import dan200.computercraft.test.core.assertArrayEquals +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.sounds.SoundEvents + +class Speaker_Test { + /** + * [SpeakerPeripheral.playSound] fails if there is already a sound queued. + */ + @GameTest + fun Fails_to_play_multiple_sounds(helper: GameTestHelper) = helper.sequence { + thenOnComputer { + callPeripheral("right", "playSound", SoundEvents.NOTE_BLOCK_HARP.key().location().toString()) + .assertArrayEquals(true) + + tryMultipleTimes(2) { // We could technically call this a tick later, so try twice + callPeripheral("right", "playSound", SoundEvents.NOTE_BLOCK_HARP.key().location().toString()) + .assertArrayEquals(false) + } + } + } +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt index fc5977a2d..8da752f68 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt @@ -321,3 +321,16 @@ fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direc val hit = BlockHitResult(Vec3.atCenterOf(absolutePos), direction, absolutePos, false) stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit)) } + +/** + * Run a function multiple times until it succeeds. + */ +inline fun tryMultipleTimes(count: Int, action: () -> Unit) { + for (remaining in count - 1 downTo 0) { + try { + action() + } catch (e: AssertionError) { + if (remaining == 0) throw e + } + } +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt index c3a418d30..e6ac3895d 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt @@ -86,6 +86,7 @@ object TestHooks { Printer_Test::class.java, Printout_Test::class.java, Recipe_Test::class.java, + Speaker_Test::class.java, Turtle_Test::class.java, ) diff --git a/projects/common/src/testMod/resources/data/cctest/structures/speaker_test.fails_to_play_multiple_sounds.snbt b/projects/common/src/testMod/resources/data/cctest/structures/speaker_test.fails_to_play_multiple_sounds.snbt new file mode 100644 index 000000000..8ec5cb1db --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/speaker_test.fails_to_play_multiple_sounds.snbt @@ -0,0 +1,138 @@ +{ + DataVersion: 3465, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [0, 1, 0], state: "minecraft:air"}, + {pos: [0, 1, 1], state: "minecraft:air"}, + {pos: [0, 1, 2], state: "minecraft:air"}, + {pos: [0, 1, 3], state: "minecraft:air"}, + {pos: [0, 1, 4], state: "minecraft:air"}, + {pos: [1, 1, 0], state: "minecraft:air"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "computercraft:speaker{facing:north}", nbt: {id: "computercraft:speaker"}}, + {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:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "speaker_test.fails_to_play_multiple_sounds", On: 1b, id: "computercraft:computer_normal"}}, + {pos: [2, 1, 3], state: "minecraft:air"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "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:air", + "computercraft:speaker{facing:north}", + "computercraft:computer_normal{facing:north,state:on}" + ] +} From 0e5248e5e64d2871736a8428b6ecf3878adee4ab Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 24 Mar 2024 12:53:57 +0000 Subject: [PATCH 05/19] Prevent playing music discs with speaker.playSound I have mixed feelings about speaker.playSound. On one hand, it's pretty useful to be able to play any sound. On the other, it sometimes feels ... maybe a little too magic? One particular thing I don't like is that it allows you to play arbitrary records, which sidesteps both a vanilla mechanic (finding record discs) and existing CC functionality (disk.playAudio). We now prevent playing record tracks from the speaker. --- .../peripheral/speaker/SpeakerPeripheral.java | 18 ++- .../computercraft/gametest/Speaker_Test.kt | 11 ++ .../speaker_test.will_not_play_record.snbt | 138 ++++++++++++++++++ 3 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/speaker_test.will_not_play_record.snbt diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java index df995fb6d..8a081deb6 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -18,16 +18,18 @@ import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage; import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage; import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; import dan200.computercraft.shared.network.server.ServerNetworking; +import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.util.PauseAwareTimer; -import net.minecraft.ResourceLocationException; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.game.ClientboundSoundPacket; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; import net.minecraft.util.Mth; +import net.minecraft.world.item.RecordItem; import net.minecraft.world.level.block.state.properties.NoteBlockInstrument; import javax.annotation.Nullable; @@ -252,15 +254,15 @@ public abstract class SpeakerPeripheral implements IPeripheral { var volume = (float) clampVolume(checkFinite(1, volumeA.orElse(1.0))); var pitch = (float) checkFinite(2, pitchA.orElse(1.0)); - ResourceLocation identifier; - try { - identifier = new ResourceLocation(name); - } catch (ResourceLocationException e) { - throw new LuaException("Malformed sound name '" + name + "' "); - } + var identifier = ResourceLocation.tryParse(name); + if (identifier == null) throw new LuaException("Malformed sound name '" + name + "' "); + + // Prevent playing music discs. + var soundEvent = PlatformHelper.get().tryGetRegistryObject(Registries.SOUND_EVENT, identifier); + if (soundEvent != null && RecordItem.getBySound(soundEvent) != null) return false; synchronized (lock) { - if (pendingSound != null | (dfpwmState != null && dfpwmState.isPlaying())) return false; + if (pendingSound != null || (dfpwmState != null && dfpwmState.isPlaying())) return false; dfpwmState = null; pendingSound = new PendingSound<>(identifier, volume, pitch); return true; diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt index be8cab748..8a2d9572f 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Speaker_Test.kt @@ -29,4 +29,15 @@ class Speaker_Test { } } } + + /** + * [SpeakerPeripheral.playSound] will not play records. + */ + @GameTest + fun Will_not_play_record(helper: GameTestHelper) = helper.sequence { + thenOnComputer { + callPeripheral("right", "playSound", SoundEvents.MUSIC_DISC_PIGSTEP.location.toString()) + .assertArrayEquals(false) + } + } } diff --git a/projects/common/src/testMod/resources/data/cctest/structures/speaker_test.will_not_play_record.snbt b/projects/common/src/testMod/resources/data/cctest/structures/speaker_test.will_not_play_record.snbt new file mode 100644 index 000000000..eb2a11850 --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/speaker_test.will_not_play_record.snbt @@ -0,0 +1,138 @@ +{ + DataVersion: 3465, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [0, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [1, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [2, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [3, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 0], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 1], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 2], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 3], state: "minecraft:polished_andesite"}, + {pos: [4, 0, 4], state: "minecraft:polished_andesite"}, + {pos: [0, 1, 0], state: "minecraft:air"}, + {pos: [0, 1, 1], state: "minecraft:air"}, + {pos: [0, 1, 2], state: "minecraft:air"}, + {pos: [0, 1, 3], state: "minecraft:air"}, + {pos: [0, 1, 4], state: "minecraft:air"}, + {pos: [1, 1, 0], state: "minecraft:air"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "computercraft:speaker{facing:north}", nbt: {id: "computercraft:speaker"}}, + {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:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "speaker_test.will_not_play_record", On: 1b, id: "computercraft:computer_normal"}}, + {pos: [2, 1, 3], state: "minecraft:air"}, + {pos: [2, 1, 4], state: "minecraft:air"}, + {pos: [3, 1, 0], state: "minecraft:air"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "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:air", + "computercraft:speaker{facing:north}", + "computercraft:computer_normal{facing:north,state:on}" + ] +} From 836d6b939e058600c49afc514e2c4159c6a8e997 Mon Sep 17 00:00:00 2001 From: cyberbit Date: Sun, 24 Mar 2024 09:54:01 -0500 Subject: [PATCH 06/19] Fix cc.image.nft.draw signature --- .../data/computercraft/lua/rom/modules/main/cc/image/nft.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua index 8ba116f0f..09042db95 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua @@ -89,7 +89,7 @@ end -- -- @tparam table image An image, as returned from [`load`] or [`parse`]. -- @tparam number xPos The x position to start drawing at. --- @tparam number xPos The y position to start drawing at. +-- @tparam number yPos The y position to start drawing at. -- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the -- current terminal. local function draw(image, xPos, yPos, target) From 0d3e00cc416f7e3732bb4f2f9c200a7452b7ea73 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 24 Mar 2024 15:12:23 +0000 Subject: [PATCH 07/19] Small cleanup to OS API docs - Mention the timer event in os.startTimer. Really we should have a similar example here too, but let's at least link the two for now. - Fix strftime link --- gradle/libs.versions.toml | 2 +- .../dan200/computercraft/core/apis/OSAPI.java | 31 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f108cea40..7a8512048 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ forgeGradle = "6.0.20" githubRelease = "2.5.2" gradleVersions = "0.50.0" ideaExt = "1.1.7" -illuaminate = "0.1.0-69-gf294ab2" +illuaminate = "0.1.0-71-g378d86e" librarian = "1.+" lwjgl = "3.3.3" minotaur = "2.+" diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java index 4540acd50..43685ab7a 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -143,27 +143,27 @@ public class OSAPI implements ILuaAPI { /** * Starts a timer that will run for the specified number of seconds. Once - * the timer fires, a {@code timer} event will be added to the queue with - * the ID returned from this function as the first parameter. + * the timer fires, a [`timer`] event will be added to the queue with the ID + * returned from this function as the first parameter. *

- * As with [sleep][`os.sleep`], {@code timer} will automatically be rounded up - * to the nearest multiple of 0.05 seconds, as it waits for a fixed amount - * of world ticks. + * As with [sleep][`os.sleep`], the time will automatically be rounded up to + * the nearest multiple of 0.05 seconds, as it waits for a fixed amount of + * world ticks. * - * @param timer The number of seconds until the timer fires. - * @return The ID of the new timer. This can be used to filter the - * {@code timer} event, or {@link #cancelTimer cancel the timer}. + * @param time The number of seconds until the timer fires. + * @return The ID of the new timer. This can be used to filter the [`timer`] + * event, or {@linkplain #cancelTimer cancel the timer}. * @throws LuaException If the time is below zero. * @see #cancelTimer To cancel a timer. */ @LuaFunction - public final int startTimer(double timer) throws LuaException { - return apiEnvironment.startTimer(Math.round(checkFinite(0, timer) / 0.05)); + public final int startTimer(double time) throws LuaException { + return apiEnvironment.startTimer(Math.round(checkFinite(0, time) / 0.05)); } /** - * Cancels a timer previously started with startTimer. This will stop the - * timer from firing. + * Cancels a timer previously started with {@link #startTimer(double)}. This + * will stop the timer from firing. * * @param token The ID of the timer to cancel. * @cc.since 1.6 @@ -399,10 +399,9 @@ public class OSAPI implements ILuaAPI { * Returns a date string (or table) using a specified format string and * optional time to format. *

- * The format string takes the same formats as C's {@code strftime} function - * (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it - * can be prefixed with an exclamation mark ({@code !}) to use UTC time - * instead of the server's local timezone. + * The format string takes the same formats as C's [strftime](http://www.cplusplus.com/reference/ctime/strftime/) + * function. The format string can also be prefixed with an exclamation mark + * ({@code !}) to use UTC time instead of the server's local timezone. *

* If the format is exactly {@code *t} (optionally prefixed with {@code !}), a * table will be returned instead. This table has fields for the year, month, From 9af1aa1ecf66b3726a757eecde51cb95ab364f26 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 25 Mar 2024 08:59:08 +0000 Subject: [PATCH 08/19] Fallback to the current side when getting item cap Fixes #1764 --- .../generic/methods/InventoryMethods.java | 13 +++++++++---- .../computercraft/shared/util/CapabilityUtil.java | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java index 49ab64b2d..09cec73f1 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -8,7 +8,9 @@ import dan200.computercraft.api.detail.VanillaDetailRegistries; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.shared.platform.ForgeContainerTransfer; +import dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.world.Container; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraftforge.common.capabilities.ForgeCapabilities; @@ -73,7 +75,7 @@ public final class InventoryMethods extends AbstractInventoryMethods The type of the underlying capability. * @return The extracted capability, if present. */ - public static LazyOptional getCapability(ICapabilityProvider provider, Capability capability, Direction side) { + public static LazyOptional getCapability(ICapabilityProvider provider, Capability capability, @Nullable Direction side) { var cap = provider.getCapability(capability); - return cap.isPresent() ? cap : provider.getCapability(capability, side); + return !cap.isPresent() && side != null ? provider.getCapability(capability, side) : cap; } } From 63580b4acb81236566d7fe2698dc58e142905127 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 26 Mar 2024 21:40:44 +0000 Subject: [PATCH 09/19] Fallback to the current side when getting fluid cap We did this for item caps in 9af1aa1ecf66b3726a757eecde51cb95ab364f26, but makes sense to do this for fluid methods too. --- .../peripheral/generic/methods/FluidMethods.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java index a550b073f..4408b8b95 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java @@ -8,7 +8,9 @@ import dan200.computercraft.api.detail.ForgeDetailRegistries; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.shared.platform.RegistryWrappers; +import dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.common.capabilities.ICapabilityProvider; @@ -53,7 +55,7 @@ public final class FluidMethods extends AbstractFluidMethods { var location = computer.getAvailablePeripheral(toName); if (location == null) throw new LuaException("Target '" + toName + "' does not exist"); - var to = extractHandler(location.getTarget()); + var to = extractHandler(location); if (to == null) throw new LuaException("Target '" + toName + "' is not an tank"); int actualLimit = limit.orElse(Integer.MAX_VALUE); @@ -78,7 +80,7 @@ public final class FluidMethods extends AbstractFluidMethods { var location = computer.getAvailablePeripheral(fromName); if (location == null) throw new LuaException("Target '" + fromName + "' does not exist"); - var from = extractHandler(location.getTarget()); + var from = extractHandler(location); if (from == null) throw new LuaException("Target '" + fromName + "' is not an tank"); int actualLimit = limit.orElse(Integer.MAX_VALUE); @@ -90,11 +92,14 @@ public final class FluidMethods extends AbstractFluidMethods { } @Nullable - private static IFluidHandler extractHandler(@Nullable Object object) { + private static IFluidHandler extractHandler(IPeripheral peripheral) { + var object = peripheral.getTarget(); + var direction = peripheral instanceof dan200.computercraft.shared.peripheral.generic.GenericPeripheral sided ? sided.side() : null; + if (object instanceof BlockEntity blockEntity && blockEntity.isRemoved()) return null; if (object instanceof ICapabilityProvider provider) { - var cap = provider.getCapability(ForgeCapabilities.FLUID_HANDLER); + var cap = CapabilityUtil.getCapability(provider, ForgeCapabilities.FLUID_HANDLER, direction); if (cap.isPresent()) return cap.orElseThrow(NullPointerException::new); } From 6363164f2b9d351cd26290177cb7aa8dbf5722c3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 26 Mar 2024 21:54:48 +0000 Subject: [PATCH 10/19] Fix creating a zero-sized pocket terminal When the terminal data is not present, width/height are set to 0, rather than the terminal's width/height. This meant we'd create an empty terminal, which then crashes when we try to render it. We now make the terminal nullable and initialise it the first time we receive the terminal data. To prevent future mistakes, we hide width/height, and use TerminalState.create everywhere. Fixes #1765 --- .../client/pocket/ClientPocketComputers.java | 8 ++----- .../client/pocket/PocketComputerData.java | 21 ++++++++++++++----- .../computer/terminal/TerminalState.java | 7 +++---- .../peripheral/monitor/ClientMonitor.java | 7 +++++-- .../computer/terminal/TerminalStateTest.java | 13 ++++-------- .../gametest/Pocket_Computer_Test.kt | 4 ++-- 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java b/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java index 120f9d3ea..cdbb9144b 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java +++ b/projects/common/src/client/java/dan200/computercraft/client/pocket/ClientPocketComputers.java @@ -6,7 +6,6 @@ package dan200.computercraft.client.pocket; import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ServerComputer; -import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.network.client.PocketComputerDataMessage; import dan200.computercraft.shared.pocket.items.PocketComputerItem; @@ -47,13 +46,10 @@ public final class ClientPocketComputers { public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) { var computer = instances.get(instanceId); if (computer == null) { - var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour); - instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal)); + instances.put(instanceId, new PocketComputerData(state, lightColour, terminalData)); } else { - computer.setState(state, lightColour); + computer.setState(state, lightColour, terminalData); } - - if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal()); } public static @Nullable PocketComputerData get(ItemStack stack) { diff --git a/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java b/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java index a5cf16d31..d94de5388 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java +++ b/projects/common/src/client/java/dan200/computercraft/client/pocket/PocketComputerData.java @@ -6,8 +6,11 @@ package dan200.computercraft.client.pocket; import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; +import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.pocket.core.PocketServerComputer; +import javax.annotation.Nullable; + /** * Clientside data about a pocket computer. *

@@ -19,21 +22,21 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer; * @see PocketServerComputer The server-side pocket computer. */ public final class PocketComputerData { - private final NetworkedTerminal terminal; + private @Nullable NetworkedTerminal terminal; private ComputerState state; private int lightColour; - PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) { + PocketComputerData(ComputerState state, int lightColour, TerminalState terminalData) { this.state = state; this.lightColour = lightColour; - this.terminal = terminal; + if (terminalData.hasTerminal()) terminal = terminalData.create(); } public int getLightState() { return state != ComputerState.OFF ? lightColour : -1; } - public NetworkedTerminal getTerminal() { + public @Nullable NetworkedTerminal getTerminal() { return terminal; } @@ -41,8 +44,16 @@ public final class PocketComputerData { return state; } - void setState(ComputerState state, int lightColour) { + void setState(ComputerState state, int lightColour, TerminalState terminalData) { this.state = state; this.lightColour = lightColour; + + if (terminalData.hasTerminal()) { + if (terminal == null) { + terminal = terminalData.create(); + } else { + terminalData.apply(terminal); + } + } } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java index 19a554850..91b25a594 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java @@ -18,10 +18,9 @@ import javax.annotation.Nullable; * states, etc... */ public class TerminalState { - public final boolean colour; - - public final int width; - public final int height; + private final boolean colour; + private final int width; + private final int height; @Nullable private final ByteBuf buffer; diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java index 53630d80f..d4f553d81 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java @@ -56,8 +56,11 @@ public final class ClientMonitor { void read(TerminalState state) { if (state.hasTerminal()) { - if (terminal == null) terminal = new NetworkedTerminal(state.width, state.height, state.colour); - state.apply(terminal); + if (terminal == null) { + terminal = state.create(); + } else { + state.apply(terminal); + } terminalChanged = true; } else { if (terminal != null) { diff --git a/projects/common/src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java b/projects/common/src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java index dceb64e7f..1d953dd2a 100644 --- a/projects/common/src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java +++ b/projects/common/src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java @@ -11,7 +11,8 @@ import org.junit.jupiter.api.RepeatedTest; import java.util.Random; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Tests {@link TerminalState} round tripping works as expected. @@ -42,6 +43,7 @@ public class TerminalStateTest { private static void checkEqual(Terminal expected, Terminal actual) { assertNotNull(expected, "Expected cannot be null"); assertNotNull(actual, "Actual cannot be null"); + assertEquals(expected.isColour(), actual.isColour(), "isColour must match"); assertEquals(expected.getHeight(), actual.getHeight(), "Heights must match"); assertEquals(expected.getWidth(), actual.getWidth(), "Widths must match"); @@ -51,13 +53,6 @@ public class TerminalStateTest { } private static NetworkedTerminal read(FriendlyByteBuf buffer) { - var state = new TerminalState(buffer); - assertTrue(state.colour); - - if (!state.hasTerminal()) return null; - - var other = new NetworkedTerminal(state.width, state.height, true); - state.apply(other); - return other; + return new TerminalState(buffer).create(); } } diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt index d45844039..9df702940 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt @@ -41,7 +41,7 @@ class Pocket_Computer_Test { val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!! assertEquals(ComputerState.ON, pocketComputer.state) - val term = pocketComputer.terminal + val term = pocketComputer.terminal!! assertEquals("Hello, world!", term.getLine(0).toString().trim(), "Terminal contents is synced") } // Update the terminal contents again. @@ -57,7 +57,7 @@ class Pocket_Computer_Test { val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!! assertEquals(ComputerState.BLINKING, pocketComputer.state) - val term = pocketComputer.terminal + val term = pocketComputer.terminal!! assertEquals("Updated text :)", term.getLine(0).toString().trim(), "Terminal contents is synced") } } From b9ba2534a4183ae47efb29ec0a6b29e3b7756457 Mon Sep 17 00:00:00 2001 From: Matthew Wilbern <39929480+fatboychummy@users.noreply.github.com> Date: Fri, 29 Mar 2024 04:24:11 -0600 Subject: [PATCH 11/19] `speaker sound` command (#1747) --- .../lua/rom/programs/fun/speaker.lua | 38 +++++++++++++++++++ .../data/computercraft/lua/rom/startup.lua | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua index 928c403dd..cee07b714 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua @@ -132,9 +132,47 @@ elseif cmd == "play" then end handle.close() +elseif cmd == "sound" then + local _, sound, volume, pitch, name = ... + + if not sound then + error("Usage: speaker sound [volume] [pitch] [speaker]", 0) + return + end + + if volume then + volume = tonumber(volume) + if not volume then + error("Volume must be a number", 0) + end + if volume < 0 or volume > 3 then + error("Volume must be between 0 and 3", 0) + end + end + + if pitch then + pitch = tonumber(pitch) + if not pitch then + error("Pitch must be a number", 0) + end + if pitch < 0 or pitch > 2 then + error("Pitch must be between 0 and 2", 0) + end + end + + local speaker = get_speakers(name)[1] + + if speaker.playSound(sound, volume, pitch) then + print(("Played sound %q on speaker %q with volume %s and pitch %s."):format( + sound, peripheral.getName(speaker), volume or 1, pitch or 1 + )) + else + error(("Could not play sound %q"):format(sound), 0) + end else local programName = arg[0] or fs.getName(shell.getRunningProgram()) print("Usage:") print(programName .. " play [speaker]") + print(programName .. " sound [volume] [pitch] [speaker]") print(programName .. " stop [speaker]") end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua index 2c1f147a3..f3b9f7a08 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua @@ -117,7 +117,7 @@ shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build( completion.peripheral )) shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build( - { completion.choice, { "play ", "stop " } }, + { completion.choice, { "play ", "sound ", "stop " } }, function(shell, text, previous) if previous[2] == "play" then return completion.file(shell, text, previous, true) elseif previous[2] == "stop" then return completion.peripheral(shell, text, previous, false) From 0f623c2cca87ee27e9f8438e5e9df45a7a15ab26 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 1 Apr 2024 13:55:44 +0100 Subject: [PATCH 12/19] Move can-place modem logic to one place This should make future changes easier. Closes #1769. --- .../shared/peripheral/modem/ModemShapes.java | 16 ++++++++++++++++ .../peripheral/modem/wired/CableBlock.java | 5 +++-- .../modem/wireless/WirelessModemBlock.java | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java index 57c5c5742..f8d623e02 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java @@ -4,7 +4,10 @@ package dan200.computercraft.shared.peripheral.modem; +import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -22,4 +25,17 @@ public final class ModemShapes { var direction = facing.ordinal(); return direction < BOXES.length ? BOXES[direction] : Shapes.block(); } + + /** + * Determine if a block can support a modem. + * + * @param level The current level. + * @param pos The position of the adjacent block. + * @param side The side the modem will be placed against. + * @return Whether this block can support a modem. + */ + public static boolean canSupport(LevelReader level, BlockPos pos, Direction side) { + // TODO(1.20.4): Check the side is a full-block instead. + return Block.canSupportCenter(level, pos, side); + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java index bb9a728dc..0d068b58f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlock.java @@ -6,6 +6,7 @@ package dan200.computercraft.shared.peripheral.modem.wired; import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.peripheral.modem.ModemShapes; import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.util.WaterloggableHelpers; import dan200.computercraft.shared.util.WorldUtil; @@ -183,7 +184,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB // Pop our modem if needed. var dir = state.getValue(MODEM).getFacing(); - if (dir != null && dir.equals(side) && !canSupportCenter(level, otherPos, side.getOpposite())) { + if (dir != null && dir.equals(side) && !ModemShapes.canSupport(level, otherPos, side.getOpposite())) { // If we've no cable, follow normal Minecraft logic and just remove the block. if (!state.getValue(CABLE)) return getFluidState(state).createLegacyBlock(); @@ -212,7 +213,7 @@ public class CableBlock extends Block implements SimpleWaterloggedBlock, EntityB var facing = state.getValue(MODEM).getFacing(); if (facing == null) return true; - return canSupportCenter(world, pos.relative(facing), facing.getOpposite()); + return ModemShapes.canSupport(world, pos.relative(facing), facing.getOpposite()); } @Nullable diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlock.java index c2cb274ed..391dbd785 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlock.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlock.java @@ -75,7 +75,7 @@ public class WirelessModemBlock extends DirectionalBlock implements SimpleWaterl @Deprecated public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) { var facing = state.getValue(FACING); - return canSupportCenter(world, pos.relative(facing), facing.getOpposite()); + return ModemShapes.canSupport(world, pos.relative(facing), facing.getOpposite()); } @Nullable From 0c1ab780bb65c55ffdea6c6779adb5914239f8d4 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 1 Apr 2024 22:21:18 +0100 Subject: [PATCH 13/19] Validate arguments in the vector API This doesn't produce the best error messages (should "self" be argument 0 or 1?), but is better than throwing errors in vector's internals. --- .../computercraft/lua/rom/apis/vector.lua | 66 ++++++++++++----- .../test-rom/spec/apis/vector_spec.lua | 71 +++++++++++++++++++ 2 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 projects/core/src/test/resources/test-rom/spec/apis/vector_spec.lua diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua index 71f4be94f..9501f53c1 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/vector.lua @@ -13,6 +13,11 @@ -- @module vector -- @since 1.31 +local getmetatable = getmetatable +local expect = dofile("rom/modules/main/cc/expect.lua").expect + +local vmetatable + --- A 3-dimensional vector, with `x`, `y`, and `z` values. -- -- This is suitable for representing both position and directional vectors. @@ -27,6 +32,9 @@ local vector = { -- @usage v1:add(v2) -- @usage v1 + v2 add = function(self, o) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end + return vector.new( self.x + o.x, self.y + o.y, @@ -42,6 +50,9 @@ local vector = { -- @usage v1:sub(v2) -- @usage v1 - v2 sub = function(self, o) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end + return vector.new( self.x - o.x, self.y - o.y, @@ -52,30 +63,36 @@ local vector = { --- Multiplies a vector by a scalar value. -- -- @tparam Vector self The vector to multiply. - -- @tparam number m The scalar value to multiply with. + -- @tparam number factor The scalar value to multiply with. -- @treturn Vector A vector with value `(x * m, y * m, z * m)`. - -- @usage v:mul(3) - -- @usage v * 3 - mul = function(self, m) + -- @usage vector.new(1, 2, 3):mul(3) + -- @usage vector.new(1, 2, 3) * 3 + mul = function(self, factor) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + expect(2, factor, "number") + return vector.new( - self.x * m, - self.y * m, - self.z * m + self.x * factor, + self.y * factor, + self.z * factor ) end, --- Divides a vector by a scalar value. -- -- @tparam Vector self The vector to divide. - -- @tparam number m The scalar value to divide by. + -- @tparam number factor The scalar value to divide by. -- @treturn Vector A vector with value `(x / m, y / m, z / m)`. - -- @usage v:div(3) - -- @usage v / 3 - div = function(self, m) + -- @usage vector.new(1, 2, 3):div(3) + -- @usage vector.new(1, 2, 3) / 3 + div = function(self, factor) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + expect(2, factor, "number") + return vector.new( - self.x / m, - self.y / m, - self.z / m + self.x / factor, + self.y / factor, + self.z / factor ) end, @@ -83,8 +100,9 @@ local vector = { -- -- @tparam Vector self The vector to negate. -- @treturn Vector The negated vector. - -- @usage -v + -- @usage -vector.new(1, 2, 3) unm = function(self) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end return vector.new( -self.x, -self.y, @@ -99,6 +117,9 @@ local vector = { -- @treturn Vector The dot product of `self` and `o`. -- @usage v1:dot(v2) dot = function(self, o) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end + return self.x * o.x + self.y * o.y + self.z * o.z end, @@ -109,6 +130,9 @@ local vector = { -- @treturn Vector The cross product of `self` and `o`. -- @usage v1:cross(v2) cross = function(self, o) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end + return vector.new( self.y * o.z - self.z * o.y, self.z * o.x - self.x * o.z, @@ -120,6 +144,7 @@ local vector = { -- @tparam Vector self This vector. -- @treturn number The length of this vector. length = function(self) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) end, @@ -141,6 +166,9 @@ local vector = { -- nearest 0.5. -- @treturn Vector The rounded vector. round = function(self, tolerance) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + expect(2, tolerance, "number", "nil") + tolerance = tolerance or 1.0 return vector.new( math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance, @@ -156,6 +184,8 @@ local vector = { -- @usage v:tostring() -- @usage tostring(v) tostring = function(self) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + return self.x .. "," .. self.y .. "," .. self.z end, @@ -165,11 +195,15 @@ local vector = { -- @tparam Vector other The second vector to compare to. -- @treturn boolean Whether or not the vectors are equal. equals = function(self, other) + if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end + if getmetatable(other) ~= vmetatable then expect(2, other, "vector") end + return self.x == other.x and self.y == other.y and self.z == other.z end, } -local vmetatable = { +vmetatable = { + __name = "vector", __index = vector, __add = vector.add, __sub = vector.sub, diff --git a/projects/core/src/test/resources/test-rom/spec/apis/vector_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/vector_spec.lua new file mode 100644 index 000000000..663081b2e --- /dev/null +++ b/projects/core/src/test/resources/test-rom/spec/apis/vector_spec.lua @@ -0,0 +1,71 @@ +-- SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +describe("The vector library", function() + local vec = vector.new(1, 2, 3) + + describe("vector.add", function() + it("validates arguments", function() + expect.error(vec.add, nil, vec):eq("bad argument #1 (vector expected, got nil)") + expect.error(vec.add, vec, nil):eq("bad argument #2 (vector expected, got nil)") + end) + + it("returns the correct value", function() + expect(vector.new(1, 2, 3) + vector.new(6, 4, 2)):eq(vector.new(7, 6, 5)) + end) + end) + + describe("vector.sub", function() + it("validates arguments", function() + expect.error(vec.sub, nil, vec):eq("bad argument #1 (vector expected, got nil)") + expect.error(vec.sub, vec, nil):eq("bad argument #2 (vector expected, got nil)") + end) + + it("returns the correct value", function() + expect(vector.new(6, 4, 2) - vector.new(1, 2, 3)):eq(vector.new(5, 2, -1)) + end) + end) + + describe("vector.mul", function() + it("validates arguments", function() + expect.error(vec.mul, nil, vec):eq("bad argument #1 (vector expected, got nil)") + expect.error(vec.mul, vec, nil):eq("bad argument #2 (number expected, got nil)") + end) + + it("returns the correct value", function() + expect(vector.new(1, 2, 3) * 2):eq(vector.new(2, 4, 6)) + end) + end) + + describe("vector.div", function() + it("validates arguments", function() + expect.error(vec.div, nil, vec):eq("bad argument #1 (vector expected, got nil)") + expect.error(vec.div, vec, nil):eq("bad argument #2 (number expected, got nil)") + end) + + it("returns the correct value", function() + expect(vector.new(1, 2, 3) / 2):eq(vector.new(0.5, 1, 1.5)) + end) + end) + + describe("vector.unm", function() + it("validates arguments", function() + expect.error(vec.unm, nil):eq("bad argument #1 (vector expected, got nil)") + end) + + it("returns the correct value", function() + expect(-vector.new(2, 3, 6)):eq(vector.new(-2, -3, -6)) + end) + end) + + describe("vector.length", function() + it("validates arguments", function() + expect.error(vec.length, nil):eq("bad argument #1 (vector expected, got nil)") + end) + + it("returns the correct value", function() + expect(vector.new(2, 3, 6):length()):eq(7) + end) + end) +end) From c8eadf401190db2b9c3145f768063097b9c345bd Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 3 Apr 2024 08:44:30 +0100 Subject: [PATCH 14/19] Register CC's modems as brittle This tells Create that modems will pop-off if their neighbour is moved, and so changes the order that the block is moved in. We possibly should use BlockMovementChecks.AttachedCheck instead, to properly handle the direction modems are facing in. However, this doesn't appear to be part of the public API, so probably best avoided. Fixes #948 --- .../computercraft/data/TagProvider.java | 7 ++++ .../shared/integration/ExternalModTags.java | 37 +++++++++++++++++++ .../data/create/tags/blocks/brittle.json | 4 ++ .../data/create/tags/blocks/brittle.json | 3 ++ 4 files changed, 51 insertions(+) create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/integration/ExternalModTags.java create mode 100644 projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json create mode 100644 projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json diff --git a/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java b/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java index 9b207e09d..4d273bb9e 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java @@ -6,6 +6,7 @@ package dan200.computercraft.data; import dan200.computercraft.api.ComputerCraftTags; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.integration.ExternalModTags; import dan200.computercraft.shared.platform.RegistryWrappers; import net.minecraft.data.tags.ItemTagsProvider; import net.minecraft.data.tags.TagsProvider; @@ -81,6 +82,12 @@ class TagProvider { ); tags.tag(BlockTags.WITHER_IMMUNE).add(ModRegistry.Blocks.COMPUTER_COMMAND.get()); + + tags.tag(ExternalModTags.Blocks.CREATE_BRITTLE).add( + ModRegistry.Blocks.CABLE.get(), + ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get(), + ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get() + ); } public static void itemTags(ItemTagConsumer tags) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/ExternalModTags.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/ExternalModTags.java new file mode 100644 index 000000000..29a666217 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/ExternalModTags.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; + +/** + * Tags defined by external mods. + */ +public final class ExternalModTags { + private ExternalModTags() { + } + + /** + * Block tags defined by external mods. + */ + public static final class Blocks { + private Blocks() { + } + + /** + * Create's "brittle" tag, used to determine if this block needs to be moved before its neighbours. + * + * @see {@code BlockMovementChecks} + */ + public static final TagKey CREATE_BRITTLE = make("create", "brittle"); + + private static TagKey make(String mod, String name) { + return TagKey.create(Registries.BLOCK, new ResourceLocation(mod, name)); + } + } +} diff --git a/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json b/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json new file mode 100644 index 000000000..32215971e --- /dev/null +++ b/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["computercraft:cable", "computercraft:wireless_modem_advanced", "computercraft:wireless_modem_advanced"] +} diff --git a/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json b/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json new file mode 100644 index 000000000..7dc5f506f --- /dev/null +++ b/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json @@ -0,0 +1,3 @@ +{ + "values": ["computercraft:cable", "computercraft:wireless_modem_advanced", "computercraft:wireless_modem_advanced"] +} From 6d14ce625f73d230b1241bd3681a32a18ac316b0 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 3 Apr 2024 09:29:31 +0100 Subject: [PATCH 15/19] Use the correct modem in create:brittle I tested this in-game, I swear! Just, typically, only with ender and wired modems. --- .../src/main/java/dan200/computercraft/data/TagProvider.java | 2 +- .../generated/resources/data/create/tags/blocks/brittle.json | 2 +- .../generated/resources/data/create/tags/blocks/brittle.json | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java b/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java index 4d273bb9e..833287af6 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java @@ -85,7 +85,7 @@ class TagProvider { tags.tag(ExternalModTags.Blocks.CREATE_BRITTLE).add( ModRegistry.Blocks.CABLE.get(), - ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get(), + ModRegistry.Blocks.WIRELESS_MODEM_NORMAL.get(), ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get() ); } diff --git a/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json b/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json index 32215971e..32e528bcf 100644 --- a/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json +++ b/projects/fabric/src/generated/resources/data/create/tags/blocks/brittle.json @@ -1,4 +1,4 @@ { "replace": false, - "values": ["computercraft:cable", "computercraft:wireless_modem_advanced", "computercraft:wireless_modem_advanced"] + "values": ["computercraft:cable", "computercraft:wireless_modem_normal", "computercraft:wireless_modem_advanced"] } diff --git a/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json b/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json index 7dc5f506f..137d8de24 100644 --- a/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json +++ b/projects/forge/src/generated/resources/data/create/tags/blocks/brittle.json @@ -1,3 +1 @@ -{ - "values": ["computercraft:cable", "computercraft:wireless_modem_advanced", "computercraft:wireless_modem_advanced"] -} +{"values": ["computercraft:cable", "computercraft:wireless_modem_normal", "computercraft:wireless_modem_advanced"]} From bce099ef324148d69277509cd0bba5fd81d54ec0 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 3 Apr 2024 21:27:18 +0100 Subject: [PATCH 16/19] Allow mounting folders in the standalone emulator This theoretically allows you to use the emulator to run the test suite (via --mount-ro projects/core/src/test/resources/test-rom/:test-rom), but not sure how useful this is in practice. --- .../main/java/cc/tweaked/standalone/Main.java | 124 ++++++++++++++---- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java index 49b26a6f3..157ef7e24 100644 --- a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java +++ b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java @@ -5,11 +5,16 @@ package cc.tweaked.standalone; +import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.CoreConfig; +import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRule; import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.filesystem.FileMount; +import dan200.computercraft.core.filesystem.FileSystemException; +import dan200.computercraft.core.filesystem.WritableFileMount; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.TextBuffer; import dan200.computercraft.core.util.Colour; @@ -31,6 +36,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.OptionalInt; @@ -55,37 +61,58 @@ public class Main { private static final Logger LOG = LoggerFactory.getLogger(Main.class); private static final boolean DEBUG = Checks.DEBUG; - private record TermSize(int width, int height) { - public static final TermSize DEFAULT = new TermSize(51, 19); - public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$"); - } - - private static T getParsedOptionValue(CommandLine cli, Option opt, Class klass) throws ParseException { - var res = cli.getOptionValue(opt); - if (klass == Path.class) { - try { - return klass.cast(Path.of(res)); - } catch (InvalidPathException e) { - throw new ParseException("'" + res + "' is not a valid path (" + e.getReason() + ")"); - } - } else if (klass == TermSize.class) { - var matcher = TermSize.PATTERN.matcher(res); - if (!matcher.matches()) throw new ParseException("'" + res + "' is not a valid terminal size."); - - return klass.cast(new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)))); - } else { - return klass.cast(TypeHandler.createValue(res, klass)); + private static Path parsePath(String path) throws ParseException { + try { + return Path.of(path); + } catch (InvalidPathException e) { + throw new ParseException("'" + path + "' is not a valid path (" + e.getReason() + ")"); } } + private record TermSize(int width, int height) { + public static final TermSize DEFAULT = new TermSize(51, 19); + public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$"); + + public static TermSize parse(String value) throws ParseException { + var matcher = TermSize.PATTERN.matcher(value); + if (!matcher.matches()) throw new ParseException("'" + value + "' is not a valid terminal size."); + + return new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))); + } + } + + private record MountPaths(Path src, String dest) { + public static final Pattern PATTERN = Pattern.compile("^([^:]+):([^:]+)$"); + + public static MountPaths parse(String value) throws ParseException { + var matcher = MountPaths.PATTERN.matcher(value); + if (!matcher.matches()) throw new ParseException("'" + value + "' is not a mount spec."); + + return new MountPaths(parsePath(matcher.group(1)), matcher.group(2)); + } + } + + private interface ValueParser { + T parse(String path) throws ParseException; + } + @Contract("_, _, _, !null -> !null") - private static @Nullable T getParsedOptionValue(CommandLine cli, Option opt, Class klass, @Nullable T defaultValue) throws ParseException { - return cli.hasOption(opt) ? getParsedOptionValue(cli, opt, klass) : defaultValue; + private static @Nullable T getParsedOptionValue(CommandLine cli, Option opt, ValueParser parser, @Nullable T defaultValue) throws ParseException { + return cli.hasOption(opt) ? parser.parse(cli.getOptionValue(opt)) : defaultValue; + } + + private static List getParsedOptionValues(CommandLine cli, Option opt, ValueParser parser) throws ParseException { + var values = cli.getOptionValues(opt); + if (values == null) return List.of(); + + List parsedValues = new ArrayList<>(values.length); + for (var value : values) parsedValues.add(parser.parse(value)); + return List.copyOf(parsedValues); } public static void main(String[] args) throws InterruptedException { var options = new Options(); - Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt; + Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt, mountOpt, mountRoOpt; options.addOption(resourceOpt = Option.builder("r").argName("PATH").longOpt("resources").hasArg() .desc("The path to the resources directory") .build()); @@ -98,6 +125,12 @@ public class Main { options.addOption(allowLocalDomainsOpt = Option.builder("L").longOpt("allow-local-domains") .desc("Allow accessing local domains with the HTTP API.") .build()); + options.addOption(mountOpt = Option.builder().longOpt("mount").hasArg().argName("SRC:DEST") + .desc("Mount a folder SRC at directory DEST on the computer.") + .build()); + options.addOption(mountRoOpt = Option.builder().longOpt("mount-ro").hasArg().argName("SRC:DEST") + .desc("Mount a read-only folder SRC at directory DEST on the computer.") + .build()); options.addOption(helpOpt = Option.builder("h").longOpt("help") .desc("Print help message") @@ -107,6 +140,7 @@ public class Main { Path computerDirectory; TermSize termSize; boolean allowLocalDomains; + List mounts, readOnlyMounts; try { var cli = new DefaultParser().parse(options, args); if (cli.hasOption(helpOpt)) { @@ -115,10 +149,12 @@ public class Main { } if (!cli.hasOption(resourceOpt)) throw new ParseException("--resources directory is required"); - resourcesDirectory = getParsedOptionValue(cli, resourceOpt, Path.class); - computerDirectory = getParsedOptionValue(cli, computerOpt, Path.class, null); - termSize = getParsedOptionValue(cli, termSizeOpt, TermSize.class, TermSize.DEFAULT); + resourcesDirectory = parsePath(cli.getOptionValue(resourceOpt)); + computerDirectory = getParsedOptionValue(cli, computerOpt, Main::parsePath, null); + termSize = getParsedOptionValue(cli, termSizeOpt, TermSize::parse, TermSize.DEFAULT); allowLocalDomains = cli.hasOption(allowLocalDomainsOpt); + mounts = getParsedOptionValues(cli, mountOpt, MountPaths::parse); + readOnlyMounts = getParsedOptionValues(cli, mountRoOpt, MountPaths::parse); } catch (ParseException e) { System.err.println(e.getLocalizedMessage()); @@ -143,6 +179,7 @@ public class Main { new Terminal(termSize.width(), termSize.height(), true, () -> isDirty.set(true)), 0 ); + computer.addApi(new FileMounter(computer.getAPIEnvironment(), readOnlyMounts, mounts)); computer.turnOn(); runAndInit(gl, computer, isDirty); @@ -154,6 +191,41 @@ public class Main { } } + /** + * An {@link ILuaAPI} which is used to mount additional files, but does not expose any new globals/methods. + */ + private static final class FileMounter implements ILuaAPI { + private final IAPIEnvironment environment; + private final List readOnlyMounts; + private final List mounts; + + FileMounter(IAPIEnvironment environment, List readOnlyMounts, List mounts) { + this.environment = environment; + this.readOnlyMounts = readOnlyMounts; + this.mounts = mounts; + } + + @Override + public String[] getNames() { + return new String[0]; + } + + @Override + public void startup() { + try { + var fs = environment.getFileSystem(); + for (var mount : readOnlyMounts) { + fs.mount(mount.dest(), mount.dest(), new FileMount(mount.src())); + } + for (var mount : mounts) { + fs.mount(mount.dest(), mount.dest(), new WritableFileMount(mount.src().toFile(), 1_000_000)); + } + } catch (FileSystemException e) { + throw new IllegalStateException(e); + } + } + } + private static final int SCALE = 2; private static final int MARGIN = 2; private static final int PIXEL_WIDTH = 6; From 8b2516abb5b01345dc619da35ab956dbbd149d9b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 6 Apr 2024 08:38:44 +0100 Subject: [PATCH 17/19] Update to latest NullAway This fixes several issues with @Nullable fields not being checked. This is great in principle, but a little annoying in practice as MC's @Nullable annotations are sometimes a little overly strict -- we now need to wrap a couple of things in assertNonNull checks. --- gradle/libs.versions.toml | 2 +- .../client/gui/AbstractComputerScreen.java | 18 ++++++++++++------ .../client/gui/NoTermComputerScreen.java | 19 +++++++++++++------ .../client/render/ItemMapLikeRenderer.java | 4 +++- .../blocks/AbstractComputerBlockEntity.java | 2 +- .../diskdrive/DiskDriveBlockEntity.java | 2 +- .../modem/wired/CableBlockEntity.java | 4 ++-- .../monitor/MonitorBlockEntity.java | 4 ++-- .../turtle/blocks/TurtleBlockEntity.java | 2 +- .../dan200/computercraft/export/Exporter.java | 3 ++- .../client/ComputerCraftClient.java | 4 +++- 11 files changed, 41 insertions(+), 23 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a8512048..bbc223a56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ illuaminate = "0.1.0-71-g378d86e" librarian = "1.+" lwjgl = "3.3.3" minotaur = "2.+" -nullAway = "0.9.9" +nullAway = "0.10.25" spotless = "6.23.3" taskTree = "2.1.1" teavm = "0.10.0-SQUID.3" diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java index 50f12c15e..c25c0d059 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java @@ -9,6 +9,7 @@ import dan200.computercraft.client.gui.widgets.DynamicImageButton; import dan200.computercraft.client.gui.widgets.TerminalWidget; import dan200.computercraft.client.network.ClientNetworking; import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.InputHandler; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; @@ -18,6 +19,7 @@ import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.network.server.UploadFileMessage; import net.minecraft.ChatFormatting; import net.minecraft.Util; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; @@ -96,8 +98,8 @@ public abstract class AbstractComputerScreen ext getTerminal().update(); if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) { - new ItemToast(minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN) - .showOrReplace(minecraft.getToasts()); + new ItemToast(minecraft(), displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN) + .showOrReplace(minecraft().getToasts()); uploadNagDeadline = Long.MAX_VALUE; } } @@ -207,7 +209,7 @@ public abstract class AbstractComputerScreen ext return; } - if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer); + if (!toUpload.isEmpty()) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer); } public void uploadResult(UploadResult result, @Nullable Component message) { @@ -223,9 +225,13 @@ public abstract class AbstractComputerScreen ext } private void alert(Component title, Component message) { - OptionScreen.show(minecraft, title, message, - List.of(OptionScreen.newButton(OK, b -> minecraft.setScreen(this))), - () -> minecraft.setScreen(this) + OptionScreen.show(minecraft(), title, message, + List.of(OptionScreen.newButton(OK, b -> minecraft().setScreen(this))), + () -> minecraft().setScreen(this) ); } + + private Minecraft minecraft() { + return Nullability.assertNonNull(minecraft); + } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java index 67ad95ba5..71a3609ad 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/NoTermComputerScreen.java @@ -6,8 +6,10 @@ package dan200.computercraft.client.gui; import dan200.computercraft.client.gui.widgets.TerminalWidget; import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.MenuAccess; @@ -16,6 +18,7 @@ import net.minecraft.world.entity.player.Inventory; import org.lwjgl.glfw.GLFW; import javax.annotation.Nullable; +import java.util.Objects; import static dan200.computercraft.core.util.Nullability.assertNonNull; @@ -44,8 +47,8 @@ public class NoTermComputerScreen extends Screen protected void init() { // First ensure we're still grabbing the mouse, so the user can look around. Then reset bits of state that // grabbing unsets. - minecraft.mouseHandler.grabMouse(); - minecraft.screen = this; + minecraft().mouseHandler.grabMouse(); + minecraft().screen = this; KeyMapping.releaseAll(); super.init(); @@ -64,13 +67,13 @@ public class NoTermComputerScreen extends Screen @Override public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) { - minecraft.player.getInventory().swapPaint(pDelta); + Objects.requireNonNull(minecraft().player).getInventory().swapPaint(pDelta); return super.mouseScrolled(pMouseX, pMouseY, pDelta); } @Override public void onClose() { - minecraft.player.closeContainer(); + Objects.requireNonNull(minecraft().player).closeContainer(); super.onClose(); } @@ -93,12 +96,16 @@ public class NoTermComputerScreen extends Screen public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { super.render(graphics, mouseX, mouseY, partialTicks); - var font = minecraft.font; + var font = minecraft().font; var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8)); var y = 10; for (var line : lines) { - graphics.drawString(font, line, (width / 2) - (minecraft.font.width(line) / 2), y, 0xFFFFFF, true); + graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFF, true); y += 9; } } + + private Minecraft minecraft() { + return Nullability.assertNonNull(minecraft); + } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java index 07a2f8c97..9fecf2bd7 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java @@ -17,6 +17,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemStack; +import java.util.Objects; + /** * A base class for items which have map-like rendering when held in the hand. * @@ -35,7 +37,7 @@ public abstract class ItemMapLikeRenderer { protected abstract void renderItem(PoseStack transform, MultiBufferSource render, ItemStack stack, int light); public void renderItemFirstPerson(PoseStack transform, MultiBufferSource render, int lightTexture, InteractionHand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack) { - Player player = Minecraft.getInstance().player; + Player player = Objects.requireNonNull(Minecraft.getInstance().player); transform.pushPose(); if (hand == InteractionHand.MAIN_HAND && player.getOffhandItem().isEmpty()) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java index 462945cbe..aaf4efe90 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/blocks/AbstractComputerBlockEntity.java @@ -223,7 +223,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements var offsetSide = dir.getOpposite(); var localDir = remapToLocalSide(dir); - computer.setRedstoneInput(localDir, RedstoneUtil.getRedstoneInput(level, targetPos, dir)); + computer.setRedstoneInput(localDir, RedstoneUtil.getRedstoneInput(getLevel(), targetPos, dir)); computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide)); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java index 1cf33ac44..337ef87fa 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveBlockEntity.java @@ -303,7 +303,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity { // Set the id (if needed) and write it back to the media stack. var stack = media.stack().copy(); - mount = media.media().createDataMount(stack, (ServerLevel) level); + mount = media.media().createDataMount(stack, (ServerLevel) getLevel()); updateMediaStack(stack, immediate); return mount; diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java index e52f1b299..5e619d361 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableBlockEntity.java @@ -108,7 +108,7 @@ public class CableBlockEntity extends BlockEntity { void neighborChanged(BlockPos neighbour) { var dir = getModemDirection(); - if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) { + if (!getLevel().isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) { queueRefreshPeripheral(); } } @@ -164,7 +164,7 @@ public class CableBlockEntity extends BlockEntity { .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral()); if (oldVariant != newVariant) { - level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant)); + getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant)); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java index bc377a519..4442ba324 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorBlockEntity.java @@ -175,7 +175,7 @@ public class MonitorBlockEntity extends BlockEntity { } else { // Otherwise fetch the origin and attempt to get its monitor // Note this may load chunks, but we don't really have a choice here. - var te = level.getBlockEntity(toWorldPos(0, 0)); + var te = getLevel().getBlockEntity(toWorldPos(0, 0)); if (!(te instanceof MonitorBlockEntity monitor)) return null; return serverMonitor = monitor.createServerMonitor(); @@ -417,7 +417,7 @@ public class MonitorBlockEntity extends BlockEntity { @Nullable private MonitorBlockEntity tryResizeAt(BlockPos pos, int width, int height) { - var tile = level.getBlockEntity(pos); + var tile = getLevel().getBlockEntity(pos); if (tile instanceof MonitorBlockEntity monitor && isCompatible(monitor)) { monitor.resize(width, height); return monitor; diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java index af3136129..3160aac02 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/blocks/TurtleBlockEntity.java @@ -165,7 +165,7 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba public void setDirection(Direction dir) { if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH; - level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir)); + getLevel().setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir)); updateRedstone(); updateInputsImmediately(); diff --git a/projects/common/src/testMod/java/dan200/computercraft/export/Exporter.java b/projects/common/src/testMod/java/dan200/computercraft/export/Exporter.java index 266b6c669..ebec60496 100644 --- a/projects/common/src/testMod/java/dan200/computercraft/export/Exporter.java +++ b/projects/common/src/testMod/java/dan200/computercraft/export/Exporter.java @@ -33,6 +33,7 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -79,7 +80,7 @@ public class Exporter { } // Now find all CC recipes. - var level = Minecraft.getInstance().level; + var level = Objects.requireNonNull(Minecraft.getInstance().level); for (var recipe : level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING)) { var result = recipe.getResultItem(level.registryAccess()); if (!RegistryWrappers.ITEMS.getKey(result.getItem()).getNamespace().equals(ComputerCraftAPI.MOD_ID)) { diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java index 4a4f89f73..51e99242b 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java +++ b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java @@ -32,6 +32,8 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; +import java.util.Objects; + import static dan200.computercraft.core.util.Nullability.assertNonNull; public class ComputerCraftClient { @@ -77,7 +79,7 @@ public class ComputerCraftClient { if (hit.getType() != HitResult.Type.BLOCK) return ItemStack.EMPTY; var pos = ((BlockHitResult) hit).getBlockPos(); - var level = Minecraft.getInstance().level; + var level = Objects.requireNonNull(Minecraft.getInstance().level); var state = level.getBlockState(pos); if (!(state.getBlock() instanceof CableBlock cable)) return ItemStack.EMPTY; From 825d45eb261e9fa7b546d2526e1a9ab9122c9399 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 6 Apr 2024 08:44:03 +0100 Subject: [PATCH 18/19] Fix NPE when rendering turtle's label Minecraft.hitResult may /technically/ be null when rendering a turtle. In vanilla, this doesn't appear to happen, but other mods (e.g. Immersive Portals) may still take advantage of this. This hitResult is then propagated to BlockEntityRenderDispatcher, where the field was /not/ marked as nullable. This meant we didn't even notice the potential of an NPE! Closes #1775 --- .../client/render/TurtleBlockEntityRenderer.java | 2 +- .../main/kotlin/cc/tweaked/linter/MinecraftLibraryModel.kt | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java index c52c5a485..caf56621e 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java @@ -55,7 +55,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer = ImmutableSet.of( + // This inherits from Minecraft.hitResult, and so can also be null. + fieldRef("net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher", "cameraHitResult"), + ) } From ad493253760cc84d5e7cd3bac9732ef78bc1fad8 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 7 Apr 2024 21:20:56 +0100 Subject: [PATCH 19/19] Bump CC:T to 1.110.2 --- gradle.properties | 2 +- .../data/computercraft/lua/rom/help/changelog.md | 13 +++++++++++++ .../data/computercraft/lua/rom/help/whatsnew.md | 13 ++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index b49c90155..8363bedf6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error # Mod properties isUnstable=false -modVersion=1.110.1 +modVersion=1.110.2 # Minecraft properties: We want to configure this here so we can read it in settings.gradle mcVersion=1.20.1 diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md index a5ea53369..6e6f0e452 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/changelog.md @@ -1,3 +1,16 @@ +# New features in CC: Tweaked 1.110.2 + +* Add `speaker sound` command (fatboychummy). + +Several bug fixes: +* Improve error when calling `speaker play` with no path (fatboychummy). +* Prevent playing music discs with `speaker.playSound`. +* Various documentation fixes (cyberbit). +* Fix generic peripherals not being able to transfer to some inventories on Forge. +* Fix rare crash when holding a pocket computer. +* Fix modems breaking when moved by Create. +* Fix crash when rendering a turtle through an Immersive Portals portal. + # New features in CC: Tweaked 1.110.1 Several bug fixes: diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md index 860667454..4de3ce687 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md @@ -1,7 +1,14 @@ -New features in CC: Tweaked 1.110.1 +New features in CC: Tweaked 1.110.2 + +* Add `speaker sound` command (fatboychummy). Several bug fixes: -* Fix computers not turning on after they're unloaded/not-ticked for a while. -* Fix networking cables sometimes not connecting on Forge. +* Improve error when calling `speaker play` with no path (fatboychummy). +* Prevent playing music discs with `speaker.playSound`. +* Various documentation fixes (cyberbit). +* Fix generic peripherals not being able to transfer to some inventories on Forge. +* Fix rare crash when holding a pocket computer. +* Fix modems breaking when moved by Create. +* Fix crash when rendering a turtle through an Immersive Portals portal. Type "help changelog" to see the full version history.