mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-22 17:37:38 +00:00
Compare commits
17 Commits
v1.16.5-1.
...
v1.16.5-1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e558b31b2b | ||
![]() |
afd82fbf1f | ||
![]() |
f470478a0f | ||
![]() |
aa009df740 | ||
![]() |
0c6c0badde | ||
![]() |
bed2e0b658 | ||
![]() |
0f9ddac83c | ||
![]() |
932b77d7ee | ||
![]() |
5eedea1bbb | ||
![]() |
114261944a | ||
![]() |
4d10639efb | ||
![]() |
aa36b49c50 | ||
![]() |
8a1067940d | ||
![]() |
0477b2742c | ||
![]() |
b048b6666d | ||
![]() |
e16f66e128 | ||
![]() |
1cfad31a0d |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -13,3 +13,4 @@ src/testMod/server-files/structures linguist-generated
|
||||
|
||||
*.png binary
|
||||
*.jar binary
|
||||
*.dfpwm binary
|
||||
|
53
build.gradle
53
build.gradle
@@ -6,6 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+'
|
||||
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
|
||||
@@ -64,6 +66,8 @@ minecraft {
|
||||
source sourceSets.main
|
||||
}
|
||||
}
|
||||
|
||||
arg "-mixin.config=computercraft.mixins.json"
|
||||
}
|
||||
|
||||
client {
|
||||
@@ -109,6 +113,10 @@ minecraft {
|
||||
accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
|
||||
}
|
||||
|
||||
mixin {
|
||||
add sourceSets.main, 'computercraft.mixins.refmap.json'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
@@ -130,6 +138,7 @@ dependencies {
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:8.25"
|
||||
|
||||
minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
|
||||
annotationProcessor 'org.spongepowered:mixin:0.8.4:processor'
|
||||
|
||||
compileOnly fg.deobf("mezz.jei:jei-1.16.5:7.7.0.104:api")
|
||||
compileOnly fg.deobf("com.blamejared.crafttweaker:CraftTweaker-1.16.5:7.1.0.313")
|
||||
@@ -149,7 +158,7 @@ dependencies {
|
||||
|
||||
testModImplementation sourceSets.main.output
|
||||
|
||||
cctJavadoc 'cc.tweaked:cct-javadoc:1.4.2'
|
||||
cctJavadoc 'cc.tweaked:cct-javadoc:1.4.5'
|
||||
}
|
||||
|
||||
// Compile tasks
|
||||
@@ -181,13 +190,16 @@ task luaJavadoc(type: Javadoc) {
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(["Specification-Title" : "computercraft",
|
||||
"Specification-Vendor" : "SquidDev",
|
||||
"Specification-Version" : "1",
|
||||
"Implementation-Title" : "CC: Tweaked",
|
||||
"Implementation-Version" : "${mod_version}",
|
||||
"Implementation-Vendor" : "SquidDev",
|
||||
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")])
|
||||
attributes([
|
||||
"Specification-Title" : "computercraft",
|
||||
"Specification-Vendor" : "SquidDev",
|
||||
"Specification-Version" : "1",
|
||||
"Implementation-Title" : "CC: Tweaked",
|
||||
"Implementation-Version" : "${mod_version}",
|
||||
"Implementation-Vendor" : "SquidDev",
|
||||
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
|
||||
"MixinConfigs" : "computercraft.mixins.json",
|
||||
])
|
||||
}
|
||||
|
||||
from configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
@@ -261,18 +273,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"
|
||||
|
||||
@@ -280,7 +281,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")
|
||||
|
||||
@@ -288,9 +289,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"
|
||||
}
|
||||
|
||||
|
@@ -51,5 +51,6 @@ exclude: |
|
||||
src/generated|
|
||||
src/test/resources/test-rom/data/json-parsing/|
|
||||
src/testMod/server-files/|
|
||||
config/idea/
|
||||
config/idea/|
|
||||
.*\.dfpwm
|
||||
)
|
||||
|
@@ -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)))
|
||||
|
27
doc/events/speaker_audio_empty.md
Normal file
27
doc/events/speaker_audio_empty.md
Normal 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
200
doc/guides/speaker_audio.md
Normal 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"
|
83
doc/guides/using_require.md
Normal file
83
doc/guides/using_require.md
Normal 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).
|
@@ -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.
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# Mod properties
|
||||
mod_version=1.99.1
|
||||
mod_version=1.100.0
|
||||
|
||||
# Minecraft properties (update mods.toml when changing)
|
||||
mc_version=1.16.5
|
||||
mapping_version=2021.08.08
|
||||
forge_version=36.1.0
|
||||
forge_version=36.2.20
|
||||
# NO SERIOUSLY, UPDATE mods.toml WHEN CHANGING
|
||||
|
@@ -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
86
package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
},
|
||||
|
@@ -5,9 +5,8 @@
|
||||
*/
|
||||
package dan200.computercraft.api.lua;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.*;
|
||||
|
@@ -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 TileEntity} (for instance {@literal minecraft:chest}).
|
||||
* resulting peripheral uses the resource name of the wrapped {@link TileEntity} (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.
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,85 +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.audio.ISound;
|
||||
import net.minecraft.client.audio.ITickableSound;
|
||||
import net.minecraft.client.audio.LocatableSound;
|
||||
import net.minecraft.client.audio.SoundHandler;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.SoundCategory;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
|
||||
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, Vector3d position, ResourceLocation event, float volume, float pitch )
|
||||
{
|
||||
SoundHandler 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 )
|
||||
{
|
||||
ISound sound = sounds.remove( source );
|
||||
if( sound == null ) return;
|
||||
|
||||
Minecraft.getInstance().getSoundManager().stop( sound );
|
||||
}
|
||||
|
||||
public static void moveSound( UUID source, Vector3d position )
|
||||
{
|
||||
MoveableSound sound = sounds.get( source );
|
||||
if( sound != null ) sound.setPosition( position );
|
||||
}
|
||||
|
||||
public static void reset()
|
||||
{
|
||||
sounds.clear();
|
||||
}
|
||||
|
||||
private static class MoveableSound extends LocatableSound implements ITickableSound
|
||||
{
|
||||
protected MoveableSound( ResourceLocation sound, Vector3d position, float volume, float pitch )
|
||||
{
|
||||
super( sound, SoundCategory.RECORDS );
|
||||
setPosition( position );
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
attenuation = ISound.AttenuationType.LINEAR;
|
||||
}
|
||||
|
||||
void setPosition( Vector3d position )
|
||||
{
|
||||
x = (float) position.x();
|
||||
y = (float) position.y();
|
||||
z = (float) position.z();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStopped()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -173,7 +173,6 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.rewind();
|
||||
toUpload.add( new FileUpload( name, buffer, digest ) );
|
||||
}
|
||||
catch( IOException e )
|
||||
|
132
src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
Normal file
132
src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.audio.IAudioStream;
|
||||
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 IAudioStream
|
||||
{
|
||||
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( head.array(), head.position(), toRead ); // TODO: In 1.17 convert this to a ByteBuffer override
|
||||
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();
|
||||
}
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.client.audio.SoundHandler;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
|
||||
/**
|
||||
* 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.source != null )
|
||||
{
|
||||
sound.executor.execute( () -> {
|
||||
if( !sound.source.stopped() ) sound.source.pumpBuffers( 1 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
public void playAudio( Vector3d position, float volume )
|
||||
{
|
||||
SoundHandler 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( Vector3d position, ResourceLocation location, float volume, float pitch )
|
||||
{
|
||||
SoundHandler 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( Vector3d position )
|
||||
{
|
||||
if( sound != null ) sound.setPosition( position );
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if( sound != null ) Minecraft.getInstance().getSoundManager().stop( sound );
|
||||
|
||||
currentStream = null;
|
||||
sound = null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.util.math.vector.Vector3d;
|
||||
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) ) return;
|
||||
SpeakerSound sound = (SpeakerSound) event.getSound();
|
||||
if( sound.stream == null ) return;
|
||||
|
||||
event.getSource().attachBufferStream( sound.stream );
|
||||
event.getSource().play();
|
||||
|
||||
sound.source = 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, Vector3d position )
|
||||
{
|
||||
SpeakerInstance sound = sounds.get( source );
|
||||
if( sound != null ) sound.setPosition( position );
|
||||
}
|
||||
|
||||
public static void reset()
|
||||
{
|
||||
sounds.clear();
|
||||
}
|
||||
}
|
@@ -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 net.minecraft.client.audio.IAudioStream;
|
||||
import net.minecraft.client.audio.ITickableSound;
|
||||
import net.minecraft.client.audio.LocatableSound;
|
||||
import net.minecraft.client.audio.SoundSource;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.SoundCategory;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class SpeakerSound extends LocatableSound implements ITickableSound
|
||||
{
|
||||
SoundSource source;
|
||||
Executor executor;
|
||||
DfpwmStream stream;
|
||||
|
||||
SpeakerSound( ResourceLocation sound, DfpwmStream stream, Vector3d position, float volume, float pitch )
|
||||
{
|
||||
super( sound, SoundCategory.RECORDS );
|
||||
setPosition( position );
|
||||
this.stream = stream;
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
attenuation = AttenuationType.LINEAR;
|
||||
}
|
||||
|
||||
void setPosition( Vector3d 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 IAudioStream getStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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.matrix.MatrixStack;
|
||||
import com.mojang.blaze3d.vertex.IVertexBuilder;
|
||||
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.block.BlockState;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.BlockModelRenderer;
|
||||
import net.minecraft.client.renderer.BlockModelShapes;
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher;
|
||||
import net.minecraft.client.renderer.model.IBakedModel;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.BlockRayTraceResult;
|
||||
import net.minecraft.util.math.RayTraceResult;
|
||||
import net.minecraft.world.IBlockDisplayReader;
|
||||
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 BlockRendererDispatcher#renderBlockDamage(BlockState, BlockPos, IBlockDisplayReader, MatrixStack, IVertexBuilder, IModelData)
|
||||
*/
|
||||
@Mixin( BlockRendererDispatcher.class )
|
||||
public class BlockRendererDispatcherMixin
|
||||
{
|
||||
@Shadow
|
||||
private final Random random;
|
||||
@Shadow
|
||||
private final BlockModelShapes blockModelShaper;
|
||||
@Shadow
|
||||
private final BlockModelRenderer modelRenderer;
|
||||
|
||||
public BlockRendererDispatcherMixin( Random random, BlockModelShapes blockModelShaper, BlockModelRenderer modelRenderer )
|
||||
{
|
||||
this.random = random;
|
||||
this.blockModelShaper = blockModelShaper;
|
||||
this.modelRenderer = modelRenderer;
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "name=/^renderBlockDamage$/ 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, IBlockDisplayReader world, MatrixStack pose, IVertexBuilder 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;
|
||||
}
|
||||
|
||||
RayTraceResult hit = Minecraft.getInstance().hitResult;
|
||||
if( hit == null || hit.getType() != RayTraceResult.Type.BLOCK ) return;
|
||||
BlockPos hitPos = ((BlockRayTraceResult) 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 );
|
||||
|
||||
IBakedModel model = blockModelShaper.getBlockModel( newState );
|
||||
long seed = newState.getSeed( pos );
|
||||
modelRenderer.renderModel( world, model, newState, pos, pose, buffers, true, random, seed, OverlayTexture.NO_OVERLAY, modelData );
|
||||
}
|
||||
}
|
@@ -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.
|
||||
|
@@ -53,10 +53,9 @@ public class FileSlice
|
||||
return;
|
||||
}
|
||||
|
||||
bytes.rewind();
|
||||
file.position( offset );
|
||||
file.put( bytes );
|
||||
file.rewind();
|
||||
ByteBuffer other = file.duplicate();
|
||||
other.position( offset ); // TODO: In 1.17 we can use a separate put(idx, _) method.
|
||||
other.put( bytes );
|
||||
|
||||
if( bytes.remaining() != 0 ) throw new IllegalStateException( "Should have read the whole buffer" );
|
||||
}
|
||||
|
@@ -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 )
|
||||
|
@@ -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 );
|
||||
}
|
||||
|
||||
public static void sendToPlayer( PlayerEntity player, NetworkMessage packet )
|
||||
|
@@ -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.PacketBuffer;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import net.minecraftforge.fml.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 Vector3d pos;
|
||||
private final ByteBuffer content;
|
||||
private final float volume;
|
||||
|
||||
public SpeakerAudioClientMessage( UUID source, Vector3d pos, float volume, ByteBuffer content )
|
||||
{
|
||||
this.source = source;
|
||||
this.pos = pos;
|
||||
this.content = content;
|
||||
this.volume = volume;
|
||||
}
|
||||
|
||||
public SpeakerAudioClientMessage( PacketBuffer buf )
|
||||
{
|
||||
source = buf.readUUID();
|
||||
pos = new Vector3d( buf.readDouble(), buf.readDouble(), buf.readDouble() );
|
||||
volume = buf.readFloat();
|
||||
|
||||
SpeakerManager.getSound( source ).pushAudio( buf );
|
||||
content = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBytes( @Nonnull PacketBuffer 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 );
|
||||
}
|
||||
}
|
@@ -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.PacketBuffer;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
@@ -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.PacketBuffer;
|
||||
import net.minecraft.util.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 );
|
||||
}
|
||||
}
|
||||
|
@@ -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.PacketBuffer;
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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.math.MathHelper;
|
||||
|
||||
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 = MathHelper.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;
|
||||
}
|
||||
}
|
@@ -9,10 +9,15 @@ 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.network.play.server.SPlaySoundPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.state.properties.NoteBlockInstrument;
|
||||
@@ -20,66 +25,161 @@ import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.ResourceLocationException;
|
||||
import net.minecraft.util.SoundCategory;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
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 Vector3d 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 );
|
||||
|
||||
Vector3d pos = getPosition();
|
||||
World world = getWorld();
|
||||
if( world == null ) return;
|
||||
MinecraftServer server = world.getServer();
|
||||
|
||||
synchronized( pendingNotes )
|
||||
{
|
||||
for( PendingSound sound : pendingNotes )
|
||||
{
|
||||
lastPlayTime = clock;
|
||||
server.getPlayerList().broadcast(
|
||||
null, pos.x, pos.y, pos.z, sound.volume * 16, world.dimension(),
|
||||
new SPlaySoundPacket( sound.location, SoundCategory.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 ),
|
||||
world, 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 ) ),
|
||||
getWorld().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 )
|
||||
{
|
||||
Vector3d 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 ),
|
||||
getWorld().getChunkAt( new BlockPos( position ) )
|
||||
);
|
||||
syncedPosition( position );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public abstract World getWorld();
|
||||
|
||||
@Nonnull
|
||||
public abstract Vector3d 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,123 @@ 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( Vector3d position )
|
||||
{
|
||||
if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS )
|
||||
lastPosition = position;
|
||||
lastPositionTime = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attach( @Nonnull IComputerAccess computer )
|
||||
{
|
||||
computers.add( computer );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach( @Nonnull IComputerAccess computer )
|
||||
{
|
||||
computers.remove( computer );
|
||||
}
|
||||
|
||||
private static final class PendingSound
|
||||
{
|
||||
final ResourceLocation location;
|
||||
final float volume;
|
||||
final float pitch;
|
||||
|
||||
private PendingSound( ResourceLocation location, float volume, float pitch )
|
||||
{
|
||||
// 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;
|
||||
this.location = location;
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
World world = getWorld();
|
||||
Vector3d pos = getPosition();
|
||||
|
||||
float actualVolume = MathHelper.clamp( volume, 0.0f, 3.0f );
|
||||
float range = actualVolume * 16;
|
||||
|
||||
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 SPlaySoundPacket( name, SoundCategory.RECORDS, pos, actualVolume, pitch )
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
NetworkHandler.sendToAllAround(
|
||||
new SpeakerPlayClientMessage( getSource(), pos, name, actualVolume, pitch ),
|
||||
world, pos, range
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} );
|
||||
|
||||
lastPlayTime = clock;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -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 implements ITickableTileEntity
|
||||
{
|
||||
private final SpeakerPeripheral peripheral;
|
||||
private LazyOptional<IPeripheral> peripheralCap;
|
||||
private final UUID source = UUID.randomUUID();
|
||||
|
||||
public TileSpeaker( TileEntityType<TileSpeaker> type )
|
||||
{
|
||||
@@ -49,7 +47,7 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity
|
||||
super.setRemoved();
|
||||
if( level != null && !level.isClientSide )
|
||||
{
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( peripheral.getSource() ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +86,7 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity
|
||||
return speaker.getLevel();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Vector3d getPosition()
|
||||
{
|
||||
@@ -95,12 +94,6 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity
|
||||
return new Vector3d( pos.getX(), pos.getY(), pos.getZ() );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID getSource()
|
||||
{
|
||||
return speaker.source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( @Nullable IPeripheral other )
|
||||
{
|
||||
|
@@ -9,32 +9,22 @@ 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.fml.LogicalSidedProvider;
|
||||
import net.minecraftforge.fml.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.
|
||||
*/
|
||||
public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral
|
||||
{
|
||||
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() ) );
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -25,6 +24,7 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||
import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider;
|
||||
import net.minecraft.client.util.ITooltipFlag;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.item.ItemEntity;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.item.Item;
|
||||
@@ -83,53 +83,65 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tick( @Nonnull ItemStack stack, @Nonnull World world, @Nonnull Entity entity, @Nonnull PocketServerComputer computer )
|
||||
{
|
||||
IPocketUpgrade upgrade = getUpgrade( stack );
|
||||
|
||||
computer.setWorld( 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, World world, @Nonnull Entity entity, int slotNum, boolean selected )
|
||||
{
|
||||
if( !world.isClientSide )
|
||||
{
|
||||
// Server side
|
||||
IInventory inventory = entity instanceof PlayerEntity ? ((PlayerEntity) entity).inventory : null;
|
||||
PocketServerComputer computer = createServerComputer( world, inventory, entity, stack );
|
||||
if( computer != null )
|
||||
{
|
||||
IPocketUpgrade upgrade = getUpgrade( stack );
|
||||
PocketServerComputer computer = createServerComputer( world, entity, inventory, stack );
|
||||
computer.keepAlive();
|
||||
|
||||
// Ping computer
|
||||
computer.keepAlive();
|
||||
computer.setWorld( 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 ActionResult<ItemStack> use( World world, PlayerEntity player, @Nonnull Hand hand )
|
||||
@@ -137,22 +149,18 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
||||
ItemStack stack = player.getItemInHand( hand );
|
||||
if( !world.isClientSide )
|
||||
{
|
||||
PocketServerComputer computer = createServerComputer( world, player.inventory, player, stack );
|
||||
PocketServerComputer computer = createServerComputer( world, player, player.inventory, 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 == Hand.OFF_HAND;
|
||||
new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
|
||||
@@ -210,17 +218,17 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
||||
return super.getCreatorModId( stack );
|
||||
}
|
||||
|
||||
public PocketServerComputer createServerComputer( final World world, IInventory inventory, Entity entity, @Nonnull ItemStack stack )
|
||||
@Nonnull
|
||||
public PocketServerComputer createServerComputer( World world, Entity entity, @Nullable IInventory 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 );
|
||||
}
|
||||
@@ -238,13 +246,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 );
|
||||
@@ -254,15 +256,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 );
|
||||
@@ -277,6 +281,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ClientComputer getClientComputer( @Nonnull ItemStack stack )
|
||||
{
|
||||
int instanceID = getInstanceID( stack );
|
||||
|
@@ -43,6 +43,6 @@ public class PocketSpeaker extends AbstractPocketUpgrade
|
||||
}
|
||||
|
||||
speaker.update();
|
||||
access.setLight( speaker.madeSound( 20 ) ? 0x3320fc : -1 );
|
||||
access.setLight( speaker.madeSound() ? 0x3320fc : -1 );
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral;
|
||||
import net.minecraft.util.math.vector.Vector3d;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
|
||||
{
|
||||
private World world = null;
|
||||
@@ -27,6 +29,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
|
||||
return world;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Vector3d getPosition()
|
||||
{
|
||||
|
@@ -25,8 +25,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
|
||||
*/
|
||||
|
@@ -24,6 +24,7 @@ import dan200.computercraft.shared.util.HolidayUtil;
|
||||
import dan200.computercraft.shared.util.InventoryDelegate;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.block.material.PushReaction;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.MoverType;
|
||||
import net.minecraft.fluid.FluidState;
|
||||
@@ -34,7 +35,6 @@ import net.minecraft.particles.ParticleTypes;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.Direction;
|
||||
import net.minecraft.util.EntityPredicates;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.math.AxisAlignedBB;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
@@ -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;
|
||||
@@ -887,7 +890,7 @@ public class TurtleBrain implements ITurtleAccess
|
||||
}
|
||||
|
||||
AxisAlignedBB aabb = new AxisAlignedBB( minX, minY, minZ, maxX, maxY, maxZ );
|
||||
List<Entity> list = world.getEntitiesOfClass( Entity.class, aabb, EntityPredicates.NO_SPECTATORS );
|
||||
List<Entity> list = world.getEntitiesOfClass( Entity.class, aabb, PUSHABLE_ENTITY );
|
||||
if( !list.isEmpty() )
|
||||
{
|
||||
double pushStep = 1.0f / ANIM_DURATION;
|
||||
|
@@ -43,6 +43,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade
|
||||
return turtle.getWorld();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Vector3d getPosition()
|
||||
{
|
||||
|
@@ -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.client.Minecraft;
|
||||
import net.minecraft.util.Util;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,12 @@
|
||||
public net.minecraft.client.renderer.FirstPersonRenderer func_178100_c(F)F # getMapAngleFromPitch
|
||||
public net.minecraft.client.renderer.FirstPersonRenderer func_228401_a_(Lcom/mojang/blaze3d/matrix/MatrixStack;Lnet/minecraft/client/renderer/IRenderTypeBuffer;IFFLnet/minecraft/util/HandSide;)V # renderArmFirstPerson
|
||||
public net.minecraft.client.renderer.FirstPersonRenderer func_228403_a_(Lcom/mojang/blaze3d/matrix/MatrixStack;Lnet/minecraft/client/renderer/IRenderTypeBuffer;ILnet/minecraft/util/HandSide;)V # renderArm
|
||||
|
||||
# ClientTableFormatter
|
||||
public net.minecraft.client.gui.NewChatGui func_146234_a(Lnet/minecraft/util/text/ITextComponent;I)V # printChatMessageWithOptionalDeletion
|
||||
public net.minecraft.client.gui.NewChatGui func_146242_c(I)V # deleteChatLine
|
||||
public net.minecraft.client.Minecraft field_71462_r # currentScreen
|
||||
|
||||
# SpeakerInstance/SpeakerManager
|
||||
public net.minecraft.client.audio.SoundSource func_216421_a(I)V # pumpBuffers
|
||||
public net.minecraft.client.audio.SoundEngine field_217940_j # executor
|
||||
|
@@ -20,6 +20,6 @@ CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles a
|
||||
[[dependencies.computercraft]]
|
||||
modId="forge"
|
||||
mandatory=true
|
||||
versionRange="[36.1.0,37)"
|
||||
versionRange="[36.2.20,37)"
|
||||
ordering="NONE"
|
||||
side="BOTH"
|
||||
|
10
src/main/resources/assets/computercraft/sounds.json
Normal file
10
src/main/resources/assets/computercraft/sounds.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"speaker.dfpwm_fake_audio_should_not_be_played": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "computercraft:empty",
|
||||
"stream": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
BIN
src/main/resources/assets/computercraft/sounds/empty.ogg
Normal file
BIN
src/main/resources/assets/computercraft/sounds/empty.ogg
Normal file
Binary file not shown.
13
src/main/resources/computercraft.mixins.json
Normal file
13
src/main/resources/computercraft.mixins.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"minVersion": "0.8",
|
||||
"required": true,
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"refmap": "computercraft.mixins.refmap.json",
|
||||
"package": "dan200.computercraft.mixin",
|
||||
"client": [
|
||||
"BlockRendererDispatcherMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -66,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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 } })
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -1,3 +1,15 @@
|
||||
# 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)
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
@@ -1,12 +1,13 @@
|
||||
New features in CC: Tweaked 1.99.1
|
||||
New features in CC: Tweaked 1.100.0
|
||||
|
||||
* 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.
|
||||
* 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 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.
|
||||
* 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.
|
||||
|
@@ -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,
|
||||
}
|
@@ -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.
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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 " } },
|
||||
|
@@ -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
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
2982
src/web/mount/example.dfpwm
Normal file
2982
src/web/mount/example.dfpwm
Normal file
File diff suppressed because one or more lines are too long
3
src/web/mount/example.dfpwm.LICENSE
Normal file
3
src/web/mount/example.dfpwm.LICENSE
Normal file
@@ -0,0 +1,3 @@
|
||||
Playing Soliloquy [Remake] by Alcakight
|
||||
Source: https://soundcloud.com/alcaknight/soliloquy-remake
|
||||
License: under CC BY 3.0
|
@@ -1,3 +1,12 @@
|
||||
-- Print out license information if needed
|
||||
if fs.exists("data/example.dfpwm") then
|
||||
local h = io.open("data/example.dfpwm.LICENSE")
|
||||
local contents = h:read("*a")
|
||||
h:close()
|
||||
|
||||
write(contents)
|
||||
end
|
||||
|
||||
-- Make the startup file invisible, then run the file. We could use
|
||||
-- shell.run, but this ensures the program is in shell history, etc...
|
||||
fs.delete("startup.lua")
|
||||
|
@@ -19,10 +19,23 @@ declare module "*.settings" {
|
||||
export default contents;
|
||||
}
|
||||
|
||||
declare module "*.LICENSE" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
declare module "*.dfpwm" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
|
||||
declare module "copycat/embed" {
|
||||
import { h, Component, render, ComponentChild } from "preact";
|
||||
|
||||
export type Side = "up" | "down" | "left" | "right" | "front" | "back";
|
||||
export type PeripheralKind = "speaker";
|
||||
|
||||
export { h, Component, render };
|
||||
|
||||
export type ComputerAccess = unknown;
|
||||
@@ -35,6 +48,9 @@ declare module "copycat/embed" {
|
||||
width?: number,
|
||||
height?: number,
|
||||
resolve?: (computer: ComputerAccess) => void,
|
||||
peripherals?: {
|
||||
[side in Side]?: PeripheralKind | null
|
||||
},
|
||||
}
|
||||
|
||||
class Computer extends Component<MainProps, unknown> {
|
||||
|
Reference in New Issue
Block a user