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

Compare commits

...

35 Commits

Author SHA1 Message Date
Jonathan Coates
62f2cd5cb2 Merge branch 'mc-1.16.x' into mc-1.17.x 2021-12-25 08:05:25 +00:00
Jonathan Coates
e558b31b2b Fix some typos in a dfpwm example 2021-12-21 22:25:16 +00:00
Jonathan Coates
afd82fbf1f Add speaker support to the documentation website
Happy to pick a different piece of audio, but this seemed like a fun one
to me.
2021-12-21 22:20:57 +00:00
Jonathan Coates
f794ce42ab Merge branch 'mc-1.16.x' into mc-1.17.x 2021-12-21 15:10:19 +00:00
Jonathan Coates
f470478a0f Bump CC:T version to 1.100
We're still a few days away from release, but don't think anything else
is going to change. And I /really/ don't want to have to write this
changelog (and then merge into later versions) on the 25th.
2021-12-21 14:55:01 +00:00
Jonathan Coates
aa009df740 Improve fs API introduction
Again, not perfect, but better than a single sentence.
2021-12-21 14:39:08 +00:00
Jonathan Coates
0c6c0badde Move turtle docs into the Java code instead
Yeah, should have seen that coming
2021-12-21 12:00:13 +00:00
Jonathan Coates
bed2e0b658 Write an introduction to the turtle API
It's better at least, I just don't know if it's good.
2021-12-21 11:53:46 +00:00
Jonathan Coates
0f9ddac83c Copy and paste the wiki guide on require
I wrote the original, so I don't need to feel guilty :)

Closes #565.
2021-12-21 00:55:16 +00:00
Jonathan Coates
932b77d7ee Rewrite several doc introductions
Mostly focussing on rednet and modem here. Not sure if I made them any
better, we'll see!
2021-12-21 00:27:07 +00:00
Jonathan Coates
5eedea1bbb Don't push non-pushable entities
Fixes #949
2021-12-20 17:58:39 +00:00
Jonathan Coates
114261944a Tick pocket computers in item entity form
See #995. And no, just because I'm adding this doesn't mean it's not a
terrible issue.
2021-12-20 17:37:42 +00:00
Jonathan Coates
4d10639efb Use correct Java annotations package 2021-12-20 12:19:52 +00:00
Jonathan Coates
aa36b49c50 Enqueue audio when receiving it
While Minecraft will automatically push a new buffer when one is
exhausted, this doesn't help if there's only a single buffer in the
queue, and you end up with stutter.

By enquing a buffer when receiving sound we ensure there's always
something queued. I'm not 100% happy with this solution, but it does
alleviate some of the concerns in #993.

Also reduce the size of the client buffer to 0.5s from 1.5s. This is
still enough to ensure seamless audio when the server is running slow (I
tested at 13 tps, but should be able to go much worse).
2021-12-19 19:50:43 +00:00
Jonathan Coates
8a1067940d Account for the game being paused when tracking sound progress
When the game is paused in SSP world, speakers are not ticked. However,
System.nanoTime() continues to increase, which means the next tick
speakers believe there has been a big jump and so schedule a bunch of
extra audio.

To avoid this, we keep track of how long the game has been paused offset
nanoTime by that amount.

Fixes #994
2021-12-19 16:29:06 +00:00
Jonathan Coates
aa0d544bba Remove MoreRed integration
It's not been updated to 1.17/1.18, nor touched since July. Can easily
be added back in if this changes.
2021-12-18 11:35:52 +00:00
Jonathan Coates
2f6ad00764 Use Java 16 ByteBuffer methods where possible 2021-12-18 11:34:44 +00:00
Jonathan Coates
05da4dd362 Merge branch 'mc-1.16.x' into mc-1.17.x 2021-12-18 11:25:28 +00:00
Jonathan Coates
0477b2742c Use duplicate() instead of rewind()
It's just more confusing having to keep track of where the ByteBuffer is
at. In this case, I think we were forgetting to rewind after computing
the digest.

Hopefully we'll be able to drop some of these in 1.17 as Java 16 has
a few more ByteBuffer methods

Fixes #992
2021-12-18 11:23:12 +00:00
Jonathan Coates
fe3c42ce22 Mark 1.17 (and 1.18) as stable 2021-12-17 16:44:29 +00:00
Jonathan Coates
82a7edee12 Merge branch 'mc-1.16.x' into mc-1.17.x 2021-12-14 20:07:48 +00:00
Jonathan Coates
b048b6666d Add arbitrary audio support to speakers (#982)
Speakers can now play arbitrary PCM audio, sampled at 48kHz and with a
resolution of 8 bits. Programs can build up buffers of audio locally,
play it using `speaker.playAudio`, where it is encoded to DFPWM, sent
across the network, decoded, and played on the client.

`speaker.playAudio` may return false when a chunk of audio has been
submitted but not yet sent to the client. In this case, the program
should wait for a speaker_audio_empty event and try again, repeating
until it works.

While the API is a little odd, this gives us fantastic flexibility (we
can play arbitrary streams of audio) while still being resilient in the
presence of server lag (either TPS or on the computer thread).

Some other notes:
 - There is a significant buffer on both the client and server, which
   means that sound take several seconds to finish after playing has
   started. One can force it to be stopped playing with the new
  `speaker.stop` call.

 - This also adds a `cc.audio.dfpwm` module, which allows encoding and
   decoding DFPWM1a audio files.

 - I spent so long writing the documentation for this. Who knows if it'll
   be helpful!
2021-12-13 22:56:59 +00:00
Jonathan Coates
e16f66e128 Some bits of rednet cleanup
- Remove all the hungrarian notation in variables. Currently leaving
   the format of rednet messages for now, while I work out whether this
   counts as part of the public API or not.

 - Fix the "repeat" program failing with broadcast packets. This was
   introduced in #900, but I don't think anybody noticed. Will be more
   relevant when #955 is implemented though.
2021-12-13 14:30:13 +00:00
Jonathan Coates
1cfad31a0d Separate breaking progress for wired modems
This means that if the current player is breaking a cable/wired modem,
only the part they're looking at has breaking progress. Closes #355.

A mixin is definitely not the cleanest way to do this. There's a couple
of alternatives:

 - CodeChickenLib's approach of overriding the BlockRendererDispatcher
   instance with a delegating subclasss. One mod doing this is fine,
   several is Not Great.o

 - Adding a PR to Forge: I started this, and it's definitely the ideal
   solution, but any event for this would have a ton of fields and just
   ended up looking super ugly.
2021-12-13 13:30:43 +00:00
Jonathan Coates
6196aae488 Merge branch 'mc-1.16.x' into mc-1.17.x 2021-12-11 07:49:33 +00:00
Jonathan Coates
92a0ef2b75 Bump CC:T version 2021-12-11 07:37:10 +00:00
Jonathan Coates
1f6e0f287d Ensure the origin monitor is valid too
Blurh, still not sure if this is Correct or anything, but have no clue
what's causing this. Fixes #985. Hopefully.
2021-12-10 13:13:31 +00:00
Jonathan Coates
0e4b7a5a75 Prevent terminal buttons stealing focus
I have shutdown my computer by accident far too many times now.
2021-12-08 23:16:53 +00:00
Jonathan Coates
47ad7a35dc Fix NPE when pulling an event with no type
I assume people have broken coroutine dispatchers - I didn't think it
was possible to queue an actual event with no type.

See cc-tweaked/cc-restitched#31. Will fix it too once merged downstream!
2021-12-08 22:47:21 +00:00
Jonathan Coates
3eab2a9b57 Add support for a zero-copy Lua table
The API is entirely designed for the needs of the speaker right now, so
doesn't do much else.
2021-12-07 18:27:29 +00:00
Jonathan Coates
c4024a4c4c Use an admonition instead 2021-12-02 22:41:58 +00:00
Jonathan Coates
f5fb82cd7d Merge pull request #977 from MCJack123/patch-9
Add package.searchpath
2021-12-02 12:34:07 +00:00
MCJack123
e18ba8a2c2 Add package.searchpath 2021-12-01 18:55:24 -05:00
Jonathan Coates
422bfdb60d Add 1.18 and remove 1.15 from the issue template
No, we're not pushing it to Curse yet, but while I remember.
2021-12-01 20:24:37 +00:00
Jonathan Coates
1851ed31cd Release keys when opening the offhand pocket computer screen
Opening a screen KeyBinding.releaseAll(), which forces all inputs to be
considered released. However, our init() function then calls
grabMouse(), which calls Keybinding.setAll(), undoing this work.

The fix we're going for here is to call releaseAll() one more time[^1]
after grabbing the mouse. I think if this becomes any more of a problem,
we should roll our own grabMouse which _doesn't_ implement any specific
behaviour.

Fixes #975

[^1]: Obvious problem here is that we do minecraft.screen=xyz rather
      than setScreen. We need to - otherwise we'd just hit a stack
      overflow - but it's not great.
2021-12-01 20:09:38 +00:00
104 changed files with 6261 additions and 880 deletions

1
.gitattributes vendored
View File

@@ -13,3 +13,4 @@ src/testMod/server-files/structures linguist-generated
*.png binary
*.jar binary
*.dfpwm binary

View File

@@ -8,9 +8,9 @@ body:
label: Minecraft Version
description: What version of Minecraft are you using?
options:
- 1.15.x
- 1.16.x
- 1.17.x
- 1.18.x
validations:
required: true
- type: input

View File

@@ -6,6 +6,7 @@ buildscript {
}
dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.24'
classpath "org.spongepowered:mixingradle:0.7.+"
classpath 'org.parchmentmc:librarian:1.+'
}
}
@@ -22,6 +23,7 @@ plugins {
}
apply plugin: 'net.minecraftforge.gradle'
apply plugin: "org.spongepowered.mixin"
apply plugin: 'org.parchmentmc.librarian.forgegradle'
version = mod_version
@@ -46,9 +48,6 @@ tasks.withType(JavaExec).configureEach {
}
sourceSets {
main.java {
exclude 'dan200/computercraft/shared/integration/morered/**'
}
main.resources {
srcDir 'src/generated/resources'
}
@@ -71,6 +70,8 @@ minecraft {
source sourceSets.main
}
}
arg "-mixin.config=computercraft.mixins.json"
}
client {
@@ -127,6 +128,11 @@ minecraft {
accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
}
mixin {
add sourceSets.main, 'computercraft.mixins.refmap.json'
}
repositories {
mavenCentral()
maven {
@@ -150,9 +156,9 @@ dependencies {
checkstyle "com.puppycrawl.tools:checkstyle:8.45"
minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
annotationProcessor 'org.spongepowered:mixin:0.8.4:processor'
compileOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14:api")
// compileOnly fg.deobf("commoble.morered:morered-1.16.5:2.1.1.0")
runtimeOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14")
@@ -168,7 +174,7 @@ dependencies {
testModImplementation sourceSets.main.output
testModExtra 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21'
cctJavadoc 'cc.tweaked:cct-javadoc:1.4.2'
cctJavadoc 'cc.tweaked:cct-javadoc:1.4.5'
}
// Compile tasks
@@ -208,6 +214,8 @@ jar {
"Implementation-Version" : "${mod_version}",
"Implementation-Vendor" : "SquidDev",
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
,
"MixinConfigs" : "computercraft.mixins.json",
])
}
@@ -280,18 +288,7 @@ task rollup(type: Exec) {
commandLine mkCommand('"node_modules/.bin/rollup" --config rollup.config.js')
}
task minifyWeb(type: Exec, dependsOn: rollup) {
group = "build"
description = "Bundles JS into rollup"
inputs.file("$buildDir/rollup/index.js").withPropertyName("sources")
inputs.file("package-lock.json").withPropertyName("package-lock.json")
outputs.file("$buildDir/rollup/index.min.js").withPropertyName("output")
commandLine mkCommand('"node_modules/.bin/terser"' + " -o '$buildDir/rollup/index.min.js' '$buildDir/rollup/index.js'")
}
task illuaminateDocs(type: Exec, dependsOn: [minifyWeb, luaJavadoc]) {
task illuaminateDocs(type: Exec, dependsOn: [rollup, luaJavadoc]) {
group = "build"
description = "Bundles JS into rollup"
@@ -299,7 +296,7 @@ task illuaminateDocs(type: Exec, dependsOn: [minifyWeb, luaJavadoc]) {
inputs.files(fileTree("src/main/resources/data/computercraft/lua/rom")).withPropertyName("lua rom")
inputs.file("illuaminate.sexp").withPropertyName("illuaminate.sexp")
inputs.dir("$buildDir/docs/luaJavadoc")
inputs.file("$buildDir/rollup/index.min.js").withPropertyName("scripts")
inputs.file("$buildDir/rollup/index.js").withPropertyName("scripts")
inputs.file("src/web/styles.css").withPropertyName("styles")
outputs.dir("$buildDir/docs/lua")
@@ -307,9 +304,13 @@ task illuaminateDocs(type: Exec, dependsOn: [minifyWeb, luaJavadoc]) {
}
task docWebsite(type: Copy, dependsOn: [illuaminateDocs]) {
from 'doc'
include 'logo.png'
include 'images/**'
from('doc') {
include 'logo.png'
include 'images/**'
}
from("$buildDir/rollup") {
exclude 'index.js'
}
into "${project.docsDir}/lua"
}
@@ -477,7 +478,7 @@ task checkRelease {
}
check.dependsOn checkRelease
def isStable = false
def isStable = true
curseforge {
apiKey = project.hasProperty('curseForgeApiKey') ? project.curseForgeApiKey : ''

View File

@@ -51,5 +51,6 @@ exclude: |
src/generated|
src/test/resources/test-rom/data/json-parsing/|
src/testMod/server-files/|
config/idea/
config/idea/|
.*\.dfpwm
)

View File

@@ -2,7 +2,7 @@
module: [kind=event] modem_message
---
The @{modem_message} event is fired when a message is received on an open channel on any modem.
The @{modem_message} event is fired when a message is received on an open channel on any @{modem}.
## Return Values
1. @{string}: The event name.
@@ -10,11 +10,15 @@ The @{modem_message} event is fired when a message is received on an open channe
3. @{number}: The channel that the message was sent on.
4. @{number}: The reply channel set by the sender.
5. @{any}: The message as sent by the sender.
6. @{number}: The distance between the sender and the receiver, in blocks (decimal).
6. @{number}: The distance between the sender and the receiver, in blocks.
## Example
Prints a message when one is sent:
Wraps a @{modem} peripheral, opens channel 0 for listening, and prints all received messages.
```lua
local modem = peripheral.find("modem") or error("No modem attached", 0)
modem.open(0)
while true do
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
print(("Message received on side %s on channel %d (reply to %d) from %f blocks away with message %s"):format(side, channel, replyChannel, distance, tostring(message)))

View File

@@ -0,0 +1,27 @@
---
module: [kind=event] speaker_audio_empty
see: speaker.playAudio To play audio using the speaker
---
## Return Values
1. @{string}: The event name.
2. @{string}: The name of the speaker which is available to play more audio.
## Example
This uses @{io.lines} to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it
using @{speaker.playAudio}. If the speaker's buffer is full, it waits for an event and tries again.
```lua {data-peripheral=speaker}
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
local buffer = decoder(chunk)
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```

200
doc/guides/speaker_audio.md Normal file
View File

@@ -0,0 +1,200 @@
---
module: [kind=guide] speaker_audio
see: speaker.playAudio Play PCM audio using a speaker.
see: cc.audio.dfpwm Provides utilities for encoding and decoding DFPWM files.
---
# Playing audio with speakers
CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the @{speaker.playAudio}
method. However, for people unfamiliar with digital audio, it's not the most intuitive thing to use. This guide provides
an introduction to digital audio, demonstrates how to play music with CC: Tweaked's speakers, and then briefly discusses
the more complex topic of audio processing.
## A short introduction to digital audio
When sound is recorded it is captured as an analogue signal, effectively the electrical version of a sound
wave. However, this signal is continuous, and so can't be used directly by a computer. Instead, we measure (or *sample*)
the amplitude of the wave many times a second and then *quantise* that amplitude, rounding it to the nearest
representable value.
This representation of sound - a long, uniformally sampled list of amplitudes is referred to as [Pulse-code
Modulation][PCM] (PCM). PCM can be thought of as the "standard" audio format, as it's incredibly easy to work with. For
instance, to mix two pieces of audio together, you can just samples from the two tracks together and take the average.
CC: Tweaked's speakers also work with PCM audio. It plays back 48,000 samples a second, where each sample is an integer
between -128 and 127. This is more commonly referred to as 48kHz and an 8-bit resolution.
Let's now look at a quick example. We're going to generate a [Sine Wave] at 220Hz, which sounds like a low monotonous
hum. First we wrap our speaker peripheral, and then we fill a table (also referred to as a *buffer*) with 128×1024
samples - this is the maximum number of samples a speaker can accept in one go.
In order to fill this buffer, we need to do a little maths. We want to play 220 sine waves each second, where each sine
wave completes a full oscillation in 2π "units". This means one seconds worth of audio is 2×π×220 "units" long. We then
need to split this into 48k samples, basically meaning for each sample we move 2×π×220/48k "along" the sine curve.
```lua {data-peripheral=speaker}
local speaker = peripheral.find("speaker")
local buffer = {}
local t, dt = 0, 2 * math.pi * 220 / 48000
for i = 1, 128 * 1024 do
buffer[i] = math.floor(math.sin(t) * 127)
t = (t + dt) % (math.pi * 2)
end
speaker.playAudio(buffer)
```
## Streaming audio
You might notice that the above snippet only generates a short bit of audio - 2.7s seconds to be precise. While we could
try increasing the number of loop iterations, we'll get an error when we try to play it through the speaker: the sound
buffer is too large for it to handle.
Our 2.7 seconds of audio is stored in a table with over 130 _thousand_ elements. If we wanted to play a full minute of
sine waves (and why wouldn't you?), you'd need a table with almost 3 _million_. Suddenly you find these numbers adding
up very quickly, and these tables take up more and more memory.
Instead of building our entire song (well, sine wave) in one go, we can produce it in small batches, each of which get
passed off to @{speaker.playAudio} when the time is right. This allows us to build a _stream_ of audio, where we read
chunks of audio one at a time (either from a file or a tone generator like above), do some optional processing to each
one, and then play them.
Let's adapt our example from above to do that instead.
```lua {data-peripheral=speaker}
local speaker = peripheral.find("speaker")
local t, dt = 0, 2 * math.pi * 220 / 48000
while true do
local buffer = {}
for i = 1, 16 * 1024 * 8 do
buffer[i] = math.floor(math.sin(t) * 127)
t = (t + dt) % (math.pi * 2)
end
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```
It looks pretty similar to before, aside from we've wrapped the generation and playing code in a while loop, and added a
rather odd loop with @{speaker.playAudio} and @{os.pullEvent}.
Let's talk about this loop, why do we need to keep calling @{speaker.playAudio}? Remember that what we're trying to do
here is avoid keeping too much audio in memory at once. However, if we're generating audio quicker than the speakers can
play it, we're not helping at all - all this audio is still hanging around waiting to be played!
In order to avoid this, the speaker rejects any new chunks of audio if its backlog is too large. When this happens,
@{speaker.playAudio} returns false. Once enough audio has played, and the backlog has been reduced, a
@{speaker_audio_empty} event is queued, and we can try to play our chunk once more.
## Storing audio
PCM is a fantastic way of representing audio when we want to manipulate it, but it's not very efficient when we want to
store it to disk. Compare the size of a WAV file (which uses PCM) to an equivalent MP3, it's often 5 times the size.
Instead, we store audio in special formats (or *codecs*) and then convert them to PCM when we need to do processing on
them.
Modern audio codecs use some incredibly impressive techniques to compress the audio as much as possible while preserving
sound quality. However, due to CC: Tweaked's limited processing power, it's not really possible to use these from your
computer. Instead, we need something much simpler.
DFPWM (Dynamic Filter Pulse Width Modulation) is the de facto standard audio format of the ComputerCraft (and
OpenComputers) world. Originally popularised by the addon mod [Computronics], CC:T now has built-in support for it with
the @{cc.audio.dfpwm} module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them
using the speaker.
Let's dive in with an example, and we'll explain things afterwards:
```lua {data-peripheral=speaker}
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
local buffer = decoder(chunk)
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```
Once again, we see the @{speaker.playAudio}/@{speaker_audio_empty} loop. However, the rest of the program is a little
different.
First, we require the dfpwm module and call @{cc.audio.dfpwm.make_decoder} to construct a new decoder. This decoder
accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, which we can then play with our speaker.
As mentioned to above, @{speaker.playAudio} accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use
@{io.lines}, which provides a nice way to loop over chunks of a file. You can of course just use @{fs.open} and
@{fs.BinaryReadHandle.read} if you prefer.
## Processing audio
As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes.
You can mix together samples from different streams by adding their amplitudes, change the rate of playback by removing
samples, etc...
Let's put together a small demonstration here. We're going to add a small delay effect to the song above, so that you
hear a faint echo about a second later.
In order to do this, we'll follow a format similar to the previous example, decoding the audio and then playing it.
However, we'll also add some new logic between those two steps, which loops over every sample in our chunk of audio, and
adds the sample from one second ago to it.
For this, we'll need to keep track of the last 48k samples - exactly one seconds worth of audio. We can do this using a
[Ring Buffer], which helps makes things a little more efficient.
```lua {data-peripheral=speaker}
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
-- Speakers play at 48kHz, so one second is 48k samples. We first fill our buffer
-- with 0s, as there's nothing to echo at the start of the track!
local samples_i, samples_n = 1, 48000
local samples = {}
for i = 1, samples_n do samples[i] = 0 end
local decoder = dfpwm.make_decoder()
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
local buffer = decoder(chunk)
for i = 1, #buffer do
local original_value = buffer[i]
-- Replace this sample with its current amplitude plus the amplitude from one second ago.
-- We scale both to ensure the resulting value is still between -128 and 127.
buffer[i] = original_value * 0.6 + samples[samples_i] * 0.4
-- Now store the current sample, and move the "head" of our ring buffer forward one place.
samples[samples_i] = original_value
samples_i = samples_i + 1
if samples_i > samples_n then samples_i = 1 end
end
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```
:::note Confused?
Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't
cover. That said, don't be afraid to ask on [Discord] or [IRC] either!
:::
It's worth noting that the examples of audio processing we've mentioned here are about manipulating the _amplitude_ of
the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex.
For this, you'd need to use the [Fast Fourier transform][FFT] to convert the stream of amplitudes to frequencies,
process those, and then convert them back to amplitudes.
This is, I'm afraid, left as an exercise to the reader.
[Computronics]: https://github.com/Vexatos/Computronics/ "Computronics on GitHub"
[FFT]: https://en.wikipedia.org/wiki/Fast_Fourier_transform "Fast Fourier transform - Wikipedia"
[PCM]: https://en.wikipedia.org/wiki/Pulse-code_modulation "Pulse-code Modulation - Wikipedia"
[Ring Buffer]: https://en.wikipedia.org/wiki/Circular_buffer "Circular buffer - Wikipedia"
[Sine Wave]: https://en.wikipedia.org/wiki/Sine_wave "Sine wave - Wikipedia"
[Discord]: https://discord.computercraft.cc "The Minecraft Computer Mods Discord"
[IRC]: http://webchat.esper.net/?channels=computercraft "IRC webchat on EsperNet"

View File

@@ -0,0 +1,83 @@
---
module: [kind=guide] using_require
---
# Reusing code with require
A library is a collection of useful functions and other definitions which is stored separately to your main program. You
might want to create a library because you have some functions which are used in multiple programs, or just to split
your program into multiple more modular files.
Let's say we want to create a small library to make working with the @{term|terminal} a little easier. We'll provide two
functions: `reset`, which clears the terminal and sets the cursor to (1, 1), and `write_center`, which prints some text
in the middle of the screen.
Start off by creating a file called `more_term.lua`:
```lua {data-snippet=more_term}
local function reset()
term.clear()
term.setCursorPos(1, 1)
end
local function write_center(text)
local x, y = term.getCursorPos()
local width, height = term.getSize()
term.setCursorPos(math.floor((width - #text) / 2) + 1, y)
term.write(text)
end
return { reset = reset, write_center = write_center }
```
Now, what's going on here? We define our two functions as one might expect, and then at the bottom return a table with
the two functions. When we require this library, this table is what is returned. With that, we can then call the
original functions. Now create a new file, with the following:
```lua {data-mount=more_term:more_term.lua}
local more_term = require("more_term")
more_term.reset()
more_term.write_center("Hello, world!")
```
When run, this'll clear the screen and print some text in the middle of the first line.
## require in depth
While the previous section is a good introduction to how @{require} operates, there are a couple of remaining points
which are worth mentioning for more advanced usage.
### Libraries can return anything
In our above example, we return a table containing the functions we want to expose. However, it's worth pointing out
that you can return ''anything'' from your library - a table, a function or even just a string! @{require} treats them
all the same, and just returns whatever your library provides.
### Module resolution and the package path
In the above examples, we defined our library in a file, and @{require} read from it. While this is what you'll do most
of the time, it is possible to make @{require} look elsewhere for your library, such as downloading from a website or
loading from an in-memory library store.
As a result, the *module name* you pass to @{require} doesn't correspond to a file path. One common mistake is to load
code from a sub-directory using `require("folder/library")` or even `require("folder/library.lua")`, neither of which
will do quite what you expect.
When loading libraries (also referred to as *modules*) from files, @{require} searches along the *@{package.path|module
path}*. By default, this looks something like:
* `?.lua`
* `?/init.lua`
* `/rom/modules/main/?.lua`
* etc...
When you call `require("my_library")`, @{require} replaces the `?` in each element of the path with your module name, and
checks if the file exists. In this case, we'd look for `my_library.lua`, `my_library/init.lua`,
`/rom/modules/main/my_library.lua` and so on. Note that this works *relative to the current program*, so if your
program is actually called `folder/program`, then we'll look for `folder/my_library.lua`, etc...
One other caveat is loading libraries from sub-directories. For instance, say we have a file
`my/fancy/library.lua`. This can be loaded by using `require("my.fancy.library")` - the '.'s are replaced with '/'
before we start looking for the library.
## External links
There are several external resources which go into require in a little more detail:
- The [Lua Module tutorial](http://lua-users.org/wiki/ModulesTutorial) on the Lua wiki.
- [Lua's manual section on @{require}](https://www.lua.org/manual/5.1/manual.html#pdf-require).

View File

@@ -12,16 +12,19 @@ rounded up to the nearest multiple of 0.05 seconds. If you are using coroutines
or the @{parallel|parallel API}, it will only pause execution of the current
thread, not the whole program.
**Note** Because sleep internally uses timers, it is a function that yields.
This means that you can use it to prevent "Too long without yielding" errors,
however, as the minimum sleep time is 0.05 seconds, it will slow your program
down.
:::tip
Because sleep internally uses timers, it is a function that yields. This means
that you can use it to prevent "Too long without yielding" errors, however, as
the minimum sleep time is 0.05 seconds, it will slow your program down.
:::
**Warning** Internally, this function queues and waits for a timer event (using
:::caution
Internally, this function queues and waits for a timer event (using
@{os.startTimer}), however it does not listen for any other events. This means
that any event that occurs while sleeping will be entirely discarded. If you
need to receive events while sleeping, consider using @{os.startTimer|timers},
or the @{parallel|parallel API}.
:::
@tparam number time The number of seconds to sleep for, rounded up to the
nearest multiple of 0.05.

View File

@@ -1,7 +1,7 @@
org.gradle.jvmargs=-Xmx3G
# Mod properties
mod_version=1.99.0
mod_version=1.100.0
# Minecraft properties (update mods.toml when changing)
mc_version=1.17.1

View File

@@ -1,8 +1,9 @@
; -*- mode: Lisp;-*-
(sources
/doc/stub/
/doc/events/
/doc/guides/
/doc/stub/
/build/docs/luaJavadoc/
/src/main/resources/*/computercraft/lua/bios.lua
/src/main/resources/*/computercraft/lua/rom/
@@ -27,7 +28,8 @@
(module-kinds
(peripheral Peripherals)
(generic_peripheral "Generic Peripherals")
(event Events))
(event Events)
(guide Guides))
(library-path
/doc/stub/

86
package-lock.json generated
View File

@@ -14,6 +14,7 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "^8.2.5",
"@rollup/plugin-url": "^6.1.0",
"requirejs": "^2.3.6",
"rollup": "^2.33.1",
"rollup-plugin-terser": "^7.0.2",
@@ -73,6 +74,23 @@
"typescript": ">=3.7.0"
}
},
"node_modules/@rollup/plugin-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-url/-/plugin-url-6.1.0.tgz",
"integrity": "sha512-FJNWBnBB7nLzbcaGmu1no+U/LlRR67TtgfRFP+VEKSrWlDTE6n9jMns/N4Q/VL6l4x6kTHQX4HQfwTcldaAfHQ==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^3.1.0",
"make-dir": "^3.1.0",
"mime": "^2.4.6"
},
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0"
}
},
"node_modules/@rollup/pluginutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
@@ -264,12 +282,39 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -382,6 +427,15 @@
}
]
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
@@ -512,6 +566,17 @@
"resolve": "^1.17.0"
}
},
"@rollup/plugin-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-url/-/plugin-url-6.1.0.tgz",
"integrity": "sha512-FJNWBnBB7nLzbcaGmu1no+U/LlRR67TtgfRFP+VEKSrWlDTE6n9jMns/N4Q/VL6l4x6kTHQX4HQfwTcldaAfHQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"make-dir": "^3.1.0",
"mime": "^2.4.6"
}
},
"@rollup/pluginutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
@@ -665,12 +730,27 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"requires": {
"semver": "^6.0.0"
}
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -740,6 +820,12 @@
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",

View File

@@ -10,6 +10,7 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "^8.2.5",
"@rollup/plugin-url": "^6.1.0",
"requirejs": "^2.3.6",
"rollup": "^2.33.1",
"rollup-plugin-terser": "^7.0.2",

View File

@@ -1,7 +1,8 @@
import { readFileSync } from "fs";
import { readFileSync } from "fs";
import path from "path";
import typescript from "@rollup/plugin-typescript";
import url from '@rollup/plugin-url';
import { terser } from "rollup-plugin-terser";
const input = "src/web";
@@ -10,7 +11,7 @@ const requirejs = readFileSync("node_modules/requirejs/require.js");
export default {
input: [`${input}/index.tsx`],
output: {
file: "build/rollup/index.js",
dir: "build/rollup/",
// We bundle requirejs (and config) into the header. It's rather gross
// but also works reasonably well.
// Also suffix a ?v=${date} onto the end in the event we need to require a specific copy-cat version.
@@ -18,7 +19,7 @@ export default {
${requirejs}
require.config({
paths: { copycat: "https://copy-cat.squiddev.cc" },
urlArgs: function(id) { return id == "copycat/embed" ? "?v=20211127" : ""; }
urlArgs: function(id) { return id == "copycat/embed" ? "?v=20211221" : ""; }
});
`,
format: "amd",
@@ -33,12 +34,18 @@ export default {
plugins: [
typescript(),
url({
include: "**/*.dfpwm",
fileName: "[name]-[hash][extname]",
publicPath: "/",
}),
{
name: "cc-tweaked",
async transform(code, file) {
// Allow loading files in /mount.
const ext = path.extname(file);
return ext != '.tsx' && ext != '.ts' && path.dirname(file) === path.resolve(`${input}/mount`)
return ext != '.dfpwm' && path.dirname(file) === path.resolve(`${input}/mount`)
? `export default ${JSON.stringify(code)};\n`
: null;
},

View File

@@ -183,6 +183,24 @@ public interface IArguments
return (Map<?, ?>) value;
}
/**
* Get an argument as a table in an unsafe manner.
*
* Classes implementing this interface may choose to implement a more optimised version which does not copy the
* table, instead returning a wrapper version, making it more efficient. However, the caller must guarantee that
* they do not access off the computer thread (and so should not be used with main-thread functions) or once the
* function call has finished (for instance, in callbacks).
*
* @param index The argument number.
* @return The argument's value.
* @throws LuaException If the value is not a table.
*/
@Nonnull
default LuaTable<?, ?> getTableUnsafe( int index ) throws LuaException
{
return new ObjectLuaTable( getTable( index ) );
}
/**
* Get an argument as a double.
*
@@ -314,6 +332,27 @@ public interface IArguments
return Optional.of( (Map<?, ?>) value );
}
/**
* Get an argument as a table in an unsafe manner.
*
* Classes implementing this interface may choose to implement a more optimised version which does not copy the
* table, instead returning a wrapper version, making it more efficient. However, the caller must guarantee that
* they do not access off the computer thread (and so should not be used with main-thread functions) or once the
* function call has finished (for instance, in callbacks).
*
* @param index The argument number.
* @return The argument's value, or {@link Optional#empty()} if not present.
* @throws LuaException If the value is not a table.
*/
@Nonnull
default Optional<LuaTable<?, ?>> optTableUnsafe( int index ) throws LuaException
{
Object value = get( index );
if( value == null ) return Optional.empty();
if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "map", value );
return Optional.of( new ObjectLuaTable( (Map<?, ?>) value ) );
}
/**
* Get an argument as a double.
*
@@ -404,4 +443,13 @@ public interface IArguments
{
return optTable( index ).orElse( def );
}
/**
* This is called when the current function finishes, before any main thread tasks have run.
*
* Called when the current function returns, and so some values are no longer guaranteed to be safe to access.
*/
default void releaseImmediate()
{
}
}

View File

@@ -51,8 +51,17 @@ public @interface LuaFunction
* Run this function on the main server thread. This should be specified for any method which interacts with
* Minecraft in a thread-unsafe manner.
*
* @return Whether this functi
* @return Whether this function should be run on the main thread.
* @see ILuaContext#issueMainThreadTask(ILuaTask)
*/
boolean mainThread() default false;
/**
* Allow using "unsafe" arguments, such {@link IArguments#getTableUnsafe(int)}.
*
* This is incompatible with {@link #mainThread()}.
*
* @return Whether this function supports unsafe arguments.
*/
boolean unsafe() default false;
}

View File

@@ -0,0 +1,113 @@
/*
* This file is part of the public ComputerCraft API - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only.
* For help using the API, and posting your mods, visit the forums at computercraft.info.
*/
package dan200.computercraft.api.lua;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Map;
import static dan200.computercraft.api.lua.LuaValues.*;
public interface LuaTable<K, V> extends Map<K, V>
{
/**
* Compute the length of the array part of this table.
*
* @return This table's length.
*/
default int length()
{
int size = 0;
while( containsKey( (double) (size + 1) ) ) size++;
return size;
}
/**
* Get an array entry as an integer.
*
* @param index The index in the table, starting at 1.
* @return The table's value.
* @throws LuaException If the value is not an integer.
*/
default long getLong( int index ) throws LuaException
{
Object value = get( (double) index );
if( !(value instanceof Number) ) throw badTableItem( index, "number", getType( value ) );
Number number = (Number) value;
double asDouble = number.doubleValue();
if( !Double.isFinite( asDouble ) ) throw badTableItem( index, "number", getNumericType( asDouble ) );
return number.longValue();
}
/**
* Get a table entry as an integer.
*
* @param key The name of the field in the table.
* @return The table's value.
* @throws LuaException If the value is not an integer.
*/
default long getLong( String key ) throws LuaException
{
Object value = get( key );
if( !(value instanceof Number) ) throw badField( key, "number", getType( value ) );
Number number = (Number) value;
double asDouble = number.doubleValue();
if( !Double.isFinite( asDouble ) ) throw badField( key, "number", getNumericType( asDouble ) );
return number.longValue();
}
/**
* Get an array entry as an integer.
*
* @param index The index in the table, starting at 1.
* @return The table's value.
* @throws LuaException If the value is not an integer.
*/
default int getInt( int index ) throws LuaException
{
return (int) getLong( index );
}
/**
* Get a table entry as an integer.
*
* @param key The name of the field in the table.
* @return The table's value.
* @throws LuaException If the value is not an integer.
*/
default int getInt( String key ) throws LuaException
{
return (int) getLong( key );
}
@Nullable
@Override
default V put( K o, V o2 )
{
throw new UnsupportedOperationException( "Cannot modify LuaTable" );
}
@Override
default V remove( Object o )
{
throw new UnsupportedOperationException( "Cannot modify LuaTable" );
}
@Override
default void putAll( @Nonnull Map<? extends K, ? extends V> map )
{
throw new UnsupportedOperationException( "Cannot modify LuaTable" );
}
@Override
default void clear()
{
throw new UnsupportedOperationException( "Cannot modify LuaTable" );
}
}

View File

@@ -102,6 +102,34 @@ public final class LuaValues
return new LuaException( "bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")" );
}
/**
* Construct a table item exception, from an expected and actual type.
*
* @param index The index into the table, starting from 1.
* @param expected The expected type for this table item.
* @param actual The provided type for this table item.
* @return The constructed exception, which should be thrown immediately.
*/
@Nonnull
public static LuaException badTableItem( int index, @Nonnull String expected, @Nonnull String actual )
{
return new LuaException( "table item #" + index + " is not " + expected + " (got " + actual + ")" );
}
/**
* Construct a field exception, from an expected and actual type.
*
* @param key The name of the field.
* @param expected The expected type for this table item.
* @param actual The provided type for this table item.
* @return The constructed exception, which should be thrown immediately.
*/
@Nonnull
public static LuaException badField( String key, @Nonnull String expected, @Nonnull String actual )
{
return new LuaException( "field " + key + " is not " + expected + " (got " + actual + ")" );
}
/**
* Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}.
*

View File

@@ -98,7 +98,10 @@ public final class MethodResult
{
Objects.requireNonNull( callback, "callback cannot be null" );
return new MethodResult( new Object[] { filter }, results -> {
if( results.length >= 1 && results[0].equals( "terminate" ) ) throw new LuaException( "Terminated", 0 );
if( results.length >= 1 && Objects.equals( results[0], "terminate" ) )
{
throw new LuaException( "Terminated", 0 );
}
return callback.resume( results );
} );
}

View File

@@ -5,10 +5,12 @@
*/
package dan200.computercraft.api.lua;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* An implementation of {@link IArguments} which wraps an array of {@link Object}.
@@ -16,6 +18,8 @@ import java.util.Objects;
public final class ObjectArguments implements IArguments
{
private static final IArguments EMPTY = new ObjectArguments();
private boolean released = false;
private final List<Object> args;
@Deprecated
@@ -63,4 +67,34 @@ public final class ObjectArguments implements IArguments
{
return args.toArray();
}
@Nonnull
@Override
public LuaTable<?, ?> getTableUnsafe( int index ) throws LuaException
{
if( released )
{
throw new IllegalStateException( "Cannot use getTableUnsafe after IArguments has been released" );
}
return IArguments.super.getTableUnsafe( index );
}
@Nonnull
@Override
public Optional<LuaTable<?, ?>> optTableUnsafe( int index ) throws LuaException
{
if( released )
{
throw new IllegalStateException( "Cannot use optTableUnsafe after IArguments has been released" );
}
return IArguments.super.optTableUnsafe( index );
}
@Override
public void releaseImmediate()
{
released = true;
}
}

View File

@@ -0,0 +1,73 @@
/*
* This file is part of the public ComputerCraft API - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only.
* For help using the API, and posting your mods, visit the forums at computercraft.info.
*/
package dan200.computercraft.api.lua;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
public class ObjectLuaTable implements LuaTable<Object, Object>
{
private final Map<Object, Object> map;
public ObjectLuaTable( Map<?, ?> map )
{
this.map = Collections.unmodifiableMap( map );
}
@Override
public int size()
{
return map.size();
}
@Override
public boolean isEmpty()
{
return map.isEmpty();
}
@Override
public boolean containsKey( Object o )
{
return map.containsKey( o );
}
@Override
public boolean containsValue( Object o )
{
return map.containsKey( o );
}
@Override
public Object get( Object o )
{
return map.get( o );
}
@Nonnull
@Override
public Set<Object> keySet()
{
return map.keySet();
}
@Nonnull
@Override
public Collection<Object> values()
{
return map.values();
}
@Nonnull
@Override
public Set<Entry<Object, Object>> entrySet()
{
return map.entrySet();
}
}

View File

@@ -23,7 +23,7 @@ public interface GenericPeripheral extends GenericSource
* Get the type of the exposed peripheral.
*
* Unlike normal {@link IPeripheral}s, {@link GenericPeripheral} do not have to have a type. By default, the
* resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@literal minecraft:chest}).
* resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@code minecraft:chest}).
*
* However, in some cases it may be more appropriate to specify a more readable name. Overriding this method allows
* you to do so.

View File

@@ -63,7 +63,7 @@ public final class PeripheralType
* Create a new non-empty peripheral type with additional traits.
*
* @param type The name of the type.
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
* @return The constructed peripheral type.
*/
public static PeripheralType ofType( @Nonnull String type, Collection<String> additionalTypes )
@@ -76,7 +76,7 @@ public final class PeripheralType
* Create a new non-empty peripheral type with additional traits.
*
* @param type The name of the type.
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
* @return The constructed peripheral type.
*/
public static PeripheralType ofType( @Nonnull String type, @Nonnull String... additionalTypes )
@@ -88,7 +88,7 @@ public final class PeripheralType
/**
* Create a new peripheral type with no primary type but additional traits.
*
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
* @return The constructed peripheral type.
*/
public static PeripheralType ofAdditional( Collection<String> additionalTypes )
@@ -99,7 +99,7 @@ public final class PeripheralType
/**
* Create a new peripheral type with no primary type but additional traits.
*
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
* @return The constructed peripheral type.
*/
public static PeripheralType ofAdditional( @Nonnull String... additionalTypes )
@@ -108,7 +108,7 @@ public final class PeripheralType
}
/**
* Get the name of this peripheral type. This may be {@literal null}.
* Get the name of this peripheral type. This may be {@code null}.
*
* @return The type of this peripheral.
*/

View File

@@ -6,6 +6,7 @@
package dan200.computercraft.client;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
@@ -22,7 +23,7 @@ public class ClientHooks
if( event.getWorld().isClientSide() )
{
ClientMonitor.destroyAll();
SoundManager.reset();
SpeakerManager.reset();
}
}

View File

@@ -1,84 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.client.resources.sounds.TickableSoundInstance;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.phys.Vec3;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class SoundManager
{
private static final Map<UUID, MoveableSound> sounds = new HashMap<>();
public static void playSound( UUID source, Vec3 position, ResourceLocation event, float volume, float pitch )
{
var soundManager = Minecraft.getInstance().getSoundManager();
MoveableSound oldSound = sounds.get( source );
if( oldSound != null ) soundManager.stop( oldSound );
MoveableSound newSound = new MoveableSound( event, position, volume, pitch );
sounds.put( source, newSound );
soundManager.play( newSound );
}
public static void stopSound( UUID source )
{
SoundInstance sound = sounds.remove( source );
if( sound == null ) return;
Minecraft.getInstance().getSoundManager().stop( sound );
}
public static void moveSound( UUID source, Vec3 position )
{
MoveableSound sound = sounds.get( source );
if( sound != null ) sound.setPosition( position );
}
public static void reset()
{
sounds.clear();
}
private static class MoveableSound extends AbstractSoundInstance implements TickableSoundInstance
{
protected MoveableSound( ResourceLocation sound, Vec3 position, float volume, float pitch )
{
super( sound, SoundSource.RECORDS );
setPosition( position );
this.volume = volume;
this.pitch = pitch;
attenuation = Attenuation.LINEAR;
}
void setPosition( Vec3 position )
{
x = (float) position.x();
y = (float) position.y();
z = (float) position.z();
}
@Override
public boolean isStopped()
{
return false;
}
@Override
public void tick()
{
}
}
}

View File

@@ -8,6 +8,7 @@ package dan200.computercraft.client.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
import dan200.computercraft.client.gui.widgets.WidgetTerminal;
import dan200.computercraft.shared.computer.core.ClientComputer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -102,6 +103,16 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
renderTooltip( stack, mouseX, mouseY );
}
@Override
public boolean mouseClicked( double x, double y, int button )
{
boolean changed = super.mouseClicked( x, y, button );
// Clicking the terminate/shutdown button steals focus, which means then pressing "enter" will click the button
// again. Restore the focus to the terminal in these cases.
if( getFocused() instanceof DynamicImageButton ) setFocused( terminal );
return changed;
}
@Override
public final boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY )
{
@@ -162,7 +173,6 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
return;
}
buffer.rewind();
toUpload.add( new FileUpload( name, buffer, digest ) );
}
catch( IOException e )

View File

@@ -10,6 +10,7 @@ import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.widgets.WidgetTerminal;
import dan200.computercraft.shared.computer.core.ClientComputer;
import dan200.computercraft.shared.computer.inventory.ContainerComputerBase;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess;
@@ -44,8 +45,11 @@ public class NoTermComputerScreen<T extends ContainerComputerBase> extends Scree
protected void init()
{
passEvents = true; // Pass mouse vents through to the game's mouse handler.
// 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;
KeyMapping.releaseAll();
super.init();
minecraft.keyboardHandler.setSendRepeatsToGui( true );

View File

@@ -0,0 +1,133 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.sound;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.sounds.AudioStream;
import org.lwjgl.BufferUtils;
import javax.annotation.Nonnull;
import javax.sound.sampled.AudioFormat;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.Queue;
class DfpwmStream implements AudioStream
{
public static final int SAMPLE_RATE = SpeakerPeripheral.SAMPLE_RATE;
private static final int PREC = 10;
private static final int LPF_STRENGTH = 140;
private static final AudioFormat MONO_16 = new AudioFormat( SAMPLE_RATE, 16, 1, true, false );
private final Queue<ByteBuffer> buffers = new ArrayDeque<>( 2 );
private int charge = 0; // q
private int strength = 0; // s
private int lowPassCharge;
private boolean previousBit = false;
DfpwmStream()
{
}
void push( @Nonnull ByteBuf input )
{
int readable = input.readableBytes();
ByteBuffer output = ByteBuffer.allocate( readable * 16 ).order( ByteOrder.nativeOrder() );
for( int i = 0; i < readable; i++ )
{
byte inputByte = input.readByte();
for( int j = 0; j < 8; j++ )
{
boolean currentBit = (inputByte & 1) != 0;
int target = currentBit ? 127 : -128;
// q' <- q + (s * (t - q) + 128)/256
int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1;
int z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
int nextStrength = strength;
if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1;
if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8);
// Apply antijerk
int chargeWithAntijerk = currentBit == previousBit
? nextCharge
: nextCharge + charge + 1 >> 1;
// And low pass filter: outQ <- outQ + ((expectedOutput - outQ) x 140 / 256)
lowPassCharge += ((chargeWithAntijerk - lowPassCharge) * LPF_STRENGTH + 0x80) >> 8;
charge = nextCharge;
strength = nextStrength;
previousBit = currentBit;
// Ideally we'd generate an 8-bit audio buffer. However, as we're piggybacking on top of another
// audio stream (which uses 16 bit audio), we need to keep in the same format.
output.putShort( (short) ((byte) (lowPassCharge & 0xFF) << 8) );
inputByte >>= 1;
}
}
output.flip();
synchronized( this )
{
buffers.add( output );
}
}
@Nonnull
@Override
public AudioFormat getFormat()
{
return MONO_16;
}
@Nonnull
@Override
public synchronized ByteBuffer read( int capacity )
{
ByteBuffer result = BufferUtils.createByteBuffer( capacity );
while( result.hasRemaining() )
{
ByteBuffer head = buffers.peek();
if( head == null ) break;
int toRead = Math.min( head.remaining(), result.remaining() );
result.put( result.position(), head, head.position(), toRead );
result.position( result.position() + toRead );
head.position( head.position() + toRead );
if( head.hasRemaining() ) break;
buffers.remove();
}
result.flip();
// This is naughty, but ensures we're not enqueuing empty buffers when the stream is exhausted.
return result.remaining() == 0 ? null : result;
}
@Override
public void close() throws IOException
{
buffers.clear();
}
public boolean isEmpty()
{
return buffers.isEmpty();
}
}

View File

@@ -0,0 +1,92 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.sound;
import dan200.computercraft.ComputerCraft;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.Vec3;
/**
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
*/
public class SpeakerInstance
{
public static final ResourceLocation DFPWM_STREAM = new ResourceLocation( ComputerCraft.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played" );
private DfpwmStream currentStream;
private SpeakerSound sound;
SpeakerInstance()
{
}
public synchronized void pushAudio( ByteBuf buffer )
{
SpeakerSound sound = this.sound;
DfpwmStream stream = currentStream;
if( stream == null ) stream = currentStream = new DfpwmStream();
boolean exhausted = stream.isEmpty();
currentStream.push( buffer );
// If we've got nothing left in the buffer, enqueue an additional one just in case.
if( exhausted && sound != null && sound.stream == stream && sound.channel != null )
{
sound.executor.execute( () -> {
if( !sound.channel.stopped() ) sound.channel.pumpBuffers( 1 );
} );
}
}
public void playAudio( Vec3 position, float volume )
{
var soundManager = Minecraft.getInstance().getSoundManager();
if( sound != null && sound.stream != currentStream )
{
soundManager.stop( sound );
sound = null;
}
if( sound != null && !soundManager.isActive( sound ) ) sound = null;
if( sound == null && currentStream != null )
{
sound = new SpeakerSound( DFPWM_STREAM, currentStream, position, volume, 1.0f );
soundManager.play( sound );
}
}
public void playSound( Vec3 position, ResourceLocation location, float volume, float pitch )
{
var soundManager = Minecraft.getInstance().getSoundManager();
currentStream = null;
if( sound != null )
{
soundManager.stop( sound );
sound = null;
}
sound = new SpeakerSound( location, null, position, volume, pitch );
soundManager.play( sound );
}
void setPosition( Vec3 position )
{
if( sound != null ) sound.setPosition( position );
}
void stop()
{
if( sound != null ) Minecraft.getInstance().getSoundManager().stop( sound );
currentStream = null;
sound = null;
}
}

View File

@@ -0,0 +1,60 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.sound;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.sound.PlayStreamingSourceEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Maps speakers source IDs to a {@link SpeakerInstance}.
*/
@Mod.EventBusSubscriber( Dist.CLIENT )
public class SpeakerManager
{
private static final Map<UUID, SpeakerInstance> sounds = new ConcurrentHashMap<>();
@SubscribeEvent
public static void playStreaming( PlayStreamingSourceEvent event )
{
if( !(event.getSound() instanceof SpeakerSound sound) ) return;
if( sound.stream == null ) return;
event.getSource().attachBufferStream( sound.stream );
event.getSource().play();
sound.channel = event.getSource();
sound.executor = event.getManager().executor;
}
public static SpeakerInstance getSound( UUID source )
{
return sounds.computeIfAbsent( source, x -> new SpeakerInstance() );
}
public static void stopSound( UUID source )
{
SpeakerInstance sound = sounds.remove( source );
if( sound != null ) sound.stop();
}
public static void moveSound( UUID source, Vec3 position )
{
SpeakerInstance sound = sounds.get( source );
if( sound != null ) sound.setPosition( position );
}
public static void reset()
{
sounds.clear();
}
}

View File

@@ -0,0 +1,58 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.sound;
import com.mojang.blaze3d.audio.Channel;
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
import net.minecraft.client.resources.sounds.TickableSoundInstance;
import net.minecraft.client.sounds.AudioStream;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.concurrent.Executor;
public class SpeakerSound extends AbstractSoundInstance implements TickableSoundInstance
{
Channel channel;
Executor executor;
DfpwmStream stream;
SpeakerSound( ResourceLocation sound, DfpwmStream stream, Vec3 position, float volume, float pitch )
{
super( sound, SoundSource.RECORDS );
setPosition( position );
this.stream = stream;
this.volume = volume;
this.pitch = pitch;
attenuation = Attenuation.LINEAR;
}
void setPosition( Vec3 position )
{
x = (float) position.x();
y = (float) position.y();
z = (float) position.z();
}
@Override
public boolean isStopped()
{
return false;
}
@Override
public void tick()
{
}
@Nullable
public AudioStream getStream()
{
return stream;
}
}

View File

@@ -30,7 +30,36 @@ import java.util.OptionalLong;
import java.util.function.Function;
/**
* The FS API allows you to manipulate files and the filesystem.
* The FS API provides access to the computer's files and filesystem, allowing you to manipulate files, directories and
* paths. This includes:
*
* <ul>
* <li>**Reading and writing files:** Call {@link #open} to obtain a file "handle", which can be used to read from or
* write to a file.</li>
* <li>**Path manipulation:** {@link #combine}, {@link #getName} and {@link #getDir} allow you to manipulate file
* paths, joining them together or extracting components.</li>
* <li>**Querying paths:** For instance, checking if a file exists, or whether it's a directory. See {@link #getSize},
* {@link #exists}, {@link #isDir}, {@link #isReadOnly} and {@link #attributes}.</li>
* <li>**File and directory manipulation:** For instance, moving or copying files. See {@link #makeDir}, {@link #move},
* {@link #copy} and {@link #delete}.</li>
* </ul>
*
* :::note
* All functions in the API work on absolute paths, and do not take the @{shell.dir|current directory} into account.
* You can use @{shell.resolve} to convert a relative path into an absolute one.
* :::
*
* ## Mounts
* While a computer can only have one hard drive and filesystem, other filesystems may be "mounted" inside it. For
* instance, the {@link dan200.computercraft.shared.peripheral.diskdrive.DiskDrivePeripheral drive peripheral} mounts
* its disk's contents at {@code "disk/"}, {@code "disk1/"}, etc...
*
* You can see which mount a path belongs to with the {@link #getDrive} function. This returns {@code "hdd"} for the
* computer's main filesystem ({@code "/"}), {@code "rom"} for the rom ({@code "rom/"}).
*
* Most filesystems have a limited capacity, operations which would cause that capacity to be reached (such as writing
* an incredibly large file) will fail. You can see a mount's capacity with {@link #getCapacity} and the remaining
* space with {@link #getFreeSpace}.
*
* @cc.module fs
*/
@@ -440,6 +469,12 @@ public class FSAPI implements ILuaAPI
* @return The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files.
* @throws LuaException If the path doesn't exist.
* @cc.treturn string The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files.
* @cc.usage Print the drives of a couple of mounts:
*
* <pre>{@code
* print("/: " .. fs.getDrive("/"))
* print("/rom/: " .. fs.getDrive("rom"))
* }</pre>
*/
@LuaFunction
public final Object[] getDrive( String path ) throws LuaException

View File

@@ -22,7 +22,7 @@ import dan200.computercraft.core.computer.ComputerSide;
* as those from Project:Red. These allow you to send 16 separate on/off signals. Each channel corresponds to a
* colour, with the first being @{colors.white} and the last @{colors.black}.
*
* Whenever a redstone input changes, a {@code redstone} event will be fired. This may be used instead of repeativly
* Whenever a redstone input changes, a @{event!redstone} event will be fired. This may be used instead of repeativly
* polling.
*
* This module may also be referred to as {@code rs}. For example, one may call {@code rs.getSides()} instead of

View File

@@ -15,7 +15,7 @@ import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketCl
import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE;
/**
* An alternative to {@link WebSocketClientCompressionHandler} which supports the {@literal client_no_context_takeover}
* An alternative to {@link WebSocketClientCompressionHandler} which supports the {@code client_no_context_takeover}
* extension. Makes CC <em>slightly</em> more flexible.
*/
@ChannelHandler.Sharable

View File

@@ -130,8 +130,6 @@ public final class Generator<T>
private void addMethod( List<NamedMethod<T>> methods, Method method, LuaFunction annotation, PeripheralType genericType, T instance )
{
if( annotation.mainThread() ) instance = wrap.apply( instance );
String[] names = annotation.value();
boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
if( names.length == 0 )
@@ -183,6 +181,13 @@ public final class Generator<T>
}
}
LuaFunction annotation = method.getAnnotation( LuaFunction.class );
if( annotation.unsafe() && annotation.mainThread() )
{
ComputerCraft.log.error( "Lua Method {} cannot use unsafe and mainThread", name );
return Optional.empty();
}
// We have some rather ugly handling of static methods in both here and the main generate function. Static methods
// only come from generic sources, so this should be safe.
Class<?> target = Modifier.isStatic( modifiers ) ? method.getParameterTypes()[0] : method.getDeclaringClass();
@@ -190,11 +195,13 @@ public final class Generator<T>
try
{
String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement();
byte[] bytes = generate( className, target, method );
byte[] bytes = generate( className, target, method, annotation.unsafe() );
if( bytes == null ) return Optional.empty();
Class<?> klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() );
return Optional.of( klass.asSubclass( base ).getDeclaredConstructor().newInstance() );
T instance = klass.asSubclass( base ).getDeclaredConstructor().newInstance();
return Optional.of( annotation.mainThread() ? wrap.apply( instance ) : instance );
}
catch( ReflectiveOperationException | ClassFormatError | RuntimeException e )
{
@@ -205,7 +212,7 @@ public final class Generator<T>
}
@Nullable
private byte[] generate( String className, Class<?> target, Method method )
private byte[] generate( String className, Class<?> target, Method method, boolean unsafe )
{
String internalName = className.replace( ".", "/" );
@@ -238,7 +245,7 @@ public final class Generator<T>
int argIndex = 0;
for( java.lang.reflect.Type genericArg : method.getGenericParameterTypes() )
{
Boolean loadedArg = loadArg( mw, target, method, genericArg, argIndex );
Boolean loadedArg = loadArg( mw, target, method, unsafe, genericArg, argIndex );
if( loadedArg == null ) return null;
if( loadedArg ) argIndex++;
}
@@ -285,7 +292,7 @@ public final class Generator<T>
return cw.toByteArray();
}
private Boolean loadArg( MethodVisitor mw, Class<?> target, Method method, java.lang.reflect.Type genericArg, int argIndex )
private Boolean loadArg( MethodVisitor mw, Class<?> target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex )
{
if( genericArg == target )
{
@@ -324,7 +331,7 @@ public final class Generator<T>
return true;
}
String name = Reflect.getLuaName( Primitives.unwrap( klass ) );
String name = Reflect.getLuaName( Primitives.unwrap( klass ), unsafe );
if( name != null )
{
mw.visitVarInsn( ALOAD, 2 + context.size() );
@@ -344,7 +351,7 @@ public final class Generator<T>
return true;
}
String name = arg == Object.class ? "" : Reflect.getLuaName( arg );
String name = arg == Object.class ? "" : Reflect.getLuaName( arg, unsafe );
if( name != null )
{
if( Reflect.getRawType( method, genericArg, false ) == null ) return null;

View File

@@ -6,6 +6,7 @@
package dan200.computercraft.core.asm;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.LuaTable;
import org.objectweb.asm.MethodVisitor;
import javax.annotation.Nullable;
@@ -25,7 +26,7 @@ final class Reflect
}
@Nullable
static String getLuaName( Class<?> klass )
static String getLuaName( Class<?> klass, boolean unsafe )
{
if( klass.isPrimitive() )
{
@@ -39,6 +40,7 @@ final class Reflect
if( klass == Map.class ) return "Table";
if( klass == String.class ) return "String";
if( klass == ByteBuffer.class ) return "Bytes";
if( klass == LuaTable.class && unsafe ) return "TableUnsafe";
}
return null;

View File

@@ -59,6 +59,10 @@ class BasicFunction extends VarArgFunction
}
throw new LuaError( "Java Exception Thrown: " + t, 0 );
}
finally
{
arguments.releaseImmediate();
}
if( results.getCallback() != null )
{

View File

@@ -15,6 +15,7 @@ import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.shared.util.ThreadUtils;
import org.squiddev.cobalt.*;
import org.squiddev.cobalt.LuaTable;
import org.squiddev.cobalt.compiler.CompileException;
import org.squiddev.cobalt.compiler.LoadState;
import org.squiddev.cobalt.debug.DebugFrame;

View File

@@ -69,6 +69,10 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
}
throw new LuaError( "Java Exception Thrown: " + t, 0 );
}
finally
{
arguments.releaseImmediate();
}
ILuaCallback callback = results.getCallback();
Varargs ret = machine.toValues( results.getResult() );

View File

@@ -0,0 +1,141 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaValues;
import org.squiddev.cobalt.*;
import javax.annotation.Nonnull;
import java.util.*;
import static dan200.computercraft.api.lua.LuaValues.badTableItem;
import static dan200.computercraft.api.lua.LuaValues.getNumericType;
class TableImpl implements dan200.computercraft.api.lua.LuaTable<Object, Object>
{
private final VarargArguments arguments;
private final LuaTable table;
private Map<Object, Object> backingMap;
TableImpl( VarargArguments arguments, LuaTable table )
{
this.arguments = arguments;
this.table = table;
}
@Override
public int size()
{
checkValid();
try
{
return table.keyCount();
}
catch( LuaError e )
{
throw new IllegalStateException( e );
}
}
@Override
public int length()
{
return table.length();
}
@Override
public long getLong( int index ) throws LuaException
{
LuaValue value = table.rawget( index );
if( !(value instanceof LuaNumber) ) throw LuaValues.badTableItem( index, "number", value.typeName() );
if( value instanceof LuaInteger ) return value.toInteger();
double number = value.toDouble();
if( !Double.isFinite( number ) ) throw badTableItem( index, "number", getNumericType( number ) );
return (long) number;
}
@Override
public boolean isEmpty()
{
checkValid();
try
{
return table.next( Constants.NIL ).first().isNil();
}
catch( LuaError e )
{
throw new IllegalStateException( e );
}
}
@Nonnull
private LuaValue getImpl( Object o )
{
checkValid();
if( o instanceof String ) return table.rawget( (String) o );
if( o instanceof Integer ) return table.rawget( (Integer) o );
return Constants.NIL;
}
@Override
public boolean containsKey( Object o )
{
return !getImpl( o ).isNil();
}
@Override
public Object get( Object o )
{
return CobaltLuaMachine.toObject( getImpl( o ), null );
}
@Nonnull
private Map<Object, Object> getBackingMap()
{
checkValid();
if( backingMap != null ) return backingMap;
return backingMap = Collections.unmodifiableMap(
Objects.requireNonNull( (Map<?, ?>) CobaltLuaMachine.toObject( table, null ) )
);
}
@Override
public boolean containsValue( Object o )
{
return getBackingMap().containsKey( o );
}
@Nonnull
@Override
public Set<Object> keySet()
{
return getBackingMap().keySet();
}
@Nonnull
@Override
public Collection<Object> values()
{
return getBackingMap().values();
}
@Nonnull
@Override
public Set<Entry<Object, Object>> entrySet()
{
return getBackingMap().entrySet();
}
private void checkValid()
{
if( arguments.released )
{
throw new IllegalStateException( "Cannot use LuaTable after IArguments has been released" );
}
}
}

View File

@@ -19,6 +19,7 @@ class VarargArguments implements IArguments
{
static final IArguments EMPTY = new VarargArguments( Constants.NONE );
boolean released;
private final Varargs varargs;
private Object[] cache;
@@ -98,4 +99,39 @@ class VarargArguments implements IArguments
LuaString str = ((LuaBaseString) value).strvalue();
return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer() );
}
@Nonnull
@Override
public dan200.computercraft.api.lua.LuaTable<?, ?> getTableUnsafe( int index ) throws LuaException
{
if( released )
{
throw new IllegalStateException( "Cannot use getTableUnsafe after IArguments has been released" );
}
LuaValue value = varargs.arg( index + 1 );
if( !(value instanceof LuaTable) ) throw LuaValues.badArgument( index, "table", value.typeName() );
return new TableImpl( this, (LuaTable) value );
}
@Nonnull
@Override
public Optional<dan200.computercraft.api.lua.LuaTable<?, ?>> optTableUnsafe( int index ) throws LuaException
{
if( released )
{
throw new IllegalStateException( "Cannot use optTableUnsafe after IArguments has been released" );
}
LuaValue value = varargs.arg( index + 1 );
if( value.isNil() ) return Optional.empty();
if( !(value instanceof LuaTable) ) throw LuaValues.badArgument( index, "table", value.typeName() );
return Optional.of( new TableImpl( this, (LuaTable) value ) );
}
@Override
public void releaseImmediate()
{
released = true;
}
}

View File

@@ -0,0 +1,92 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.mixin;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.shared.Registry;
import dan200.computercraft.shared.peripheral.modem.wired.BlockCable;
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.BlockModelShaper;
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
import net.minecraft.client.renderer.block.ModelBlockRenderer;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraftforge.client.model.data.IModelData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Random;
/**
* Provides custom block breaking progress for modems, so it only applies to the current part.
*
* @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer, IModelData)
*/
@Mixin( BlockRenderDispatcher.class )
public class BlockRenderDispatcherMixin
{
@Shadow
private final Random random;
@Shadow
private final BlockModelShaper blockModelShaper;
@Shadow
private final ModelBlockRenderer modelRenderer;
public BlockRenderDispatcherMixin( Random random, BlockModelShaper blockModelShaper, ModelBlockRenderer modelRenderer )
{
this.random = random;
this.blockModelShaper = blockModelShaper;
this.modelRenderer = modelRenderer;
}
@Inject(
method = "name=/^renderBreakingTexture/ desc=/IModelData;\\)V$/",
at = @At( "HEAD" ),
cancellable = true,
require = 0 // This isn't critical functionality, so don't worry if we can't apply it.
)
public void renderBlockDamage(
BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack pose, VertexConsumer buffers, IModelData modelData,
CallbackInfo info
)
{
// Only apply to cables which have both a cable and modem
if( state.getBlock() != Registry.ModBlocks.CABLE.get()
|| !state.getValue( BlockCable.CABLE )
|| state.getValue( BlockCable.MODEM ) == CableModemVariant.None
)
{
return;
}
HitResult hit = Minecraft.getInstance().hitResult;
if( hit == null || hit.getType() != HitResult.Type.BLOCK ) return;
BlockPos hitPos = ((BlockHitResult) hit).getBlockPos();
if( !hitPos.equals( pos ) ) return;
info.cancel();
BlockState newState = WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getLocation().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
? state.getBlock().defaultBlockState().setValue( BlockCable.MODEM, state.getValue( BlockCable.MODEM ) )
: state.setValue( BlockCable.MODEM, CableModemVariant.None );
BakedModel model = blockModelShaper.getBlockModel( newState );
long seed = newState.getSeed( pos );
modelRenderer.tesselateBlock( world, model, newState, pos, pose, buffers, true, random, seed, OverlayTexture.NO_OVERLAY, modelData );
}
}

View File

@@ -400,9 +400,6 @@ public final class Registry
CauldronInteraction.WATER.put( ModItems.TURTLE_NORMAL.get(), ItemTurtle.CAULDRON_INTERACTION );
CauldronInteraction.WATER.put( ModItems.TURTLE_ADVANCED.get(), ItemTurtle.CAULDRON_INTERACTION );
// Mod integration code.
// TODO: if( ModList.get().isLoaded( MoreRedIntegration.MOD_ID ) ) MoreRedIntegration.initialise();
}
public static void registerLoot()

View File

@@ -56,7 +56,7 @@ public interface IContainerComputer
void continueUpload( @Nonnull UUID uploadId, @Nonnull List<FileSlice> slices );
/**
* Finish off an upload. This either writes the uploaded files or
* Finish off an upload. This either writes the uploaded files or informs the user that files will be overwritten.
*
* @param uploader The player uploading files.
* @param uploadId The unique ID of this upload.

View File

@@ -53,11 +53,6 @@ public class FileSlice
return;
}
bytes.rewind();
file.position( offset );
file.put( bytes );
file.rewind();
if( bytes.remaining() != 0 ) throw new IllegalStateException( "Should have read the whole buffer" );
file.put( offset, bytes, bytes.position(), bytes.remaining() );
}
}

View File

@@ -56,7 +56,7 @@ public class FileUpload
public boolean checksumMatches()
{
// This is meant to be a checksum. Doesn't need to be cryptographically secure, hence non constant time.
// This is meant to be a checksum. Doesn't need to be cryptographically secure, hence non-constant time.
byte[] digest = getDigest( bytes );
return digest != null && Arrays.equals( checksum, digest );
}
@@ -66,9 +66,8 @@ public class FileUpload
{
try
{
bytes.rewind();
MessageDigest digest = MessageDigest.getInstance( "SHA-256" );
digest.update( bytes );
digest.update( bytes.duplicate() );
return digest.digest();
}
catch( NoSuchAlgorithmException e )

View File

@@ -1,123 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.integration.morered;
import commoble.morered.api.ChanneledPowerSupplier;
import commoble.morered.api.MoreRedAPI;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
import dan200.computercraft.shared.util.CapabilityUtil;
import dan200.computercraft.shared.util.FixedPointTileEntityType;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MoreRedIntegration
{
public static final String MOD_ID = "morered";
private static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "morered" );
private static final class BundledPowerSupplier implements ICapabilityProvider, ChanneledPowerSupplier
{
private final IBundledRedstoneBlock block;
private final BlockEntity tile;
private LazyOptional<ChanneledPowerSupplier> instance;
private BundledPowerSupplier( IBundledRedstoneBlock block, BlockEntity tile )
{
this.block = block;
this.tile = tile;
}
@Nonnull
@Override
public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> cap, @Nullable Direction side )
{
if( cap != MoreRedAPI.CHANNELED_POWER_CAPABILITY ) return LazyOptional.empty();
if( tile.isRemoved() || !block.getBundledRedstoneConnectivity( tile.getLevel(), tile.getBlockPos(), side ) )
{
return LazyOptional.empty();
}
return (instance == null ? (instance = LazyOptional.of( () -> this )) : instance).cast();
}
@Override
public int getPowerOnChannel( @Nonnull Level world, @Nonnull BlockPos wirePos, @Nonnull BlockState wireState, @Nullable Direction wireFace, int channel )
{
if( wireFace == null ) return 0;
BlockPos pos = wirePos.relative( wireFace );
BlockState state = world.getBlockState( pos );
if( !(state.getBlock() instanceof IBundledRedstoneBlock block) ) return 0;
return (block.getBundledRedstoneOutput( world, pos, wireFace.getOpposite() ) & (1 << channel)) != 0 ? 31 : 0;
}
void invalidate()
{
instance = CapabilityUtil.invalidate( instance );
}
}
@SubscribeEvent
public static void attachBlockCapabilities( AttachCapabilitiesEvent<BlockEntity> event )
{
BlockEntity tile = event.getObject();
if( !(tile.getType() instanceof FixedPointTileEntityType) ) return;
Block block = ((FixedPointTileEntityType<?>) tile.getType()).getBlock();
if( !(block instanceof IBundledRedstoneBlock) ) return;
BundledPowerSupplier provider = new BundledPowerSupplier( (IBundledRedstoneBlock) block, tile );
event.addCapability( ID, provider );
event.addListener( provider::invalidate );
}
public static void initialise()
{
MinecraftForge.EVENT_BUS.register( MoreRedIntegration.class );
ComputerCraftAPI.registerBundledRedstoneProvider( MoreRedIntegration::getBundledPower );
}
private static int getBundledPower( Level world, BlockPos pos, Direction side )
{
BlockEntity tile = world.getBlockEntity( pos );
if( tile == null ) return -1;
ChanneledPowerSupplier power = CapabilityUtil.unwrapUnsafe( tile.getCapability( MoreRedAPI.CHANNELED_POWER_CAPABILITY, side ) );
if( power == null ) return -1;
BlockState state = tile.getBlockState();
// Skip ones already handled by CC. We can do this more efficiently.
if( state.getBlock() instanceof IBundledRedstoneBlock ) return -1;
int mask = 0;
for( int i = 0; i < 16; i++ )
{
mask |= power.getPowerOnChannel( world, pos, state, side, i ) > 0 ? (1 << i) : 0;
}
return mask;
}
}

View File

@@ -57,10 +57,11 @@ public final class NetworkHandler
registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new );
registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new );
registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new );
registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new );
registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new );
registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new );
registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new );
}

View File

@@ -0,0 +1,69 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fmllegacy.network.NetworkEvent;
import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* Starts a sound on the client.
*
* Used by speakers to play sounds.
*
* @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker
*/
public class SpeakerAudioClientMessage implements NetworkMessage
{
private final UUID source;
private final Vec3 pos;
private final ByteBuffer content;
private final float volume;
public SpeakerAudioClientMessage( UUID source, Vec3 pos, float volume, ByteBuffer content )
{
this.source = source;
this.pos = pos;
this.content = content;
this.volume = volume;
}
public SpeakerAudioClientMessage( FriendlyByteBuf buf )
{
source = buf.readUUID();
pos = new Vec3( buf.readDouble(), buf.readDouble(), buf.readDouble() );
volume = buf.readFloat();
SpeakerManager.getSound( source ).pushAudio( buf );
content = null;
}
@Override
public void toBytes( @Nonnull FriendlyByteBuf buf )
{
buf.writeUUID( source );
buf.writeDouble( pos.x() );
buf.writeDouble( pos.y() );
buf.writeDouble( pos.z() );
buf.writeFloat( volume );
buf.writeBytes( content.duplicate() );
}
@Override
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SpeakerManager.getSound( source ).playAudio( pos, volume );
}
}

View File

@@ -5,7 +5,7 @@
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.SoundManager;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.phys.Vec3;
@@ -53,6 +53,6 @@ public class SpeakerMoveClientMessage implements NetworkMessage
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SoundManager.moveSound( source, pos );
SpeakerManager.moveSound( source, pos );
}
}

View File

@@ -5,7 +5,7 @@
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.SoundManager;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
@@ -66,6 +66,6 @@ public class SpeakerPlayClientMessage implements NetworkMessage
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SoundManager.playSound( source, pos, sound, volume, pitch );
SpeakerManager.getSound( source ).playSound( pos, sound, volume, pitch );
}
}

View File

@@ -5,7 +5,7 @@
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.SoundManager;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
@@ -46,6 +46,6 @@ public class SpeakerStopClientMessage implements NetworkMessage
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SoundManager.stopSound( source );
SpeakerManager.stopSound( source );
}
}

View File

@@ -5,7 +5,6 @@
*/
package dan200.computercraft.shared.network.server;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.computer.core.IContainerComputer;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.upload.FileSlice;
@@ -122,9 +121,9 @@ public class UploadFileMessage extends ComputerServerMessage
buf.writeByte( slice.getFileId() );
buf.writeVarInt( slice.getOffset() );
slice.getBytes().rewind();
buf.writeShort( slice.getBytes().remaining() );
buf.writeBytes( slice.getBytes() );
ByteBuffer bytes = slice.getBytes().duplicate();
buf.writeShort( bytes.remaining() );
buf.writeBytes( bytes );
}
}
@@ -158,7 +157,6 @@ public class UploadFileMessage extends ComputerServerMessage
int canWrite = Math.min( remaining, capacity - currentOffset );
ComputerCraft.log.info( "Adding slice from {} to {}", currentOffset, currentOffset + canWrite - 1 );
contents.position( currentOffset ).limit( currentOffset + canWrite );
slices.add( new FileSlice( fileId, currentOffset, contents.slice() ) );
currentOffset += canWrite;

View File

@@ -29,7 +29,7 @@ import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL;
/**
* This peripheral allows you to interact with command blocks.
*
* Command blocks are only wrapped as peripherals if the {@literal enable_command_block} option is true within the
* Command blocks are only wrapped as peripherals if the {@code enable_command_block} option is true within the
* config.
*
* This API is <em>not</em> the same as the {@link CommandAPI} API, which is exposed on command computers.

View File

@@ -19,10 +19,10 @@ import javax.annotation.Nonnull;
*
* This works with energy storage blocks, as well as generators and machines which consume energy.
*
* <blockquote>
* <strong>Note:</strong> Due to limitations with Forge's energy API, it is not possible to measure throughput (i.e. RF
* :::note
* Due to limitations with Forge's energy API, it is not possible to measure throughput (i.e. RF
* used/generated per tick).
* </blockquote>
* :::
*
* @cc.module energy_storage
*/

View File

@@ -21,9 +21,60 @@ import java.util.HashSet;
import java.util.Set;
/**
* The modem peripheral allows you to send messages between computers.
* Modems allow you to send messages between computers over long distances.
*
* :::tip
* Modems provide a fairly basic set of methods, which makes them very flexible but often hard to work with. The
* {@literal @}{rednet} API is built on top of modems, and provides a more user-friendly interface.
* :::
*
* ## Sending and receiving messages
* Modems operate on a series of channels, a bit like frequencies on a radio. Any modem can send a message on a
* particular channel, but only those which have {@link #open opened} the channel and are "listening in" can receive
* messages.
*
* Channels are represented as an integer between 0 and 65535 inclusive. These channels don't have any defined meaning,
* though some APIs or programs will assign a meaning to them. For instance, the @{gps} module sends all its messages on
* channel 65534 (@{gps.CHANNEL_GPS}), while @{rednet} uses channels equal to the computer's ID.
*
* - Sending messages is done with the {@link #transmit(int, int, Object)} message.
* - Receiving messages is done by listening to the @{modem_message} event.
*
* ## Types of modem
* CC: Tweaked comes with three kinds of modem, with different capabilities.
*
* <ul>
* <li><strong>Wireless modems:</strong> Wireless modems can send messages to any other wireless modem. They can be placed next to a
* computer, or equipped as a pocket computer or turtle upgrade.
*
* Wireless modems have a limited range, only sending messages to modems within 64 blocks. This range increases
* linearly once the modem is above y=96, to a maximum of 384 at world height.</li>
* <li><strong>Ender modems:</strong> These are upgraded versions of normal wireless modems. They do not have a distance
* limit, and can send messages between dimensions.</li>
* <li><strong>Wired modems:</strong> These send messages to other any other wired modems connected to the same network
* (using <em>Networking Cable</em>). They also can be used to attach additional peripherals to a computer.</li></ul>
*
* @cc.module modem
* @cc.see modem_message Queued when a modem receives a message on an {@link #open(int) open channel}.
* @cc.see rednet A networking API built on top of the modem peripheral.
* @cc.usage Wrap a modem and a message on channel 15, requesting a response on channel 43. Then wait for a message to
* arrive on channel 43 and print it.
*
* <pre>{@code
* local modem = peripheral.find("modem") or error("No modem attached", 0)
* modem.open(43) -- Open 43 so we can receive replies
*
* -- Send our message
* modem.transmit(15, 43, "Hello, world!")
*
* -- And wait for a reply
* local event, side, channel, replyChannel, message, distance
* repeat
* event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
* until channel == 43
*
* print("Received a reply: " .. tostring(message))
* }</pre>
*/
public abstract class ModemPeripheral implements IPeripheral, IPacketSender, IPacketReceiver
{
@@ -157,12 +208,23 @@ public abstract class ModemPeripheral implements IPeripheral, IPacketSender, IPa
* Sends a modem message on a certain channel. Modems listening on the channel will queue a {@code modem_message}
* event on adjacent computers.
*
* <blockquote><strong>Note:</strong> The channel does not need be open to send a message.</blockquote>
* :::note
* The channel does not need be open to send a message.
* :::
*
* @param channel The channel to send messages on.
* @param replyChannel The channel that responses to this message should be sent on.
* @param payload The object to send. This can be a boolean, string, number, or table.
* @param replyChannel The channel that responses to this message should be sent on. This can be the same as
* {@code channel} or entirely different. The channel must have been {@link #open opened} on
* the sending computer in order to receive the replies.
* @param payload The object to send. This can be any primitive type (boolean, number, string) as well as
* tables. Other types (like functions), as well as metatables, will not be transmitted.
* @throws LuaException If the channel is out of range.
* @cc.usage Wrap a modem and a message on channel 15, requesting a response on channel 43.
*
* <pre>{@code
* local modem = peripheral.find("modem") or error("No modem attached", 0)
* modem.transmit(15, 43, "Hello, world!")
* }</pre>
*/
@LuaFunction
public final void transmit( int channel, int replyChannel, Object payload ) throws LuaException

View File

@@ -80,8 +80,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
* If this computer is attached to the network, it _will not_ be included in
* this list.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @param computer The calling computer.
* @return Remote peripheral names on the network.
@@ -95,8 +96,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
/**
* Determine if a peripheral is available on this wired network.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @param computer The calling computer.
* @param name The peripheral's name.
@@ -112,8 +114,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
/**
* Get the type of a peripheral is available on this wired network.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @param computer The calling computer.
* @param name The peripheral's name.
@@ -132,8 +135,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
/**
* Check a peripheral is of a particular type.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @param computer The calling computer.
* @param name The peripheral's name.
@@ -153,8 +157,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
/**
* Get all available methods for the remote peripheral with the given name.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @param computer The calling computer.
* @param name The peripheral's name.
@@ -174,8 +179,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
/**
* Call a method on a peripheral on this wired network.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @param computer The calling computer.
* @param context The Lua context we're executing in.
@@ -204,8 +210,9 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW
* may be used by other computers on the network to wrap this computer as a
* peripheral.
*
* <blockquote><strong>Important:</strong> This function only appears on wired modems. Check {@link #isWireless}
* returns false before calling it.</blockquote>
* :::note
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
*
* @return The current computer's name.
* @cc.treturn string|nil The current computer's name on the wired network.

View File

@@ -94,9 +94,12 @@ class Expander
if( !isPositive )
{
BlockEntity otherOrigin = level.getBlockEntity( otherMonitor.toWorldPos( 0, 0 ) );
if( otherOrigin == null || !origin.isCompatible( (TileMonitor) otherOrigin ) ) return false;
if( !(otherOrigin instanceof TileMonitor originMonitor) || !origin.isCompatible( originMonitor ) )
{
return false;
}
origin = (TileMonitor) otherOrigin;
origin = originMonitor;
}
this.width = width;

View File

@@ -0,0 +1,118 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaTable;
import dan200.computercraft.shared.util.PauseAwareTimer;
import net.minecraft.util.Mth;
import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral.SAMPLE_RATE;
/**
* Internal state of the DFPWM decoder and the state of playback.
*/
class DfpwmState
{
private static final long SECOND = TimeUnit.SECONDS.toNanos( 1 );
/**
* The minimum size of the client's audio buffer. Once we have less than this on the client, we should send another
* batch of audio.
*/
private static final long CLIENT_BUFFER = (long) (SECOND * 0.5);
private static final int PREC = 10;
private int charge = 0; // q
private int strength = 0; // s
private boolean previousBit = false;
private boolean unplayed = true;
private long clientEndTime = PauseAwareTimer.getTime();
private float pendingVolume = 1.0f;
private ByteBuffer pendingAudio;
synchronized boolean pushBuffer( LuaTable<?, ?> table, int size, @Nonnull Optional<Double> volume ) throws LuaException
{
if( pendingAudio != null ) return false;
int outSize = size / 8;
ByteBuffer buffer = ByteBuffer.allocate( outSize );
for( int i = 0; i < outSize; i++ )
{
int thisByte = 0;
for( int j = 1; j <= 8; j++ )
{
int level = table.getInt( i * 8 + j );
if( level < -128 || level > 127 )
{
throw new LuaException( "table item #" + (i * 8 + j) + " must be between -128 and 127" );
}
boolean currentBit = level > charge || (level == charge && charge == 127);
// Identical to DfpwmStream. Not happy with this, but saves some inheritance.
int target = currentBit ? 127 : -128;
// q' <- q + (s * (t - q) + 128)/256
int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1;
int z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
int nextStrength = strength;
if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1;
if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8);
charge = nextCharge;
strength = nextStrength;
previousBit = currentBit;
thisByte = (thisByte >> 1) + (currentBit ? 128 : 0);
}
buffer.put( (byte) thisByte );
}
buffer.flip();
pendingAudio = buffer;
pendingVolume = Mth.clamp( volume.orElse( (double) pendingVolume ).floatValue(), 0.0f, 3.0f );
return true;
}
boolean shouldSendPending( long now )
{
return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
}
ByteBuffer pullPending( long now )
{
ByteBuffer audio = pendingAudio;
pendingAudio = null;
// Compute when we should consider sending the next packet.
clientEndTime = Math.max( now, clientEndTime ) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
unplayed = false;
return audio;
}
boolean isPlaying()
{
return unplayed || clientEndTime >= PauseAwareTimer.getTime();
}
float getVolume()
{
return pendingVolume;
}
}

View File

@@ -9,77 +9,177 @@ import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.LuaTable;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.SpeakerAudioClientMessage;
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.util.PauseAwareTimer;
import net.minecraft.ResourceLocationException;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.game.ClientboundCustomSoundPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nonnull;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import java.util.*;
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
/**
* Speakers allow playing notes and other sounds.
* The speaker peirpheral allow your computer to play notes and other sounds.
*
* The speaker can play three kinds of sound, in increasing orders of complexity:
* - {@link #playNote} allows you to play noteblock note.
* - {@link #playSound} plays any built-in Minecraft sound, such as block sounds or mob noises.
* - {@link #playAudio} can play arbitrary audio.
*
* @cc.module speaker
* @cc.since 1.80pr1
*/
public abstract class SpeakerPeripheral implements IPeripheral
{
private static final int MIN_TICKS_BETWEEN_SOUNDS = 1;
/**
* Number of samples/s in a dfpwm1a audio track.
*/
public static final int SAMPLE_RATE = 48000;
private final UUID source = UUID.randomUUID();
private final Set<IComputerAccess> computers = Collections.newSetFromMap( new HashMap<>() );
private long clock = 0;
private long lastPlayTime = 0;
private final AtomicInteger notesThisTick = new AtomicInteger();
private long lastPositionTime;
private Vec3 lastPosition;
private long lastPlayTime;
private final List<PendingSound> pendingNotes = new ArrayList<>();
private final Object lock = new Object();
private boolean shouldStop;
private PendingSound pendingSound = null;
private DfpwmState dfpwmState;
public void update()
{
clock++;
notesThisTick.set( 0 );
Vec3 pos = getPosition();
Level level = getLevel();
if( level == null ) return;
MinecraftServer server = level.getServer();
synchronized( pendingNotes )
{
for( PendingSound sound : pendingNotes )
{
lastPlayTime = clock;
server.getPlayerList().broadcast(
null, pos.x, pos.y, pos.z, sound.volume * 16, level.dimension(),
new ClientboundCustomSoundPacket( sound.location, SoundSource.RECORDS, pos, sound.volume, sound.pitch )
);
}
pendingNotes.clear();
}
// The audio dispatch logic here is pretty messy, which I'm not proud of. The general logic here is that we hold
// the main "lock" when modifying the dfpwmState/pendingSound variables and no other time.
// dfpwmState will only ever transition from having a buffer to not having a buffer on the main thread (so this
// method), so we don't need to bother locking that.
boolean shouldStop;
PendingSound sound;
DfpwmState dfpwmState;
synchronized( lock )
{
sound = pendingSound;
dfpwmState = this.dfpwmState;
pendingSound = null;
shouldStop = this.shouldStop;
if( shouldStop )
{
dfpwmState = this.dfpwmState = null;
sound = null;
this.shouldStop = false;
}
}
// Stop the speaker and nuke the position, so we don't update it again.
if( shouldStop && lastPosition != null )
{
lastPosition = null;
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) );
return;
}
long now = PauseAwareTimer.getTime();
if( sound != null )
{
lastPlayTime = clock;
NetworkHandler.sendToAllAround(
new SpeakerPlayClientMessage( getSource(), pos, sound.location, sound.volume, sound.pitch ),
level, pos, sound.volume * 16
);
syncedPosition( pos );
}
else if( dfpwmState != null && dfpwmState.shouldSendPending( now ) )
{
// If clients need to receive another batch of audio, send it and then notify computers our internal buffer is
// free again.
NetworkHandler.sendToAllTracking(
new SpeakerAudioClientMessage( getSource(), pos, dfpwmState.getVolume(), dfpwmState.pullPending( now ) ),
getLevel().getChunkAt( new BlockPos( pos ) )
);
syncedPosition( pos );
// And notify computers that we have space for more audio.
for( IComputerAccess computer : computers )
{
computer.queueEvent( "speaker_audio_empty", computer.getAttachmentName() );
}
}
// Push position updates to any speakers which have ever played a note,
// have moved by a non-trivial amount and haven't had a position update
// in the last second.
if( lastPlayTime > 0 && (clock - lastPositionTime) >= 20 )
if( lastPosition != null && (clock - lastPositionTime) >= 20 )
{
Vec3 position = getPosition();
if( lastPosition == null || lastPosition.distanceToSqr( position ) >= 0.1 )
if( lastPosition.distanceToSqr( position ) >= 0.1 )
{
lastPosition = position;
lastPositionTime = clock;
NetworkHandler.sendToAllTracking(
new SpeakerMoveClientMessage( getSource(), position ),
getLevel().getChunkAt( new BlockPos( position ) )
);
syncedPosition( position );
}
}
}
@Nullable
public abstract Level getLevel();
@Nonnull
public abstract Vec3 getPosition();
protected abstract UUID getSource();
public boolean madeSound( long ticks )
@Nonnull
public UUID getSource()
{
return clock - lastPlayTime <= ticks;
return source;
}
public boolean madeSound()
{
DfpwmState state = dfpwmState;
return clock - lastPlayTime <= 20 || (state != null && state.isPlaying());
}
@Nonnull
@@ -90,18 +190,81 @@ public abstract class SpeakerPeripheral implements IPeripheral
}
/**
* Plays a sound through the speaker.
* Plays a note block note through the speaker.
*
* This plays sounds similar to the {@code /playsound} command in Minecraft.
* It takes the namespaced path of a sound (e.g. {@code minecraft:block.note_block.harp})
* with an optional volume and speed multiplier, and plays it through the speaker.
* This takes the name of a note to play, as well as optionally the volume
* and pitch to play the note at.
*
* The pitch argument uses semitones as the unit. This directly maps to the
* number of clicks on a note block. For reference, 0, 12, and 24 map to F#,
* and 6 and 18 map to C.
*
* A maximum of 8 notes can be played in a single tick. If this limit is hit, this function will return
* {@literal false}.
*
* ### Valid instruments
* The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments).
* These are:
*
* {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, @code "flute"},
* {@code "bell"}, {@code "guitar"}, {@code "chime"}, {@code "xylophone"}, {@code "iron_xylophone"},
* {@code "cow_bell"}, {@code "didgeridoo"}, {@code "bit"}, {@code "banjo"} and {@code "pling"}.
*
* @param context The Lua context
* @param instrumentA The instrument to use to play this note.
* @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0.
* @param pitchA The pitch to play the note at in semitones, from 0 to 24. Defaults to 12.
* @return Whether the note could be played as the limit was reached.
* @throws LuaException If the instrument doesn't exist.
*/
@LuaFunction
public final boolean playNote( ILuaContext context, String instrumentA, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
{
float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) );
float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) );
NoteBlockInstrument instrument = null;
for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
{
if( testInstrument.getSerializedName().equalsIgnoreCase( instrumentA ) )
{
instrument = testInstrument;
break;
}
}
// Check if the note exists
if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + instrument + "\"!" );
synchronized( pendingNotes )
{
if( pendingNotes.size() >= ComputerCraft.maxNotesPerTick ) return false;
pendingNotes.add( new PendingSound( instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ) ) );
}
return true;
}
/**
* Plays a Minecraft sound through the speaker.
*
* This takes the [name of a Minecraft sound](https://minecraft.fandom.com/wiki/Sounds.json), such as
* {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch.
*
* Only one sound can be played at once. This function will return {@literal false} if another sound was started
* this tick, or if some {@link #playAudio audio} is still playing.
*
* @param context The Lua context
* @param name The name of the sound to play.
* @param volumeA The volume to play the sound at, from 0.0 to 3.0. Defaults to 1.0.
* @param pitchA The speed to play the sound at, from 0.5 to 2.0. Defaults to 1.0.
* @return Whether the sound could be played.
* @throws LuaException If the sound name couldn't be decoded.
* @throws LuaException If the sound name was invalid.
* @cc.usage Play a creeper hiss with the speaker.
*
* <pre>{@code
* local speaker = peripheral.find("speaker")
* speaker.playSound("entity.creeper.primed")
* }</pre>
*/
@LuaFunction
public final boolean playSound( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
@@ -119,89 +282,113 @@ public abstract class SpeakerPeripheral implements IPeripheral
throw new LuaException( "Malformed sound name '" + name + "' " );
}
return playSound( context, identifier, volume, pitch, false );
synchronized( lock )
{
if( dfpwmState != null && dfpwmState.isPlaying() ) return false;
dfpwmState = null;
pendingSound = new PendingSound( identifier, volume, pitch );
return true;
}
}
/**
* Plays a note block note through the speaker.
* Attempt to stream some audio data to the speaker.
*
* This takes the name of a note to play, as well as optionally the volume
* and pitch to play the note at.
* This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer
* and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for
* a @{speaker_audio_empty} event before trying again.
*
* The pitch argument uses semitones as the unit. This directly maps to the
* number of clicks on a note block. For reference, 0, 12, and 24 map to F#,
* and 6 and 18 map to C.
* :::note
* The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
* number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
* (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
* computer is lagging.
* :::
*
* @param context The Lua context
* @param name The name of the note to play.
* @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0.
* @param pitchA The pitch to play the note at in semitones, from 0 to 24. Defaults to 12.
* @return Whether the note could be played.
* @throws LuaException If the instrument doesn't exist.
* {@literal @}{speaker_audio} provides a more complete guide in to using speakers
*
* @param context The Lua context.
* @param audio The audio data to play.
* @param volume The volume to play this audio at.
* @return If there was room to accept this audio data.
* @throws LuaException If the audio data is malformed.
* @cc.tparam {number...} audio A list of amplitudes.
* @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume
* given to {@link #playAudio}.
* @cc.since 1.100
* @cc.usage Read an audio file, decode it using @{cc.audio.dfpwm}, and play it using the speaker.
*
* <pre>{@code
* local dfpwm = require("cc.audio.dfpwm")
* local speaker = peripheral.find("speaker")
*
* local decoder = dfpwm.make_decoder()
* for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
* local buffer = decoder(chunk)
*
* while not speaker.playAudio(buffer) do
* os.pullEvent("speaker_audio_empty")
* end
* end
* }</pre>
* @cc.see cc.audio.dfpwm Provides utilities for decoding DFPWM audio files into a format which can be played by
* the speaker.
* @cc.see speaker_audio For a more complete introduction to the {@link #playAudio} function.
*/
@LuaFunction( unsafe = true )
public final boolean playAudio( ILuaContext context, LuaTable<?, ?> audio, Optional<Double> volume ) throws LuaException
{
checkFinite( 1, volume.orElse( 0.0 ) );
// TODO: Use ArgumentHelpers instead?
int length = audio.length();
if( length <= 0 ) throw new LuaException( "Cannot play empty audio" );
if( length > 128 * 1024 ) throw new LuaException( "Audio data is too large" );
DfpwmState state;
synchronized( lock )
{
if( dfpwmState == null || !dfpwmState.isPlaying() ) dfpwmState = new DfpwmState();
state = dfpwmState;
pendingSound = null;
}
return state.pushBuffer( audio, length, volume );
}
/**
* Stop all audio being played by this speaker.
*
* This clears any audio that {@link #playAudio} had queued and stops the latest sound played by {@link #playSound}.
*
* @cc.since 1.100
*/
@LuaFunction
public final synchronized boolean playNote( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
public final void stop()
{
float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) );
float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) );
NoteBlockInstrument instrument = null;
for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
{
if( testInstrument.getSerializedName().equalsIgnoreCase( name ) )
{
instrument = testInstrument;
break;
}
}
// Check if the note exists
if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + name + "\"!" );
// If the resource location for note block notes changes, this method call will need to be updated
boolean success = playSound( context, instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true );
if( success ) notesThisTick.incrementAndGet();
return success;
shouldStop = true;
}
private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException
private void syncedPosition( Vec3 position )
{
if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS )
{
// Rate limiting occurs when we've already played a sound within the last tick.
if( !isNote ) return false;
// Or we've played more notes than allowable within the current tick.
if( clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick ) return false;
}
lastPosition = position;
lastPositionTime = clock;
}
Level world = getLevel();
Vec3 pos = getPosition();
@Override
public void attach( @Nonnull IComputerAccess computer )
{
computers.add( computer );
}
float actualVolume = Mth.clamp( volume, 0.0f, 3.0f );
float range = actualVolume * 16;
@Override
public void detach( @Nonnull IComputerAccess computer )
{
computers.remove( computer );
}
context.issueMainThreadTask( () -> {
MinecraftServer server = world.getServer();
if( server == null ) return null;
if( isNote )
{
server.getPlayerList().broadcast(
null, pos.x, pos.y, pos.z, range, world.dimension(),
new ClientboundCustomSoundPacket( name, SoundSource.RECORDS, pos, actualVolume, pitch )
);
}
else
{
NetworkHandler.sendToAllAround(
new SpeakerPlayClientMessage( getSource(), pos, name, actualVolume, pitch ),
world, pos, range
);
}
return null;
} );
lastPlayTime = clock;
return true;
private record PendingSound(ResourceLocation location, float volume, float pitch)
{
}
}

View File

@@ -21,7 +21,6 @@ import net.minecraftforge.common.util.LazyOptional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.UUID;
import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL;
@@ -29,7 +28,6 @@ public class TileSpeaker extends TileGeneric
{
private final SpeakerPeripheral peripheral;
private LazyOptional<IPeripheral> peripheralCap;
private final UUID source = UUID.randomUUID();
public TileSpeaker( BlockEntityType<TileSpeaker> type, BlockPos pos, BlockState state )
{
@@ -48,7 +46,7 @@ public class TileSpeaker extends TileGeneric
super.setRemoved();
if( level != null && !level.isClientSide )
{
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( peripheral.getSource() ) );
}
}
@@ -87,6 +85,7 @@ public class TileSpeaker extends TileGeneric
return speaker.getLevel();
}
@Nonnull
@Override
public Vec3 getPosition()
{
@@ -94,12 +93,6 @@ public class TileSpeaker extends TileGeneric
return new Vec3( pos.getX(), pos.getY(), pos.getZ() );
}
@Override
protected UUID getSource()
{
return speaker.source;
}
@Override
public boolean equals( @Nullable IPeripheral other )
{

View File

@@ -9,11 +9,9 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.fml.LogicalSide;
import net.minecraftforge.fmllegacy.LogicalSidedProvider;
import net.minecraftforge.fmllegacy.server.ServerLifecycleHooks;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* A speaker peripheral which is used on an upgrade, and so is only attached to one computer.
@@ -22,21 +20,13 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral
{
public static final String ADJECTIVE = "upgrade.computercraft.speaker.adjective";
private final UUID source = UUID.randomUUID();
@Override
protected final UUID getSource()
{
return source;
}
@Override
public void detach( @Nonnull IComputerAccess computer )
{
// We could be in the process of shutting down the server, so we can't send packets in this case.
MinecraftServer server = LogicalSidedProvider.INSTANCE.get( LogicalSide.SERVER );
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if( server == null || server.isStopped() ) return;
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) );
}
}

View File

@@ -17,7 +17,6 @@ import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ClientComputer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.pocket.apis.PocketAPI;
@@ -34,6 +33,7 @@ import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
@@ -80,53 +80,65 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
PocketUpgrades.getVanillaUpgrades().map( x -> create( -1, null, -1, x ) ).forEach( stacks::add );
}
private boolean tick( @Nonnull ItemStack stack, @Nonnull Level world, @Nonnull Entity entity, @Nonnull PocketServerComputer computer )
{
IPocketUpgrade upgrade = getUpgrade( stack );
computer.setLevel( world );
computer.updateValues( entity, stack, upgrade );
boolean changed = false;
// Sync ID
int id = computer.getID();
if( id != getComputerID( stack ) )
{
changed = true;
setComputerID( stack, id );
}
// Sync label
String label = computer.getLabel();
if( !Objects.equal( label, getLabel( stack ) ) )
{
changed = true;
setLabel( stack, label );
}
// Update pocket upgrade
if( upgrade != null ) upgrade.update( computer, computer.getPeripheral( ComputerSide.BACK ) );
return changed;
}
@Override
public void inventoryTick( @Nonnull ItemStack stack, Level world, @Nonnull Entity entity, int slotNum, boolean selected )
{
if( !world.isClientSide )
{
// Server side
Container inventory = entity instanceof Player ? ((Player) entity).getInventory() : null;
PocketServerComputer computer = createServerComputer( world, inventory, entity, stack );
if( computer != null )
{
IPocketUpgrade upgrade = getUpgrade( stack );
Container inventory = entity instanceof Player player ? player.getInventory() : null;
PocketServerComputer computer = createServerComputer( world, entity, inventory, stack );
computer.keepAlive();
// Ping computer
computer.keepAlive();
computer.setLevel( world );
computer.updateValues( entity, stack, upgrade );
// Sync ID
int id = computer.getID();
if( id != getComputerID( stack ) )
{
setComputerID( stack, id );
if( inventory != null ) inventory.setChanged();
}
// Sync label
String label = computer.getLabel();
if( !Objects.equal( label, getLabel( stack ) ) )
{
setLabel( stack, label );
if( inventory != null ) inventory.setChanged();
}
// Update pocket upgrade
if( upgrade != null )
{
upgrade.update( computer, computer.getPeripheral( ComputerSide.BACK ) );
}
}
boolean changed = tick( stack, world, entity, computer );
if( changed && inventory != null ) inventory.setChanged();
}
else
{
// Client side
createClientComputer( stack );
}
}
@Override
public boolean onEntityItemUpdate( ItemStack stack, ItemEntity entity )
{
if( entity.level.isClientSide ) return false;
PocketServerComputer computer = getServerComputer( stack );
if( computer != null && tick( stack, entity.level, entity, computer ) ) entity.setItem( stack.copy() );
return false;
}
@Nonnull
@Override
public InteractionResultHolder<ItemStack> use( Level world, Player player, @Nonnull InteractionHand hand )
@@ -134,22 +146,18 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
ItemStack stack = player.getItemInHand( hand );
if( !world.isClientSide )
{
PocketServerComputer computer = createServerComputer( world, player.getInventory(), player, stack );
PocketServerComputer computer = createServerComputer( world, player, player.getInventory(), stack );
computer.turnOn();
boolean stop = false;
if( computer != null )
IPocketUpgrade upgrade = getUpgrade( stack );
if( upgrade != null )
{
computer.turnOn();
IPocketUpgrade upgrade = getUpgrade( stack );
if( upgrade != null )
{
computer.updateValues( player, stack, upgrade );
stop = upgrade.onRightClick( world, computer, computer.getPeripheral( ComputerSide.BACK ) );
}
computer.updateValues( player, stack, upgrade );
stop = upgrade.onRightClick( world, computer, computer.getPeripheral( ComputerSide.BACK ) );
}
if( !stop && computer != null )
if( !stop )
{
boolean isTypingOnly = hand == InteractionHand.OFF_HAND;
new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
@@ -207,17 +215,17 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
return super.getCreatorModId( stack );
}
public PocketServerComputer createServerComputer( final Level world, Container inventory, Entity entity, @Nonnull ItemStack stack )
@Nonnull
public PocketServerComputer createServerComputer( Level world, Entity entity, @Nullable Container inventory, @Nonnull ItemStack stack )
{
if( world.isClientSide ) return null;
if( world.isClientSide ) throw new IllegalStateException( "Cannot call createServerComputer on the client" );
PocketServerComputer computer;
int instanceID = getInstanceID( stack );
int sessionID = getSessionID( stack );
int correctSessionID = ComputerCraft.serverComputerRegistry.getSessionID();
if( instanceID >= 0 && sessionID == correctSessionID &&
ComputerCraft.serverComputerRegistry.contains( instanceID ) )
if( instanceID >= 0 && sessionID == correctSessionID && ComputerCraft.serverComputerRegistry.contains( instanceID ) )
{
computer = (PocketServerComputer) ComputerCraft.serverComputerRegistry.get( instanceID );
}
@@ -235,13 +243,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
computerID = ComputerCraftAPI.createUniqueNumberedSaveDir( world, "computer" );
setComputerID( stack, computerID );
}
computer = new PocketServerComputer(
world,
computerID,
getLabel( stack ),
instanceID,
getFamily()
);
computer = new PocketServerComputer( world, computerID, getLabel( stack ), instanceID, getFamily() );
computer.updateValues( entity, stack, getUpgrade( stack ) );
computer.addAPI( new PocketAPI( computer ) );
ComputerCraft.serverComputerRegistry.add( instanceID, computer );
@@ -251,15 +253,17 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
return computer;
}
public static ServerComputer getServerComputer( @Nonnull ItemStack stack )
@Nullable
public static PocketServerComputer getServerComputer( @Nonnull ItemStack stack )
{
int session = getSessionID( stack );
if( session != ComputerCraft.serverComputerRegistry.getSessionID() ) return null;
int instanceID = getInstanceID( stack );
return instanceID >= 0 ? ComputerCraft.serverComputerRegistry.get( instanceID ) : null;
return instanceID >= 0 ? (PocketServerComputer) ComputerCraft.serverComputerRegistry.get( instanceID ) : null;
}
@Nullable
public static ClientComputer createClientComputer( @Nonnull ItemStack stack )
{
int instanceID = getInstanceID( stack );
@@ -274,6 +278,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
return null;
}
@Nullable
private static ClientComputer getClientComputer( @Nonnull ItemStack stack )
{
int instanceID = getInstanceID( stack );

View File

@@ -42,6 +42,6 @@ public class PocketSpeaker extends AbstractPocketUpgrade
}
speaker.update();
access.setLight( speaker.madeSound( 20 ) ? 0x3320fc : -1 );
access.setLight( speaker.madeSound() ? 0x3320fc : -1 );
}
}

View File

@@ -10,6 +10,8 @@ import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nonnull;
public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
{
private Level world = null;
@@ -27,6 +29,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
return world;
}
@Nonnull
@Override
public Vec3 getPosition()
{

View File

@@ -22,8 +22,50 @@ import java.util.Map;
import java.util.Optional;
/**
* The turtle API allows you to control your turtle.
* Turtles are a robotic device, which can break and place blocks, attack mobs, and move about the world. They have
* an internal inventory of 16 slots, allowing them to store blocks they have broken or would like to place.
*
* ## Movement
* Turtles are capable of moving throug the world. As turtles are blocks themselves, they are confined to Minecraft's
* grid, moving a single block at a time.
*
* {@literal @}{turtle.forward} and @{turtle.back} move the turtle in the direction it is facing, while @{turtle.up} and
* {@literal @}{turtle.down} move it up and down (as one might expect!). In order to move left or right, you first need
* to turn the turtle using @{turtle.turnLeft}/@{turtle.turnRight} and then move forward or backwards.
*
* :::info
* The name "turtle" comes from [Turtle graphics], which originated from the Logo programming language. Here you'd move
* a turtle with various commands like "move 10" and "turn left", much like ComputerCraft's turtles!
* :::
*
* Moving a turtle (though not turning it) consumes *fuel*. If a turtle does not have any @{turtle.refuel|fuel}, it
* won't move, and the movement functions will return @{false}. If your turtle isn't going anywhere, the first thing to
* check is if you've fuelled your turtle.
*
* :::tip Handling errors
* Many turtle functions can fail in various ways. For instance, a turtle cannot move forward if there's already a block
* there. Instead of erroring, functions which can fail either return @{true} if they succeed, or @{false} and some
* error message if they fail.
*
* Unexpected failures can often lead to strange behaviour. It's often a good idea to check the return values of these
* functions, or wrap them in @{assert} (for instance, use `assert(turtle.forward())` rather than `turtle.forward()`),
* so the program doesn't misbehave.
* :::
*
* ## Turtle upgrades
* While a normal turtle can move about the world and place blocks, its functionality is limited. Thankfully, turtles
* can be upgraded with *tools* and @{peripheral|peripherals}. Turtles have two upgrade slots, one on the left and right
* sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the @{turtle.equipLeft}/@{turtle.equipRight}
* functions.
*
* Turtle tools allow you to break blocks (@{turtle.dig}) and attack entities (@{turtle.attack}). Some tools are more
* suitable to a task than others. For instance, a diamond pickaxe can break every block, while a sword does more
* damage. Other tools have more niche use-cases, for instance hoes can til dirt.
*
* Peripherals (such as the @{modem|wireless modem} or @{speaker}) can also be equipped as upgrades. These are then
* accessible by accessing the `"left"` or `"right"` peripheral.
*
* [Turtle Graphics]: https://en.wikipedia.org/wiki/Turtle_graphics "Turtle graphics"
* @cc.module turtle
* @cc.since 1.3
*/

View File

@@ -31,7 +31,6 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.Container;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySelector;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.level.Level;
@@ -39,6 +38,7 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.items.IItemHandlerModifiable;
@@ -48,6 +48,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR;
import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED;
@@ -65,6 +66,8 @@ public class TurtleBrain implements ITurtleAccess
private static final int ANIM_DURATION = 8;
public static final Predicate<Entity> PUSHABLE_ENTITY = entity -> !entity.isSpectator() && entity.getPistonPushReaction() != PushReaction.IGNORE;
private TileTurtle owner;
private ComputerProxy proxy;
private GameProfile owningPlayer;
@@ -886,7 +889,7 @@ public class TurtleBrain implements ITurtleAccess
}
AABB aabb = new AABB( minX, minY, minZ, maxX, maxY, maxZ );
List<Entity> list = world.getEntitiesOfClass( Entity.class, aabb, EntitySelector.NO_SPECTATORS );
List<Entity> list = world.getEntitiesOfClass( Entity.class, aabb, PUSHABLE_ENTITY );
if( !list.isEmpty() )
{
double pushStep = 1.0f / ANIM_DURATION;

View File

@@ -43,6 +43,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade
return turtle.getLevel();
}
@Nonnull
@Override
public Vec3 getPosition()
{

View File

@@ -0,0 +1,53 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.util;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* A monotonically increasing clock which accounts for the game being paused.
*/
@Mod.EventBusSubscriber( Dist.CLIENT )
public final class PauseAwareTimer
{
private static boolean paused;
private static long pauseTime;
private static long pauseOffset;
private PauseAwareTimer()
{
}
public static long getTime()
{
return (paused ? pauseTime : Util.getNanos()) - pauseOffset;
}
@SubscribeEvent
public static void tick( TickEvent.RenderTickEvent event )
{
if( event.phase != TickEvent.Phase.START ) return;
boolean isPaused = Minecraft.getInstance().isPaused();
if( isPaused == paused ) return;
if( isPaused )
{
pauseTime = Util.getNanos();
paused = true;
}
else
{
pauseOffset += Util.getNanos() - pauseTime;
paused = false;
}
}
}

View File

@@ -7,3 +7,7 @@ public net.minecraft.client.gui.components.ChatComponent m_93787_(Lnet/minecraft
public net.minecraft.client.gui.components.ChatComponent m_93803_(I)V # removeById
# NoTermComputerScreen
public net.minecraft.client.Minecraft f_91080_ # screen
# SpeakerInstance/SpeakerManager
public com.mojang.blaze3d.audio.Channel m_83652_(I)V # pumpBuffers
public net.minecraft.client.sounds.SoundEngine f_120223_ # executor

View File

@@ -0,0 +1,10 @@
{
"speaker.dfpwm_fake_audio_should_not_be_played": {
"sounds": [
{
"name": "computercraft:empty",
"stream": true
}
]
}
}

View File

@@ -0,0 +1,13 @@
{
"minVersion": "0.8",
"required": true,
"compatibilityLevel": "JAVA_8",
"refmap": "computercraft.mixins.refmap.json",
"package": "dan200.computercraft.mixin",
"client": [
"BlockRenderDispatcherMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -1,16 +1,18 @@
--- The Disk API allows you to interact with disk drives.
--
-- These functions can operate on locally attached or remote disk drives. To use
-- a locally attached drive, specify “side” as one of the six sides
-- (e.g. `left`); to use a remote disk drive, specify its name as printed when
-- enabling its modem (e.g. `drive_0`).
--
-- **Note:** All computers (except command computers), turtles and pocket
-- computers can be placed within a disk drive to access it's internal storage
-- like a disk.
--
-- @module disk
-- @since 1.2
--[[- The Disk API allows you to interact with disk drives.
These functions can operate on locally attached or remote disk drives. To use a
locally attached drive, specify “side” as one of the six sides (e.g. `left`); to
use a remote disk drive, specify its name as printed when enabling its modem
(e.g. `drive_0`).
:::tip
All computers (except command computers), turtles and pocket computers can be
placed within a disk drive to access it's internal storage like a disk.
:::
@module disk
@since 1.2
]]
local function isDrive(name)
if type(name) ~= "string" then

View File

@@ -1,27 +1,30 @@
--- The GPS API provides a method for turtles and computers to retrieve their
-- own locations.
--
-- It broadcasts a PING message over @{rednet} and wait for responses. In order
-- for this system to work, there must be at least 4 computers used as gps hosts
-- which will respond and allow trilateration. Three of these hosts should be in
-- a plane, and the fourth should be either above or below the other three. The
-- three in a plane should not be in a line with each other. You can set up
-- hosts using the gps program.
--
-- **Note**: When entering in the coordinates for the host you need to put in
-- the `x`, `y`, and `z` coordinates of the computer, not the modem, as all
-- rednet distances are measured from the block the computer is in.
--
-- Also note that you may choose which axes x, y, or z refers to - so long as
-- your systems have the same definition as any GPS servers that're in range, it
-- works just the same. For example, you might build a GPS cluster according to
-- [this tutorial][1], using z to account for height, or you might use y to
-- account for height in the way that Minecraft's debug screen displays.
--
-- [1]: http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/
--
-- @module gps
-- @since 1.31
--[[- The GPS API provides a method for turtles and computers to retrieve their
own locations.
It broadcasts a PING message over @{rednet} and wait for responses. In order for
this system to work, there must be at least 4 computers used as gps hosts which
will respond and allow trilateration. Three of these hosts should be in a plane,
and the fourth should be either above or below the other three. The three in a
plane should not be in a line with each other. You can set up hosts using the
gps program.
:::note
When entering in the coordinates for the host you need to put in the `x`, `y`,
and `z` coordinates of the computer, not the modem, as all modem distances are
measured from the block the computer is in.
:::
Also note that you may choose which axes x, y, or z refers to - so long as your
systems have the same definition as any GPS servers that're in range, it works
just the same. For example, you might build a GPS cluster according to [this
tutorial][1], using z to account for height, or you might use y to account for
height in the way that Minecraft's debug screen displays.
[1]: http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/
@module gps
@since 1.31
]]
local expect = dofile("rom/modules/main/cc/expect.lua").expect

View File

@@ -2,13 +2,13 @@
Functions are not actually executed simultaniously, but rather this API will
automatically switch between them whenever they yield (eg whenever they call
@{coroutine.yield}, or functions that call that - eg `os.pullEvent` - or
@{coroutine.yield}, or functions that call that - eg @{os.pullEvent} - or
functions that call that, etc - basically, anything that causes the function
to "pause").
Each function executed in "parallel" gets its own copy of the event queue,
and so "event consuming" functions (again, mostly anything that causes the
script to pause - eg `sleep`, `rednet.receive`, most of the `turtle` API,
script to pause - eg @{os.sleep}, @{rednet.receive}, most of the @{turtle} API,
etc) can safely be used in one without affecting the event queue accessed by
the other.

View File

@@ -28,9 +28,11 @@ Once you have the name of a peripheral, you can call functions on it using the
@{peripheral.call} function. This takes the name of our peripheral, the name of
the function we want to call, and then its arguments.
> Some bits of the peripheral API call peripheral functions *methods* instead
> (for example, the @{peripheral.getMethods} function). Don't worry, they're the
> same thing!
:::info
Some bits of the peripheral API call peripheral functions *methods* instead
(for example, the @{peripheral.getMethods} function). Don't worry, they're the
same thing!
:::
Let's say we have a monitor above our computer (and so "top") and want to
@{monitor.write|write some text to it}. We'd write the following:
@@ -64,7 +66,7 @@ type.
What is a peripheral type though? This is a string which describes what a
peripheral is, and so what functions are available on it. For instance, speakers
are just called `"speaker"`, and monitors `"monitor"`. Some peripherals might
have more than one type; a Minecraft chest is both a `"minecraft:chest"` and
have more than one type - a Minecraft chest is both a `"minecraft:chest"` and
`"inventory"`.
You can get all the types a peripheral has with @{peripheral.getType}, and check

View File

@@ -1,21 +1,48 @@
--- The Rednet API allows systems to communicate between each other without
-- using redstone. It serves as a wrapper for the modem API, offering ease of
-- functionality (particularly in regards to repeating signals) with some
-- expense of fine control.
--
-- In order to send and receive data, a modem (either wired, wireless, or ender)
-- is required. The data reaches any possible destinations immediately after
-- sending it, but is range limited.
--
-- Rednet also allows you to use a "protocol" - simple string names indicating
-- what messages are about. Receiving systems may filter messages according to
-- their protocols, thereby automatically ignoring incoming messages which don't
-- specify an identical string. It's also possible to @{rednet.lookup|lookup}
-- which systems in the area use certain protocols, hence making it easier to
-- determine where given messages should be sent in the first place.
--
-- @module rednet
-- @since 1.2
--[[- The Rednet API allows computers to communicate between each other by using
@{modem|modems}. It provides a layer of abstraction on top of the main @{modem}
peripheral, making it slightly easier to use.
## Basic usage
In order to send a message between two computers, each computer must have a
modem on one of its sides (or in the case of pocket computers and turtles, the
modem must be equipped as an upgrade). The two computers should then call
@{rednet.open}, which sets up the modems ready to send and receive messages.
Once rednet is opened, you can send messages using @{rednet.send} and receive
them using @{rednet.receive}. It's also possible to send a message to _every_
rednet-using computer using @{rednet.broadcast}.
:::caution Network security
While rednet provides a friendly way to send messages to specific computers, it
doesn't provide any guarantees about security. Other computers could be
listening in to your messages, or even pretending to send messages from other computers!
If you're playing on a multi-player server (or at least one where you don't
trust other players), it's worth encrypting or signing your rednet messages.
:::
## Protocols and hostnames
Several rednet messages accept "protocol"s - simple string names describing what
a message is about. When sending messages using @{rednet.send} and
@{rednet.broadcast}, you can optionally specify a protocol for the message. This
same protocol can then be given to @{rednet.receive}, to ignore all messages not
using this protocol.
It's also possible to look-up computers based on protocols, providing a basic
system for service discovery and [DNS]. A computer can advertise that it
supports a particular protocol with @{rednet.host}, also providing a friendly
"hostname". Other computers may then find all computers which support this
protocol using @{rednet.lookup}.
[DNS]: https://en.wikipedia.org/wiki/Domain_Name_System "Domain Name System"
@module rednet
@since 1.2
@see rednet_message Queued when a rednet message is received.
@see modem Rednet is built on top of the modem peripheral. Modems provide a more
bare-bones but flexible interface.
]]
local expect = dofile("rom/modules/main/cc/expect.lua").expect
@@ -29,9 +56,9 @@ CHANNEL_REPEAT = 65533
-- greater or equal to this limit wrap around to 0.
MAX_ID_CHANNELS = 65500
local tReceivedMessages = {}
local tHostnames = {}
local nClearTimer
local received_messages = {}
local hostnames = {}
local prune_received_timer
local function id_as_channel(id)
return (id or os.getComputerID()) % MAX_ID_CHANNELS
@@ -46,9 +73,17 @@ This will open the modem on two channels: one which has the same
@tparam string modem The name of the modem to open.
@throws If there is no such modem with the given name
@usage Open a wireless modem on the back of the computer.
@usage Open rednet on the back of the computer, allowing you to send and receive
rednet messages using it.
rednet.open("back")
@usage Open rednet on all attached modems. This abuses the "filter" argument to
@{peripheral.find}.
peripheral.find("modem", rednet.open)
@see rednet.close
@see rednet.isOpen
]]
function open(modem)
expect(1, modem, "string")
@@ -65,6 +100,7 @@ end
-- @tparam[opt] string modem The side the modem exists on. If not given, all
-- open modems will be closed.
-- @throws If there is no such modem with the given name
-- @see rednet.open
function close(modem)
expect(1, modem, "string", "nil")
if modem then
@@ -90,6 +126,7 @@ end
-- modems will be checked.
-- @treturn boolean If the given modem is open.
-- @since 1.31
-- @see rednet.open
function isOpen(modem)
expect(1, modem, "string", "nil")
if modem then
@@ -109,16 +146,18 @@ function isOpen(modem)
end
--[[- Allows a computer or turtle with an attached modem to send a message
intended for a system with a specific ID. At least one such modem must first
intended for a sycomputer with a specific ID. At least one such modem must first
be @{rednet.open|opened} before sending is possible.
Assuming the target was in range and also had a correctly opened modem, it
may then use @{rednet.receive} to collect the message.
Assuming the target was in range and also had a correctly opened modem, the
target computer may then use @{rednet.receive} to collect the message.
@tparam number nRecipient The ID of the receiving computer.
@param message The message to send. This should not contain coroutines or
functions, as they will be converted to @{nil}.
@tparam[opt] string sProtocol The "protocol" to send this message under. When
@tparam number recipient The ID of the receiving computer.
@param message The message to send. Like with @{modem.transmit}, this can
contain any primitive type (numbers, booleans and strings) as well as
tables. Other types (like functions), as well as metatables, will not be
transmitted.
@tparam[opt] string protocol The "protocol" to send this message under. When
using @{rednet.receive} one can filter to only receive messages sent under a
particular protocol.
@treturn boolean If this message was successfully sent (i.e. if rednet is
@@ -131,41 +170,41 @@ actually _received_.
rednet.send(2, "Hello from rednet!")
]]
function send(nRecipient, message, sProtocol)
expect(1, nRecipient, "number")
expect(3, sProtocol, "string", "nil")
function send(recipient, message, protocol)
expect(1, recipient, "number")
expect(3, protocol, "string", "nil")
-- Generate a (probably) unique message ID
-- We could do other things to guarantee uniqueness, but we really don't need to
-- Store it to ensure we don't get our own messages back
local nMessageID = math.random(1, 2147483647)
tReceivedMessages[nMessageID] = os.clock() + 9.5
if not nClearTimer then nClearTimer = os.startTimer(10) end
local message_id = math.random(1, 2147483647)
received_messages[message_id] = os.clock() + 9.5
if not prune_received_timer then prune_received_timer = os.startTimer(10) end
-- Create the message
local nReplyChannel = id_as_channel()
local tMessage = {
nMessageID = nMessageID,
nRecipient = nRecipient,
local reply_channel = id_as_channel()
local message_wrapper = {
nMessageID = message_id,
nRecipient = recipient,
nSender = os.getComputerID(),
message = message,
sProtocol = sProtocol,
sProtocol = protocol,
}
local sent = false
if nRecipient == os.getComputerID() then
if recipient == os.getComputerID() then
-- Loopback to ourselves
os.queueEvent("rednet_message", os.getComputerID(), message, sProtocol)
os.queueEvent("rednet_message", os.getComputerID(), message_wrapper, protocol)
sent = true
else
-- Send on all open modems, to the target and to repeaters
if nRecipient ~= CHANNEL_BROADCAST then
nRecipient = id_as_channel(nRecipient)
if recipient ~= CHANNEL_BROADCAST then
recipient = id_as_channel(recipient)
end
for _, sModem in ipairs(peripheral.getNames()) do
if isOpen(sModem) then
peripheral.call(sModem, "transmit", nRecipient, nReplyChannel, tMessage)
peripheral.call(sModem, "transmit", CHANNEL_REPEAT, nReplyChannel, tMessage)
for _, modem in ipairs(peripheral.getNames()) do
if isOpen(modem) then
peripheral.call(modem, "transmit", recipient, reply_channel, message_wrapper)
peripheral.call(modem, "transmit", CHANNEL_REPEAT, reply_channel, message_wrapper)
sent = true
end
end
@@ -174,28 +213,31 @@ function send(nRecipient, message, sProtocol)
return sent
end
--- Broadcasts a string message over the predefined @{CHANNEL_BROADCAST}
-- channel. The message will be received by every device listening to rednet.
--
-- @param message The message to send. This should not contain coroutines or
-- functions, as they will be converted to @{nil}.
-- @tparam[opt] string sProtocol The "protocol" to send this message under. When
-- using @{rednet.receive} one can filter to only receive messages sent under a
-- particular protocol.
-- @see rednet.receive
-- @changed 1.6 Added protocol parameter.
function broadcast(message, sProtocol)
expect(2, sProtocol, "string", "nil")
send(CHANNEL_BROADCAST, message, sProtocol)
--[[- Broadcasts a string message over the predefined @{CHANNEL_BROADCAST}
channel. The message will be received by every device listening to rednet.
@param message The message to send. This should not contain coroutines or
functions, as they will be converted to @{nil}. @tparam[opt] string protocol
The "protocol" to send this message under. When using @{rednet.receive} one can
filter to only receive messages sent under a particular protocol.
@see rednet.receive
@changed 1.6 Added protocol parameter.
@usage Broadcast the words "Hello, world!" to every computer using rednet.
rednet.broadcast("Hello, world!")
]]
function broadcast(message, protocol)
expect(2, protocol, "string", "nil")
send(CHANNEL_BROADCAST, message, protocol)
end
--[[- Wait for a rednet message to be received, or until `nTimeout` seconds have
elapsed.
@tparam[opt] string sProtocolFilter The protocol the received message must be
@tparam[opt] string protocol_filter The protocol the received message must be
sent with. If specified, any messages not sent under this protocol will be
discarded.
@tparam[opt] number nTimeout The number of seconds to wait if no message is
@tparam[opt] number timeout The number of seconds to wait if no message is
received.
@treturn[1] number The computer which sent this message
@return[1] The received message
@@ -227,34 +269,34 @@ received.
print(message)
]]
function receive(sProtocolFilter, nTimeout)
function receive(protocol_filter, timeout)
-- The parameters used to be ( nTimeout ), detect this case for backwards compatibility
if type(sProtocolFilter) == "number" and nTimeout == nil then
sProtocolFilter, nTimeout = nil, sProtocolFilter
if type(protocol_filter) == "number" and timeout == nil then
protocol_filter, timeout = nil, protocol_filter
end
expect(1, sProtocolFilter, "string", "nil")
expect(2, nTimeout, "number", "nil")
expect(1, protocol_filter, "string", "nil")
expect(2, timeout, "number", "nil")
-- Start the timer
local timer = nil
local sFilter = nil
if nTimeout then
timer = os.startTimer(nTimeout)
sFilter = nil
local event_filter = nil
if timeout then
timer = os.startTimer(timeout)
event_filter = nil
else
sFilter = "rednet_message"
event_filter = "rednet_message"
end
-- Wait for events
while true do
local sEvent, p1, p2, p3 = os.pullEvent(sFilter)
if sEvent == "rednet_message" then
local event, p1, p2, p3 = os.pullEvent(event_filter)
if event == "rednet_message" then
-- Return the first matching rednet_message
local nSenderID, message, sProtocol = p1, p2, p3
if sProtocolFilter == nil or sProtocol == sProtocolFilter then
return nSenderID, message, sProtocol
local sender_id, message, protocol = p1, p2, p3
if protocol_filter == nil or protocol == protocol_filter then
return sender_id, message, protocol
end
elseif sEvent == "timer" then
elseif event == "timer" then
-- Return nil if we timeout
if p1 == timer then
return nil
@@ -263,86 +305,103 @@ function receive(sProtocolFilter, nTimeout)
end
end
--- Register the system as "hosting" the desired protocol under the specified
-- name. If a rednet @{rednet.lookup|lookup} is performed for that protocol (and
-- maybe name) on the same network, the registered system will automatically
-- respond via a background process, hence providing the system performing the
-- lookup with its ID number.
--
-- Multiple computers may not register themselves on the same network as having
-- the same names against the same protocols, and the title `localhost` is
-- specifically reserved. They may, however, share names as long as their hosted
-- protocols are different, or if they only join a given network after
-- "registering" themselves before doing so (eg while offline or part of a
-- different network).
--
-- @tparam string sProtocol The protocol this computer provides.
-- @tparam string sHostname The name this protocol exposes for the given protocol.
-- @throws If trying to register a hostname which is reserved, or currently in use.
-- @see rednet.unhost
-- @see rednet.lookup
-- @since 1.6
function host(sProtocol, sHostname)
expect(1, sProtocol, "string")
expect(2, sHostname, "string")
if sHostname == "localhost" then
--[[- Register the system as "hosting" the desired protocol under the specified
name. If a rednet @{rednet.lookup|lookup} is performed for that protocol (and
maybe name) on the same network, the registered system will automatically
respond via a background process, hence providing the system performing the
lookup with its ID number.
Multiple computers may not register themselves on the same network as having the
same names against the same protocols, and the title `localhost` is specifically
reserved. They may, however, share names as long as their hosted protocols are
different, or if they only join a given network after "registering" themselves
before doing so (eg while offline or part of a different network).
@tparam string protocol The protocol this computer provides.
@tparam string hostname The name this protocol exposes for the given protocol.
@throws If trying to register a hostname which is reserved, or currently in use.
@see rednet.unhost
@see rednet.lookup
@since 1.6
]]
function host(protocol, hostname)
expect(1, protocol, "string")
expect(2, hostname, "string")
if hostname == "localhost" then
error("Reserved hostname", 2)
end
if tHostnames[sProtocol] ~= sHostname then
if lookup(sProtocol, sHostname) ~= nil then
if hostnames[protocol] ~= hostname then
if lookup(protocol, hostname) ~= nil then
error("Hostname in use", 2)
end
tHostnames[sProtocol] = sHostname
hostnames[protocol] = hostname
end
end
--- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer
-- respond to @{rednet.lookup} requests.
--
-- @tparam string sProtocol The protocol to unregister your self from.
-- @tparam string protocol The protocol to unregister your self from.
-- @since 1.6
function unhost(sProtocol)
expect(1, sProtocol, "string")
tHostnames[sProtocol] = nil
function unhost(protocol)
expect(1, protocol, "string")
hostnames[protocol] = nil
end
--- Search the local rednet network for systems @{rednet.host|hosting} the
-- desired protocol and returns any computer IDs that respond as "registered"
-- against it.
--
-- If a hostname is specified, only one ID will be returned (assuming an exact
-- match is found).
--
-- @tparam string sProtocol The protocol to search for.
-- @tparam[opt] string sHostname The hostname to search for.
--
-- @treturn[1] { number }|nil A list of computer IDs hosting the given
-- protocol, or @{nil} if none exist.
-- @treturn[2] number|nil The computer ID with the provided hostname and protocol,
-- or @{nil} if none exists.
-- @since 1.6
function lookup(sProtocol, sHostname)
expect(1, sProtocol, "string")
expect(2, sHostname, "string", "nil")
--[[- Search the local rednet network for systems @{rednet.host|hosting} the
desired protocol and returns any computer IDs that respond as "registered"
against it.
If a hostname is specified, only one ID will be returned (assuming an exact
match is found).
@tparam string protocol The protocol to search for.
@tparam[opt] string hostname The hostname to search for.
@treturn[1] number... A list of computer IDs hosting the given protocol.
@treturn[2] number|nil The computer ID with the provided hostname and protocol,
or @{nil} if none exists.
@since 1.6
@usage Find all computers which are hosting the `"chat"` protocol.
local computers = {rednet.lookup("chat")}
print(#computers .. " computers available to chat")
for _, computer in pairs(computers) do
print("Computer #" .. computer)
end
@usage Find a computer hosting the `"chat"` protocol with a hostname of `"my_host"`.
local id = rednet.lookup("chat", "my_host")
if id then
print("Found my_host at computer #" .. id)
else
printError("Cannot find my_host")
end
]]
function lookup(protocol, hostname)
expect(1, protocol, "string")
expect(2, hostname, "string", "nil")
-- Build list of host IDs
local tResults = nil
if sHostname == nil then
tResults = {}
local results = nil
if hostname == nil then
results = {}
end
-- Check localhost first
if tHostnames[sProtocol] then
if sHostname == nil then
table.insert(tResults, os.getComputerID())
elseif sHostname == "localhost" or sHostname == tHostnames[sProtocol] then
if hostnames[protocol] then
if hostname == nil then
table.insert(results, os.getComputerID())
elseif hostname == "localhost" or hostname == hostnames[protocol] then
return os.getComputerID()
end
end
if not isOpen() then
if tResults then
return table.unpack(tResults)
if results then
return table.unpack(results)
end
return nil
end
@@ -350,8 +409,8 @@ function lookup(sProtocol, sHostname)
-- Broadcast a lookup packet
broadcast({
sType = "lookup",
sProtocol = sProtocol,
sHostname = sHostname,
sProtocol = protocol,
sHostname = hostname,
}, "dns")
-- Start a timer
@@ -362,30 +421,28 @@ function lookup(sProtocol, sHostname)
local event, p1, p2, p3 = os.pullEvent()
if event == "rednet_message" then
-- Got a rednet message, check if it's the response to our request
local nSenderID, tMessage, sMessageProtocol = p1, p2, p3
if sMessageProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup response" then
if tMessage.sProtocol == sProtocol then
if sHostname == nil then
table.insert(tResults, nSenderID)
elseif tMessage.sHostname == sHostname then
return nSenderID
local sender_id, message, message_protocol = p1, p2, p3
if message_protocol == "dns" and type(message) == "table" and message.sType == "lookup response" then
if message.sProtocol == protocol then
if hostname == nil then
table.insert(results, sender_id)
elseif message.sHostname == hostname then
return sender_id
end
end
end
else
elseif event == "timer" and p1 == timer then
-- Got a timer event, check it's the end of our timeout
if p1 == timer then
break
end
break
end
end
if tResults then
return table.unpack(tResults)
if results then
return table.unpack(results)
end
return nil
end
local bRunning = false
local started = false
--- Listen for modem messages and converts them into rednet messages, which may
-- then be @{receive|received}.
@@ -393,51 +450,51 @@ local bRunning = false
-- This is automatically started in the background on computer startup, and
-- should not be called manually.
function run()
if bRunning then
if started then
error("rednet is already running", 2)
end
bRunning = true
started = true
while bRunning do
local sEvent, p1, p2, p3, p4 = os.pullEventRaw()
if sEvent == "modem_message" then
while true do
local event, p1, p2, p3, p4 = os.pullEventRaw()
if event == "modem_message" then
-- Got a modem message, process it and add it to the rednet event queue
local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4
if nChannel == id_as_channel() or nChannel == CHANNEL_BROADCAST then
if type(tMessage) == "table" and type(tMessage.nMessageID) == "number"
and tMessage.nMessageID == tMessage.nMessageID and not tReceivedMessages[tMessage.nMessageID]
and ((tMessage.nRecipient and tMessage.nRecipient == os.getComputerID()) or nChannel == CHANNEL_BROADCAST)
and isOpen(sModem)
local modem, channel, reply_channel, message = p1, p2, p3, p4
if channel == id_as_channel() or channel == CHANNEL_BROADCAST then
if type(message) == "table" and type(message.nMessageID) == "number"
and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID]
and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST)
and isOpen(modem)
then
tReceivedMessages[tMessage.nMessageID] = os.clock() + 9.5
if not nClearTimer then nClearTimer = os.startTimer(10) end
os.queueEvent("rednet_message", tMessage.nSender or nReplyChannel, tMessage.message, tMessage.sProtocol)
received_messages[message.nMessageID] = os.clock() + 9.5
if not prune_received_timer then prune_received_timer = os.startTimer(10) end
os.queueEvent("rednet_message", message.nSender or reply_channel, message.message, message.sProtocol)
end
end
elseif sEvent == "rednet_message" then
elseif event == "rednet_message" then
-- Got a rednet message (queued from above), respond to dns lookup
local nSenderID, tMessage, sProtocol = p1, p2, p3
if sProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup" then
local sHostname = tHostnames[tMessage.sProtocol]
if sHostname ~= nil and (tMessage.sHostname == nil or tMessage.sHostname == sHostname) then
rednet.send(nSenderID, {
local sender, message, protocol = p1, p2, p3
if protocol == "dns" and type(message) == "table" and message.sType == "lookup" then
local hostname = hostnames[message.sProtocol]
if hostname ~= nil and (message.sHostname == nil or message.sHostname == hostname) then
send(sender, {
sType = "lookup response",
sHostname = sHostname,
sProtocol = tMessage.sProtocol,
sHostname = hostname,
sProtocol = message.sProtocol,
}, "dns")
end
end
elseif sEvent == "timer" and p1 == nClearTimer then
-- Got a timer event, use it to clear the event queue
nClearTimer = nil
local nNow, bHasMore = os.clock(), nil
for nMessageID, nDeadline in pairs(tReceivedMessages) do
if nDeadline <= nNow then tReceivedMessages[nMessageID] = nil
else bHasMore = true end
elseif event == "timer" and p1 == prune_received_timer then
-- Got a timer event, use it to prune the set of received messages
prune_received_timer = nil
local now, has_more = os.clock(), nil
for message_id, deadline in pairs(received_messages) do
if deadline <= now then received_messages[message_id] = nil
else has_more = true end
end
nClearTimer = bHasMore and os.startTimer(10)
prune_received_timer = has_more and os.startTimer(10)
end
end
end

View File

@@ -59,7 +59,8 @@ end
-- @treturn Redirect The current terminal redirect
-- @since 1.6
-- @usage
-- Create a new @{window} which draws to the current redirect target
-- Create a new @{window} which draws to the current redirect target.
--
-- window.create(term.current(), 1, 1, 10, 10)
term.current = function()
return redirectTarget

View File

@@ -108,39 +108,48 @@ local function makePagedScroll(_term, _nFreeLines)
end
end
--- Prints a given string to the display.
--
-- If the action can be completed without scrolling, it acts much the same as
-- @{print}; otherwise, it will throw up a "Press any key to continue" prompt at
-- the bottom of the display. Each press will cause it to scroll down and write
-- a single line more before prompting again, if need be.
--
-- @tparam string _sText The text to print to the screen.
-- @tparam[opt] number _nFreeLines The number of lines which will be
-- automatically scrolled before the first prompt appears (meaning _nFreeLines +
-- 1 lines will be printed). This can be set to the terminal's height - 2 to
-- always try to fill the screen. Defaults to 0, meaning only one line is
-- displayed before prompting.
-- @treturn number The number of lines printed.
-- @usage
-- local width, height = term.getSize()
-- textutils.pagedPrint(("This is a rather verbose dose of repetition.\n"):rep(30), height - 2)
function pagedPrint(_sText, _nFreeLines)
expect(2, _nFreeLines, "number", "nil")
--[[- Prints a given string to the display.
If the action can be completed without scrolling, it acts much the same as
@{print}; otherwise, it will throw up a "Press any key to continue" prompt at
the bottom of the display. Each press will cause it to scroll down and write a
single line more before prompting again, if need be.
@tparam string text The text to print to the screen.
@tparam[opt] number free_lines The number of lines which will be
automatically scrolled before the first prompt appears (meaning free_lines +
1 lines will be printed). This can be set to the cursor's y position - 2 to
always try to fill the screen. Defaults to 0, meaning only one line is
displayed before prompting.
@treturn number The number of lines printed.
@usage Generates several lines of text and then prints it, paging once the
bottom of the terminal is reached.
local lines = {}
for i = 1, 30 do lines[i] = ("This is line #%d"):format(i) end
local message = table.concat(lines, "\n")
local width, height = term.getCursorPos()
textutils.pagedPrint(message, height - 2)
]]
function pagedPrint(text, free_lines)
expect(2, free_lines, "number", "nil")
-- Setup a redirector
local oldTerm = term.current()
local newTerm = {}
for k, v in pairs(oldTerm) do
newTerm[k] = v
end
newTerm.scroll = makePagedScroll(oldTerm, _nFreeLines)
newTerm.scroll = makePagedScroll(oldTerm, free_lines)
term.redirect(newTerm)
-- Print the text
local result
local ok, err = pcall(function()
if _sText ~= nil then
result = print(_sText)
if text ~= nil then
result = print(text)
else
result = print()
end
@@ -214,32 +223,45 @@ local function tabulateCommon(bPaged, ...)
end
end
--- Prints tables in a structured form.
--
-- This accepts multiple arguments, either a table or a number. When
-- encountering a table, this will be treated as a table row, with each column
-- width being auto-adjusted.
--
-- When encountering a number, this sets the text color of the subsequent rows to it.
--
-- @tparam {string...}|number ... The rows and text colors to display.
-- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" })
-- @since 1.3
--[[- Prints tables in a structured form.
This accepts multiple arguments, either a table or a number. When
encountering a table, this will be treated as a table row, with each column
width being auto-adjusted.
When encountering a number, this sets the text color of the subsequent rows to it.
@tparam {string...}|number ... The rows and text colors to display.
@since 1.3
@usage
textutils.tabulate(
colors.orange, { "1", "2", "3" },
colors.lightBlue, { "A", "B", "C" }
)
]]
function tabulate(...)
return tabulateCommon(false, ...)
end
--- Prints tables in a structured form, stopping and prompting for input should
-- the result not fit on the terminal.
--
-- This functions identically to @{textutils.tabulate}, but will prompt for user
-- input should the whole output not fit on the display.
--
-- @tparam {string...}|number ... The rows and text colors to display.
-- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" })
-- @see textutils.tabulate
-- @see textutils.pagedPrint
-- @since 1.3
--[[- Prints tables in a structured form, stopping and prompting for input should
the result not fit on the terminal.
This functions identically to @{textutils.tabulate}, but will prompt for user
input should the whole output not fit on the display.
@tparam {string...}|number ... The rows and text colors to display.
@see textutils.tabulate
@see textutils.pagedPrint
@since 1.3
@usage Generates a long table, tabulates it, and prints it to the screen.
local rows = {}
for i = 1, 30 do rows[i] = {("Row #%d"):format(i), math.random(1, 400)} end
textutils.tabulate(colors.orange, {"Column", "Value"}, colors.lightBlue, table.unpack(rows))
]]
function pagedTabulate(...)
return tabulateCommon(true, ...)
end
@@ -692,11 +714,11 @@ saving in a file or pretty-printing.
@throws If the object contains a value which cannot be
serialised. This includes functions and tables which appear multiple
times.
@see cc.pretty.pretty An alternative way to display a table, often more suitable for
pretty printing.
@see cc.pretty.pretty_print An alternative way to display a table, often more
suitable for pretty printing.
@since 1.3
@changed 1.97.0 Added `opts` argument.
@usage Pretty print a basic table.
@usage Serialise a basic table.
textutils.serialise({ 1, 2, 3, a = 1, ["another key"] = { true } })

View File

@@ -1,6 +1,4 @@
--- The turtle API allows you to control your turtle.
--
-- @module turtle
--- @module turtle
if not turtle then
error("Cannot load turtle API on computer", 2)
@@ -8,9 +6,8 @@ end
--- The builtin turtle API, without any generated helper functions.
--
-- Generally you should not need to use this table - it only exists for
-- backwards compatibility reasons.
-- @deprecated
-- @deprecated Historically this table behaved differently to the main turtle API, but this is no longer the base. You
-- should not need to use it.
native = turtle.native or turtle
local function addCraftMethod(object)

View File

@@ -1,3 +1,26 @@
# New features in CC: Tweaked 1.100.0
* Speakers can now play arbitrary PCM audio.
* Add support for encoding and decoding DFPWM streams, with the `cc.audio.dfpwm` module.
* Wired modems now only render breaking progress for the part which is being broken.
* Various documentation improvements.
Several bug fixes:
* Fix the "repeat" program not repeating broadcast rednet messages.
* Fix the drag-and-drop upload functionality writing empty files.
* Prevent turtles from pushing non-pushable entities.
# New features in CC: Tweaked 1.99.1
* Add package.searchpath to the cc.require API. (MCJack123)
* Provide a more efficient way for the Java API to consume Lua tables in certain restricted cases.
Several bug fixes:
* Fix keys being "sticky" when opening the off-hand pocket computer GUI.
* Correctly handle broken coroutine managers resuming Java code with a `nil` event.
* Prevent computer buttons stealing focus from the terminal.
* Fix a class cast exception when a monitor is malformed in ways I do not quite understand.
# New features in CC: Tweaked 1.99.0
* Pocket computers in their offhand will open without showing a terminal. You can look around and interact with the world, but your keyboard will be forwarded to the computer. (Wojbie, MagGen-hub).

View File

@@ -6,7 +6,7 @@ Uses LuaJ from http://luaj.sourceforge.net/
The ComputerCraft 1.76 update was sponsored by MinecraftU and Deep Space.
Visit http://www.minecraftu.org and http://www.deepspace.me/space-cadets to find out more.
Join the ComputerCraft community online at http://www.computercraft.info
Join the ComputerCraft community online at https://computercraft.cc
Follow @DanTwoHundred on Twitter!
To help contribute to ComputerCraft, browse the source code at https://github.com/dan200/ComputerCraft.

View File

@@ -0,0 +1,5 @@
The speaker program plays audio files using speakers attached to this computer.
## Examples:
- `speaker play example.dfpwm left` plays the "example.dfpwm" audio file using the speaker on the left of the computer.
- `speaker stop` stops any currently playing audio.

View File

@@ -1,32 +1,13 @@
New features in CC: Tweaked 1.99.0
New features in CC: Tweaked 1.100.0
* Pocket computers in their offhand will open without showing a terminal. You can look around and interact with the world, but your keyboard will be forwarded to the computer. (Wojbie, MagGen-hub).
* Peripherals can now have multiple types. `peripheral.getType` now returns multiple values, and `peripheral.hasType` checks if a peripheral has a specific type.
* Add several missing keys to the `keys` table. (ralphgod3)
* Add feature introduction/changed version information to the documentation. (MCJack123)
* Increase the file upload limit to 512KiB.
* Rednet can now handle computer IDs larger than 65535. (Ale32bit)
* Optimise deduplication of rednet messages (MCJack123)
* Make `term.blit` colours case insensitive. (Ocawesome101)
* Add a new `about` program for easier version identification. (MCJack123)
* Optimise peripheral calls in `rednet.run`. (xAnavrins)
* Add dimension parameter to `commands.getBlockInfo`.
* Add `cc.pretty.pretty_print` helper function (Lupus590).
* Add back JEI integration.
* Turtle and pocket computer upgrades can now be added and modified with data packs.
* Various translation updates (MORIMORI3017, Ale2Bit, mindy15963)
* Speakers can now play arbitrary PCM audio.
* Add support for encoding and decoding DFPWM streams, with the `cc.audio.dfpwm` module.
* Wired modems now only render breaking progress for the part which is being broken.
* Various documentation improvements.
And several bug fixes:
* Fix various computer commands failing when OP level was 4.
* Various documentation fixes. (xXTurnerLP, MCJack123)
* Fix `textutils.serialize` not serialising infinity and nan values. (Wojbie)
* Wired modems now correctly clean up mounts when a peripheral is detached.
* Fix incorrect turtle and pocket computer upgrade recipes in the recipe book.
* Fix speakers not playing sounds added via resource packs which are not registered in-game.
* Fix speaker upgrades sending packets after the server has stopped.
* Monitor sizing has been rewritten, hopefully making it more stable.
* Peripherals are now invalidated when the computer ticks, rather than when the peripheral changes.
* Fix printouts and pocket computers rendering at fullbright when in item frames.
* All mod blocks now have an effective tool (pickaxe).
Several bug fixes:
* Fix the "repeat" program not repeating broadcast rednet messages.
* Fix the drag-and-drop upload functionality writing empty files.
* Prevent turtles from pushing non-pushable entities.
Type "help changelog" to see the full version history.

View File

@@ -0,0 +1,228 @@
--[[-
Provides utilities for converting between streams of DFPWM audio data and a list of amplitudes.
DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact
format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode
in real time.
Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a
string, and converted a format suitable for @{speaker.playAudio}.
## Encoding and decoding files
This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder.
The returned encoder/decoder is itself a function, which converts between the two kinds of data.
These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for
a specific audio stream. Typically you will want to create a decoder for each stream of audio you read, and an encoder
for each one you write.
## Converting audio to DFPWM
DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it.
Instead, you can convert audio files online using [music.madefor.cc] or with the [LionRay Wav Converter][LionRay] Java
application.
[music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked"
[LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter "
@see guide!speaker_audio Gives a more general introduction to audio processing and the speaker.
@see speaker.playAudio To play the decoded audio data.
@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
```lua
local dfpwm = require("cc.audio.dfpwm")
local encoder = dfpwm.make_encoder()
local decoder = dfpwm.make_decoder()
local out = fs.open("speedy.dfpwm", "wb")
for input in io.lines("data/example.dfpwm", 16 * 1024 * 2) do
local decoded = decoder(input)
local output = {}
-- Read two samples at once and take the average.
for i = 1, #decoded, 2 do
local value_1, value_2 = decoded[i], decoded[i + 1]
output[(i + 1) / 2] = (value_1 + value_2) / 2
end
out.write(encoder(output))
sleep(0) -- This program takes a while to run, so we need to make sure we yield.
end
out.close()
```
]]
local expect = require "cc.expect".expect
local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift
local PREC = 10
local PREC_POW = 2 ^ PREC
local PREC_POW_HALF = 2 ^ (PREC - 1)
local STRENGTH_MIN = 2 ^ (PREC - 8 + 1)
local function make_predictor()
local charge, strength, previous_bit = 0, 0, false
return function(current_bit)
local target = current_bit and 127 or -128
local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW)
if next_charge == charge and next_charge ~= target then
next_charge = next_charge + (current_bit and 1 or -1)
end
local z = current_bit == previous_bit and PREC_POW - 1 or 0
local next_strength = strength
if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end
if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end
charge, strength, previous_bit = next_charge, next_strength, current_bit
return charge
end
end
--[[- Create a new encoder for converting PCM audio data into DFPWM.
The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and
returns the encoded DFPWM data.
:::caution Reusing encoders
Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple
streams, or use different encoders for the same stream, the resulting audio may not sound correct.
:::
@treturn function(pcm: { number... }):string The encoder function
@see encode A helper function for encoding an entire file of audio at once.
]]
local function make_encoder()
local predictor = make_predictor()
local previous_charge = 0
return function(input)
expect(1, input, "table")
local output, output_n = {}, 0
for i = 1, #input, 8 do
local this_byte = 0
for j = 0, 7 do
local inp_charge = floor(input[i + j] or 0)
if inp_charge > 127 or inp_charge < -128 then
error(("Amplitude at position %d was %d, but should be between -128 and 127"):format(i + j, inp_charge), 2)
end
local current_bit = inp_charge > previous_charge or (inp_charge == previous_charge and inp_charge == 127)
this_byte = floor(this_byte / 2) + (current_bit and 128 or 0)
previous_charge = predictor(current_bit)
end
output_n = output_n + 1
output[output_n] = char(this_byte)
end
return table.concat(output, "", 1, output_n)
end
end
--[[- Create a new decoder for converting DFPWM into PCM audio data.
The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value
between -128 and 127.
:::caution Reusing decoders
Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple
streams, or use different decoders for the same stream, the resulting audio may not sound correct.
:::
@treturn function(dfpwm: string):{ number... } The encoder function
@see decode A helper function for decoding an entire file of audio at once.
@usage Reads "data/example.dfpwm" in blocks of 16KiB (the speaker can accept a maximum of 128×1024 samples), decodes
them and then plays them through the speaker.
```lua {data-peripheral=speaker}
local dfpwm = require "cc.audio.dfpwm"
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
for input in io.lines("data/example.dfpwm", 16 * 1024) do
local decoded = decoder(input)
while not speaker.playAudio(decoded) do
os.pullEvent("speaker_audio_empty")
end
end
```
]]
local function make_decoder()
local predictor = make_predictor()
local low_pass_charge = 0
local previous_charge, previous_bit = 0, false
return function (input, output)
expect(1, input, "string")
local output, output_n = {}, 0
for i = 1, #input do
local input_byte = byte(input, i)
for _ = 1, 8 do
local current_bit = band(input_byte, 1) ~= 0
local charge = predictor(current_bit)
local antijerk = charge
if current_bit ~= previous_bit then
antijerk = floor((charge + previous_charge + 1) / 2)
end
previous_charge, previous_bit = charge, current_bit
low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256)
output_n = output_n + 1
output[output_n] = low_pass_charge
input_byte = rshift(input_byte, 1)
end
end
return output
end
end
--[[- A convenience function for decoding a complete file of audio at once.
This should only be used for short files. For larger files, one should read the file in chunks and process it using
@{make_decoder}.
@tparam string input The DFPWM data to convert.
@treturn { number... } The produced amplitude data.
@see make_decoder
]]
local function decode(input)
expect(1, input, "string")
return make_decoder()(input)
end
--[[- A convenience function for encoding a complete file of audio at once.
This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place,
you should use an encoder returned by @{make_encoder} instead.
@tparam { number... } input The table of amplitude data.
@treturn string The encoded DFPWM data.
@see make_encoder
]]
local function encode(input)
expect(1, input, "table")
return make_encoder()(input)
end
return {
make_encoder = make_encoder,
encode = encode,
make_decoder = make_decoder,
decode = decode,
}

View File

@@ -224,8 +224,8 @@ end
--- Display a document on the terminal.
--
-- @tparam Doc doc The document to render
-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
-- @tparam Doc doc The document to render
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
local function write(doc, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, ribbon_frac, "number", "nil")
@@ -286,8 +286,8 @@ end
--- Display a document on the terminal with a trailing new line.
--
-- @tparam Doc doc The document to render.
-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
-- @tparam Doc doc The document to render.
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
local function print(doc, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, ribbon_frac, "number", "nil")
@@ -297,10 +297,10 @@ end
--- Render a document, converting it into a string.
--
-- @tparam Doc doc The document to render.
-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to
-- fit this width - it is only used for finding the best layout.
-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
-- @tparam Doc doc The document to render.
-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to fit
-- this width - it is only used for finding the best layout.
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
-- @treturn string The rendered document as a string.
local function render(doc, width, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
@@ -483,7 +483,7 @@ Controls how various properties are displayed.
- `function_args`: Show the arguments to a function if known (`false` by default).
- `function_source`: Show where the function was defined, instead of
`function: xxxxxxxx` (`false` by default).
@tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
@tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
@usage Display a table on the screen.

View File

@@ -1,22 +1,24 @@
--- This provides a pure Lua implementation of the builtin @{require} function
-- and @{package} library.
--
-- Generally you do not need to use this module - it is injected into the
-- every program's environment. However, it may be useful when building a
-- custom shell or when running programs yourself.
--
-- @module cc.require
-- @since 1.88.0
-- @usage Construct the package and require function, and insert them into a
-- custom environment.
--
-- local r = require "cc.require"
-- local env = setmetatable({}, { __index = _ENV })
-- env.require, env.package = r.make(env, "/")
--
-- -- Now we have our own require function, separate to the original.
-- local r2 = env.require "cc.require"
-- print(r, r2)
--[[- This provides a pure Lua implementation of the builtin @{require} function
and @{package} library.
Generally you do not need to use this module - it is injected into the every
program's environment. However, it may be useful when building a custom shell or
when running programs yourself.
@module cc.require
@since 1.88.0
@see using_require For an introduction on how to use @{require}.
@usage Construct the package and require function, and insert them into a
custom environment.
local r = require "cc.require"
local env = setmetatable({}, { __index = _ENV })
env.require, env.package = r.make(env, "/")
-- Now we have our own require function, separate to the original.
local r2 = env.require "cc.require"
print(r, r2)
]]
local expect = require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")
local expect = expect.expect
@@ -31,22 +33,37 @@ local function preload(package)
end
end
local function from_file(package, env, dir)
local function from_file(package, env)
return function(name)
local fname = string.gsub(name, "%.", "/")
local sPath, sError = package.searchpath(name, package.path)
if not sPath then
return nil, sError
end
local fnFile, sError = loadfile(sPath, nil, env)
if fnFile then
return fnFile, sPath
else
return nil, sError
end
end
end
local function make_searchpath(dir)
return function(name, path, sep, rep)
expect(1, name, "string")
expect(2, path, "string")
sep = expect(3, sep, "string", "nil") or "."
rep = expect(4, rep, "string", "nil") or "/"
local fname = string.gsub(name, sep:gsub("%.", "%%%."), rep)
local sError = ""
for pattern in string.gmatch(package.path, "[^;]+") do
for pattern in string.gmatch(path, "[^;]+") do
local sPath = string.gsub(pattern, "%?", fname)
if sPath:sub(1, 1) ~= "/" then
sPath = fs.combine(dir, sPath)
end
if fs.exists(sPath) and not fs.isDir(sPath) then
local fnFile, sError = loadfile(sPath, nil, env)
if fnFile then
return fnFile, sPath
else
return nil, sError
end
return sPath
else
if #sError > 0 then
sError = sError .. "\n "
@@ -118,7 +135,8 @@ local function make_package(env, dir)
end
package.config = "/\n;\n?\n!\n-"
package.preload = {}
package.loaders = { preload(package), from_file(package, env, dir) }
package.loaders = { preload(package), from_file(package, env) }
package.searchpath = make_searchpath(dir)
return make_require(package), package
end

View File

@@ -0,0 +1,62 @@
local function get_speakers(name)
if name then
local speaker = peripheral.wrap(name)
if speaker == nil then
error(("Speaker %q does not exist"):format(name), 0)
return
elseif not peripheral.hasType(name, "speaker") then
error(("%q is not a speaker"):format(name), 0)
end
return { speaker }
else
local speakers = { peripheral.find("speaker") }
if #speakers == 0 then
error("No speakers attached", 0)
end
return speakers
end
end
local cmd = ...
if cmd == "stop" then
local _, name = ...
for _, speaker in pairs(get_speakers(name)) do speaker.stop() end
elseif cmd == "play" then
local _, file, name = ...
local speaker = get_speakers(name)[1]
local handle, err
if http and file:match("^https?://") then
print("Downloading...")
handle, err = http.get{ url = file, binary = true }
else
handle, err = fs.open(file, "rb")
end
if not handle then
printError("Could not play audio:")
error(err, 0)
end
print("Playing " .. file)
local decoder = require "cc.audio.dfpwm".make_decoder()
while true do
local chunk = handle.read(16 * 1024)
if not chunk then break end
local buffer = decoder(chunk)
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
handle.close()
else
local programName = arg[0] or fs.getName(shell.getRunningProgram())
print("Usage:")
print(programName .. " play <file or url> [speaker]")
print(programName .. " stop [speaker]")
end

View File

@@ -171,7 +171,7 @@ end
local current_section = nil
local offset = 0
--- Find the currently visible seciton, or nil if this document has no sections.
--- Find the currently visible section, or nil if this document has no sections.
--
-- This could potentially be a binary search, but right now it's not worth it.
local function find_section()

View File

@@ -14,10 +14,6 @@ else
print(#tModems .. " modems found.")
end
local function idAsChannel(id)
return (id or os.getComputerID()) % rednet.MAX_ID_CHANNELS
end
local function open(nChannel)
for n = 1, #tModems do
local sModem = tModems[n]
@@ -53,11 +49,16 @@ local ok, error = pcall(function()
tReceivedMessages[tMessage.nMessageID] = true
tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID
local recipient_channel = tMessage.nRecipient
if tMessage.nRecipient ~= rednet.CHANNEL_BROADCAST then
recipient_channel = recipient_channel % rednet.MAX_ID_CHANNELS
end
-- Send on all other open modems, to the target and to other repeaters
for n = 1, #tModems do
local sOtherModem = tModems[n]
peripheral.call(sOtherModem, "transmit", rednet.CHANNEL_REPEAT, nReplyChannel, tMessage)
peripheral.call(sOtherModem, "transmit", idAsChannel(tMessage.nRecipient), nReplyChannel, tMessage)
peripheral.call(sOtherModem, "transmit", recipient_channel, nReplyChannel, tMessage)
end
-- Log the event

View File

@@ -115,6 +115,18 @@ shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build(
{ completion.choice, { "play", "play ", "stop " } },
completion.peripheral
))
shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build(
{ completion.choice, { "play ", "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)
end
end,
function(shell, text, previous)
if previous[2] == "play" then return completion.peripheral(shell, text, previous, false)
end
end
))
shell.setCompletionFunction("rom/programs/fun/advanced/paint.lua", completion.build(completion.file))
shell.setCompletionFunction("rom/programs/http/pastebin.lua", completion.build(
{ completion.choice, { "put ", "get ", "run " } },

View File

@@ -97,7 +97,7 @@ public class ComputerTestDelegate
if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." );
Terminal term = new Terminal( 80, 30 );
Terminal term = new Terminal( 80, 100 );
IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 );
// Remove any existing files

View File

@@ -120,6 +120,13 @@ public class GeneratorTest
contramap( notNullValue(), "callback", MethodResult::getCallback ) );
}
@Test
public void testUnsafe()
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( Unsafe.class );
assertThat( methods, contains( named( "withUnsafe" ) ) );
}
public static class Basic
{
@LuaFunction
@@ -222,6 +229,21 @@ public class GeneratorTest
{}
}
public static class Unsafe
{
@LuaFunction( unsafe = true )
public final void withUnsafe( LuaTable<?, ?> table )
{}
@LuaFunction
public final void withoutUnsafe( LuaTable<?, ?> table )
{}
@LuaFunction( unsafe = true, mainThread = true )
public final void invalid( LuaTable<?, ?> table )
{}
}
private static <T> T find( Collection<NamedMethod<T>> methods, String name )
{
return methods.stream()

View File

@@ -0,0 +1,39 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.ObjectLuaTable;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class DfpwmStateTest
{
@Test
public void testEncoder() throws LuaException
{
int[] input = new int[] { 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3 };
Map<Object, Object> inputTbl = new HashMap<>();
for( int i = 0; i < input.length; i++ ) inputTbl.put( (double) (i + 1), input[i] );
DfpwmState state = new DfpwmState();
state.pushBuffer( new ObjectLuaTable( inputTbl ), input.length, Optional.empty() );
ByteBuffer result = state.pullPending( 0 );
byte[] contents = new byte[result.remaining()];
result.get( contents );
assertArrayEquals(
new byte[] { 87, 74, 42, -91, -92, -108, 84, -87, -86, 86, -83, 90, -83, -43, 90, -85, -42, 106, -43, -86, 106, -107, 42, -107, 74, -87, 74, -91, 74, -91, -86, -86, 106, 85, 107, -83, 106, -83, -83, 86, -75, -86, 42, 85, -107, 82, 41, -91, 82, 74, 41, -107, -86, -44, -86, 86, -75, 106, -83, -75, -86, -75, 90, -83, -86, -86, -86, 82, -91, 74, -107, -86, 82, -87, 82, 85, 85, 85, -83, 86, -75, -86, -43, 90, -83, 90, 85, 85, -107, 42, -91, 82, -86, 82, 74, 41, 85, -87, -86, -86, 106, -75, 90, -83, 86, -85, 106, -43, 106, 85, 85, 85, 85, -107, 42, 85, -86, 42, -107, -86, -86, -86, -86, 106, -75, -86, 86, -85 },
contents
);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -189,13 +189,18 @@ end
local expect_mt = {}
expect_mt.__index = expect_mt
function expect_mt:_fail(message)
if self._extra then message = self._extra .. "\n" .. message end
fail(message)
end
--- Assert that this expectation has the provided value
--
-- @param value The value to require this expectation to be equal to
-- @throws If the values are not equal
function expect_mt:equals(value)
if value ~= self.value then
fail(("Expected %s\n but got %s"):format(format(value), format(self.value)))
self:_fail(("Expected %s\n but got %s"):format(format(value), format(self.value)))
end
return self
@@ -209,7 +214,7 @@ expect_mt.eq = expect_mt.equals
-- @throws If the values are equal
function expect_mt:not_equals(value)
if value == self.value then
fail(("Expected any value but %s"):format(format(value)))
self:_fail(("Expected any value but %s"):format(format(value)))
end
return self
@@ -224,7 +229,7 @@ expect_mt.ne = expect_mt.not_equals
function expect_mt:type(exp_type)
local actual_type = type(self.value)
if exp_type ~= actual_type then
fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type))
self:_fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type))
end
return self
@@ -273,7 +278,7 @@ end
-- @throws If they are not equivalent
function expect_mt:same(value)
if not matches({}, true, self.value, value) then
fail(("Expected %s\nbut got %s"):format(format(value), format(self.value)))
self:_fail(("Expected %s\nbut got %s"):format(format(value), format(self.value)))
end
return self
@@ -286,7 +291,7 @@ end
-- @throws If this does not match the provided value
function expect_mt:matches(value)
if not matches({}, false, value, self.value) then
fail(("Expected %s\nto match %s"):format(format(self.value), format(value)))
self:_fail(("Expected %s\nto match %s"):format(format(self.value), format(value)))
end
return self
@@ -299,19 +304,19 @@ end
-- @throws If this function was not called the expected number of times.
function expect_mt:called(times)
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
fail(("Expected stubbed function, got %s"):format(type(self.value)))
self:_fail(("Expected stubbed function, got %s"):format(type(self.value)))
end
local called = #self.value.arguments
if times == nil then
if called == 0 then
fail("Expected stub to be called\nbut it was not.")
self:_fail("Expected stub to be called\nbut it was not.")
end
else
check('stub', 1, 'number', times)
if called ~= times then
fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called))
self:_fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called))
end
end
@@ -320,7 +325,7 @@ end
local function called_with_check(eq, self, ...)
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
fail(("Expected stubbed function, got %s"):format(type(self.value)))
self:_fail(("Expected stubbed function, got %s"):format(type(self.value)))
end
local exp_args = table.pack(...)
@@ -331,14 +336,14 @@ local function called_with_check(eq, self, ...)
local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args))
if #actual_args == 0 then
fail(head .. " not called at all")
self:_fail(head .. " not called at all")
elseif #actual_args == 1 then
fail(("%s called with %s."):format(head, format(actual_args[1])))
self:_fail(("%s called with %s."):format(head, format(actual_args[1])))
else
local lines = { head .. " called with:" }
for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end
fail(table.concat(lines, "\n"))
self:_fail(table.concat(lines, "\n"))
end
end
@@ -363,15 +368,24 @@ end
function expect_mt:str_match(pattern)
local actual_type = type(self.value)
if actual_type ~= "string" then
fail(("Expected value of type string\nbut got %s"):format(actual_type))
self:_fail(("Expected value of type string\nbut got %s"):format(actual_type))
end
if not self.value:find(pattern) then
fail(("Expected %q\n to match pattern %q"):format(self.value, pattern))
self:_fail(("Expected %q\n to match pattern %q"):format(self.value, pattern))
end
return self
end
--- Add extra information to this error message.
--
-- @tparam string message Additional message to prepend in the case of failures.
-- @return The current
function expect_mt:describe(message)
self._extra = tostring(message)
return self
end
local expect = {}
setmetatable(expect, expect)

View File

@@ -88,6 +88,7 @@ describe("The rednet library", function()
local fake_computer = require "support.fake_computer"
local debugx = require "support.debug_ext"
local function dawdle() while true do coroutine.yield() end end
local function computer_with_rednet(id, fn, options)
local computer = fake_computer.make_computer(id, function(env)
local fns = { env.rednet.run }
@@ -105,6 +106,10 @@ describe("The rednet library", function()
end
end
if options and options.host then
env.rednet.host("some_protocol", "host_" .. id)
end
return parallel.waitForAny(table.unpack(fns))
end)
local modem = fake_computer.add_modem(computer, "back")
@@ -203,8 +208,8 @@ describe("The rednet library", function()
env.sleep(10)
-- Ensure our pending message store is empty. Bit ugly to prod internals, but there's no other way.
expect(debugx.getupvalue(rednet.run, "tReceivedMessages")):same({})
expect(debugx.getupvalue(rednet.run, "nClearTimer")):eq(nil)
expect(debugx.getupvalue(rednet.run, "received_messages")):same({})
expect(debugx.getupvalue(rednet.run, "prune_received_timer")):eq(nil)
end, { open = true })
local computer_3, modem_3 = computer_with_rednet(3, nil, { open = true, rep = true })
@@ -222,5 +227,22 @@ describe("The rednet library", function()
fake_computer.advance_all(computers, 10)
fake_computer.run_all(computers, { computer_1, computer_2 })
end)
it("handles lookups between computers with massive IDs", function()
local id_1, id_3 = 24283947, 93428798
local computer_1, modem_1 = computer_with_rednet(id_1, function(rednet)
local ids = { rednet.lookup("some_protocol") }
expect(ids):same { id_3 }
end, { open = true })
local computer_2, modem_2 = computer_with_rednet(2, nil, { open = true, rep = true })
local computer_3, modem_3 = computer_with_rednet(id_3, dawdle, { open = true, host = true })
fake_computer.add_modem_edge(modem_1, modem_2)
fake_computer.add_modem_edge(modem_2, modem_3)
local computers = { computer_1, computer_2, computer_3 }
fake_computer.run_all(computers, false)
fake_computer.advance_all(computers, 3)
fake_computer.run_all(computers, { computer_1 })
end)
end)
end)

View File

@@ -0,0 +1,26 @@
describe("cc.audio.dfpwm", function()
local dfpwm = require "cc.audio.dfpwm"
describe("decode", function()
it("decodes some test data", function()
-- Look, I'm not proud of this.
local input = "\43\225\33\44\30\240\171\23\253\201\46\186\68\189\74\160\188\16\94\169\251\87\11\240\19\92\85\185\126\5\172\64\17\250\85\245\255\169\244\1\85\200\33\176\82\104\163\17\126\23\91\226\37\224\117\184\198\11\180\19\148\86\191\246\255\188\231\10\210\85\124\202\15\232\43\162\117\63\220\15\250\88\87\230\173\106\41\13\228\143\246\190\119\169\143\68\201\40\149\62\20\72\3\160\114\169\254\39\152\30\20\42\84\24\47\64\43\61\221\95\191\42\61\42\206\4\247\81"
local output = { 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0, -1, -2, -2, -1, 0, 1, 0, -1, -3, -5, -5, -5, -7, -9, -11, -11, -9, -9, -9, -9, -10, -12, -12, -10, -8, -6, -6, -8, -10, -12, -14, -16, -18, -17, -15, -12, -9, -6, -3, -2, -2, -2, -2, -2, -2, 0, 3, 6, 7, 7, 7, 4, 1, 1, 1, 1, 3, 5, 7, 9, 12, 15, 15, 12, 12, 12, 9, 9, 11, 12, 12, 14, 16, 17, 17, 17, 14, 11, 11, 11, 10, 12, 14, 14, 13, 13, 10, 9, 9, 7, 5, 4, 4, 4, 4, 4, 6, 8, 10, 10, 10, 10, 10, 10, 10, 9, 8, 8, 8, 7, 6, 4, 2, 0, 0, 0, 0, 0, -1, -1, 0, 1, 3, 3, 3, 3, 2, 0, -2, -2, -2, -3, -5, -7, -7, -5, -3, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, 0, 1, 1, 1, 2, 3, 4, 5, 6, 7, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 9, 8, 7, 6, 4, 2, 0, 0, 2, 4, 6, 8, 10, 10, 8, 7, 7, 5, 3, 1, -1, 0, 2, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 4, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 9, 9, 9, 9, 9, 8, 7, 6, 5, 3, 1, 1, 3, 3, 3, 3, 3, 3, 2, 1, 0, -1, -3, -3, -3, -3, -2, -3, -4, -4, -3, -4, -5, -6, -6, -5, -5, -4, -3, -2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 20, 17, 16, 16, 15, 15, 15, 15, 13, 13, 13, 13, 14, 15, 16, 18, 18, 16, 14, 12, 10, 8, 5, 5, 5, 4, 4, 4, 4, 4, 4, 2, 0, -2, -2, -2, -4, -4, -2, 0, 0, -2, -4, -6, -6, -6, -8, -10, -12, -14, -16, -15, -13, -12, -11, -11, -11, -11, -13, -13, -13, -13, -13, -14, -16, -18, -18, -18, -18, -16, -16, -16, -14, -13, -14, -15, -15, -14, -14, -12, -11, -12, -13, -13, -12, -13, -14, -15, -15, -13, -11, -9, -7, -5, -5, -5, -3, -1, -1, -1, -1, -3, -5, -5, -3, -3, -3, -1, -1, -1, -1, -3, -3, -3, -4, -6, -6, -4, -2, 0, 0, 0, 0, -2, -2, -2, -3, -5, -7, -9, -11, -13, -13, -11, -9, -7, -6, -6, -6, -6, -4, -2, -2, -4, -6, -8, -7, -5, -3, -2, -2, -2, -2, 0, 0, -2, -4, -4, -2, 0, 2, 2, 1, 1, -1, -3, -5, -7, -10, -10, -10, -10, -8, -7, -7, -5, -3, -2, -4, -4, -4, -6, -8, -10, -12, -12, -12, -12, -12, -14, -13, -13, -13, -11, -11, -11, -11, -11, -11, -11, -9, -7, -5, -3, -1, -1, -1, -1, -1, 1, 1, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 22, 19, 18, 20, 22, 24, 23, 22, 24, 26, 28, 27, 24, 23, 25, 28, 28, 28, 27, 26, 26, 23, 20, 17, 14, 14, 14, 11, 11, 11, 11, 13, 15, 16, 16, 16, 15, 15, 14, 14, 12, 10, 9, 11, 13, 15, 17, 17, 14, 13, 13, 12, 12, 10, 9, 11, 13, 15, 17, 19, 19, 16, 13, 10, 7, 4, 1, 1, 2, 2, 4, 7, 10, 13, 13, 13, 12, 12, 12, 9, 6, 6, 6, 3, 0, 0, 0, 0, 2, 3, 3, 3, 3, 5, 7, 7, 7, 9, 11, 13, 15, 18, 18, 15, 12, 9, 8, 10, 13, 13, 13, 15, 18, 21, 24, 27, 27, 23, 19, 15, 11, 10, 9, 9, 12, 16, 19, 22, 23, 19, 14, 13, 16, 16, 15, 15, 14, 17, 20, 20, 19, 19, 18, 17, 14, 13, 15, 15, 12, 11, 13, 16, 19, 19, 18, 20, 20, 19, 18, 18, 17, 17, 16, 16, 16, 15, 17, 17, 16, 16, 13, 12, 12, 11, 11, 9, 9, 9, 9, 11, 11, 9, 7, 5, 3, 1, 1, 1, -1, -1, 1, 3, 5, 7, 9, 11, 12, 9, 6, 6, 6, 6, 8, 8, 7, 9, 11, 13, 13, 12, 14, 16, 18, 20, 20, 20, 22, 24, 26, 25, 25, 27, 29, 28, 27, 26, 23, 22, 22, 21, 21, 20, 22, 24, 26, 28, 27, 24, 21, 21, 21, 18, 17, 17, 14, 11, 11, 11, 10, 10, 7, 6, 6, 4, 3, 5, 5, 3, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 3, 1, -1, -3, -3, -3, -3, -2, -3, -4, -6, -8, -10, -10, -10, -12, -12, -12, -12, -10, -10, -11, -12, -14, -16, -18, -20, -22, -24, -26, -28, -27, -27, -26, -26, -25, -25, -27, -26, -24, -22, -22, -22, -22, -24, -24, -24, -24, -23, -23, -22, -22, -21, -20, -19, -17, -15, -13, -11, -9, -7, -7, -9, -9, -9, -11, -13, -15, -17, -16, -14, -13, -15, -14, -14, -14, -12, -10, -8, -7, -9, -11, -13, -15, -14, -14, -13, -13, -15, -17, -19, -18, -18, -17, -17, -16, -16, -18, -20, -22, -21, -21, -21, -21, -21, -20, -21, -22, -24, -24, -22, -22, -24, -26, -25, -23, -21, -19, -18, -17, -17, -19, -21, -23, -25, -27, -29, -31, -30, -29, -28, -26, -25, -24, -24, -23, -23, -25, -24, -24, -24, -22, -20, -18, -18, -20, -20, -20, -20, -18, -16, -16, -16, -14, -12, -10, -8, -6, -4, -4, -4, -4, -4, -2, 0, 2, 4, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3, 3, 3, 4, 5, 6, 5, 3, 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, -1, -2, -3, -4, -4, -2, 0, 0, 0, 1, 3, 5, 7, 7, 5, 3, 3, 3, 3, 3 }
local decoded = dfpwm.decode(input)
expect(#decoded):describe("The lengths match"):eq(#output)
for i = 1, #decoded do expect(decoded[i]):describe("Item at #" .. i):eq(output[i]) end
end)
end)
describe("encode", function()
it("encodes some data", function()
local input = { 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3 }
local output = { 87, 74, 42, 165, 164, 148, 84, 169, 170, 86, 173, 90, 173, 213, 90, 171, 214, 106, 213, 170, 106, 149, 42, 149, 74, 169, 74, 165, 74, 165, 170, 170, 106, 85, 107, 173, 106, 173, 173, 86, 181, 170, 42, 85, 149, 82, 41, 165, 82, 74, 41, 149, 170, 212, 170, 86, 181, 106, 173, 181, 170, 181, 90, 173, 170, 170, 170, 82, 165, 74, 149, 170, 82, 169, 82, 85, 85, 85, 173, 86, 181, 170, 213, 90, 173, 90, 85, 85, 149, 42, 165, 82, 170, 82, 74, 41, 85, 169, 170, 170, 106, 181, 90, 173, 86, 171, 106, 213, 106, 85, 85, 85, 85, 149, 42, 85, 170, 42, 149, 170, 170, 170, 170, 106, 181, 170, 86, 171 }
local encoded = dfpwm.encode(input)
expect(#encoded):describe("The lengths match"):eq(#output)
for i = 1, #encoded do expect(encoded:byte(i)):describe("Item at #" .. i):eq(output[i] % 256) end
end)
end)
end)

View File

@@ -1,4 +1,4 @@
import { render, h, Component, Computer } from "copycat/embed";
import { render, h, Component, Computer, PeripheralKind } from "copycat/embed";
import type { ComponentChild } from "preact";
import settingsFile from "./mount/.settings";
@@ -6,6 +6,8 @@ import startupFile from "./mount/startup.lua";
import exprTemplate from "./mount/expr_template.lua";
import exampleNfp from "./mount/example.nfp";
import exampleNft from "./mount/example.nft";
import exampleAudioLicense from "./mount/example.dfpwm.LICENSE";
import exampleAudioUrl from "./mount/example.dfpwm";
const defaultFiles: { [filename: string]: string } = {
".settings": settingsFile,
@@ -21,18 +23,29 @@ const clamp = (value: number, min: number, max: number): number => {
return value;
}
const download = async (url: string): Promise<Uint8Array> => {
const result = await fetch(url);
if (result.status != 200) throw new Error(`${url} responded with ${result.status} ${result.statusText}`);
return new Uint8Array(await result.arrayBuffer());
};
let dfpwmAudio: Promise<Uint8Array> | null = null;
const Click = (options: { run: () => void }) =>
<button type="button" class="example-run" onClick={options.run}>Run </button>
type WindowProps = {};
type WindowState = {
visible: boolean,
example: string,
exampleIdx: number,
type Example = {
files: { [file: string]: string | Uint8Array },
peripheral: PeripheralKind | null,
}
type WindowState = {
exampleIdx: number,
} & ({ visible: false, example: null } | { visible: true, example: Example })
type Touch = { clientX: number, clientY: number };
class Window extends Component<WindowProps, WindowState> {
@@ -41,12 +54,14 @@ class Window extends Component<WindowProps, WindowState> {
private top: number = 0;
private dragging?: { downX: number, downY: number, initialX: number, initialY: number };
private snippets: { [file: string]: string } = {};
constructor(props: WindowProps, context: unknown) {
super(props, context);
this.state = {
visible: false,
example: "",
example: null,
exampleIdx: 0,
}
}
@@ -57,10 +72,17 @@ class Window extends Component<WindowProps, WindowState> {
const element = elements[i] as HTMLElement;
let example = element.innerText;
const snippet = element.getAttribute("data-snippet");
if (snippet) this.snippets[snippet] = example;
if (element.getAttribute("data-lua-kind") == "expr") {
example = exprTemplate.replace("__expr__", example);
}
render(<Click run={this.runExample(example)} />, element);
const mount = element.getAttribute("data-mount");
const peripheral = element.getAttribute("data-peripheral");
render(<Click run={this.runExample(example, mount, peripheral)} />, element);
}
}
@@ -76,23 +98,45 @@ class Window extends Component<WindowProps, WindowState> {
</div>
<div class="computer-container">
<Computer key={exampleIdx} files={{
"example.lua": example, ...defaultFiles
}} />
...example!.files, ...defaultFiles
}} peripherals={{ back: example!.peripheral }} />
</div>
</div> : <div class="example-window example-window-hidden" />;
}
private runExample(example: string): () => void {
return () => {
private runExample(example: string, mount: string | null, peripheral: string | null): () => void {
return async () => {
if (!this.positioned) {
this.positioned = true;
this.left = 20;
this.top = 20;
}
const files: { [file: string]: string | Uint8Array } = { "example.lua": example };
if (mount !== null) {
for (const toMount of mount.split(",")) {
const [name, path] = toMount.split(":", 2);
files[path] = this.snippets[name] || "";
}
}
if (example.includes("data/example.dfpwm")) {
files["data/example.dfpwm.LICENSE"] = exampleAudioLicense;
try {
if (dfpwmAudio === null) dfpwmAudio = download(exampleAudioUrl);
files["data/example.dfpwm"] = await dfpwmAudio;
} catch (e) {
console.error("Cannot download example dfpwm", e);
}
}
this.setState(({ exampleIdx }: WindowState) => ({
visible: true,
example: example,
example: {
files,
peripheral: peripheral as PeripheralKind | null,
},
exampleIdx: exampleIdx + 1,
}));
}

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