mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-04 07:32:59 +00:00 
			
		
		
		
	Compare commits
	
		
			42 Commits
		
	
	
		
			v1.18-1.99
			...
			v1.18.1-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2b901f2d5e | ||
| 
						 | 
					62f2cd5cb2 | ||
| 
						 | 
					e558b31b2b | ||
| 
						 | 
					afd82fbf1f | ||
| 
						 | 
					901d8d4c3b | ||
| 
						 | 
					f794ce42ab | ||
| 
						 | 
					f470478a0f | ||
| 
						 | 
					aa009df740 | ||
| 
						 | 
					0c6c0badde | ||
| 
						 | 
					bed2e0b658 | ||
| 
						 | 
					0f9ddac83c | ||
| 
						 | 
					932b77d7ee | ||
| 
						 | 
					5eedea1bbb | ||
| 
						 | 
					114261944a | ||
| 
						 | 
					4d10639efb | ||
| 
						 | 
					aa36b49c50 | ||
| 
						 | 
					8a1067940d | ||
| 
						 | 
					2562642664 | ||
| 
						 | 
					632db1cfa5 | ||
| 
						 | 
					aa0d544bba | ||
| 
						 | 
					2f6ad00764 | ||
| 
						 | 
					05da4dd362 | ||
| 
						 | 
					0477b2742c | ||
| 
						 | 
					fe3c42ce22 | ||
| 
						 | 
					f6fcba7a39 | ||
| 
						 | 
					82a7edee12 | ||
| 
						 | 
					b048b6666d | ||
| 
						 | 
					e16f66e128 | ||
| 
						 | 
					1cfad31a0d | ||
| 
						 | 
					7c373c6e06 | ||
| 
						 | 
					6196aae488 | ||
| 
						 | 
					92a0ef2b75 | ||
| 
						 | 
					57c5d19f95 | ||
| 
						 | 
					1f6e0f287d | ||
| 
						 | 
					0e4b7a5a75 | ||
| 
						 | 
					47ad7a35dc | ||
| 
						 | 
					3eab2a9b57 | ||
| 
						 | 
					c4024a4c4c | ||
| 
						 | 
					f5fb82cd7d | ||
| 
						 | 
					e18ba8a2c2 | ||
| 
						 | 
					422bfdb60d | ||
| 
						 | 
					1851ed31cd | 
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							@@ -13,3 +13,4 @@ src/testMod/server-files/structures linguist-generated
 | 
			
		||||
 | 
			
		||||
*.png binary
 | 
			
		||||
*.jar binary
 | 
			
		||||
*.dfpwm binary
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.yaml
									
									
									
									
										vendored
									
									
								
							@@ -8,9 +8,9 @@ body:
 | 
			
		||||
    label: Minecraft Version
 | 
			
		||||
    description: What version of Minecraft are you using?
 | 
			
		||||
    options:
 | 
			
		||||
      - 1.15.x
 | 
			
		||||
      - 1.16.x
 | 
			
		||||
      - 1.17.x
 | 
			
		||||
      - 1.18.x
 | 
			
		||||
  validations:
 | 
			
		||||
    required: true
 | 
			
		||||
- type: input
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								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
 | 
			
		||||
@@ -46,10 +48,6 @@ tasks.withType(JavaExec).configureEach {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sourceSets {
 | 
			
		||||
    main.java {
 | 
			
		||||
        exclude 'dan200/computercraft/shared/integration/jei/**'
 | 
			
		||||
        exclude 'dan200/computercraft/shared/integration/morered/**'
 | 
			
		||||
    }
 | 
			
		||||
    main.resources {
 | 
			
		||||
        srcDir 'src/generated/resources'
 | 
			
		||||
    }
 | 
			
		||||
@@ -72,6 +70,8 @@ minecraft {
 | 
			
		||||
                    source sourceSets.main
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            arg "-mixin.config=computercraft.mixins.json"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client {
 | 
			
		||||
@@ -129,6 +129,11 @@ minecraft {
 | 
			
		||||
    accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
 | 
			
		||||
    accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mixin {
 | 
			
		||||
    add sourceSets.main, 'computercraft.mixins.refmap.json'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
    mavenCentral()
 | 
			
		||||
    maven {
 | 
			
		||||
@@ -152,9 +157,10 @@ dependencies {
 | 
			
		||||
    checkstyle "com.puppycrawl.tools:checkstyle:8.45"
 | 
			
		||||
 | 
			
		||||
    minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
 | 
			
		||||
    annotationProcessor 'org.spongepowered:mixin:0.8.4:processor'
 | 
			
		||||
 | 
			
		||||
    // compileOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14:api")
 | 
			
		||||
    // runtimeOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14")
 | 
			
		||||
    compileOnly fg.deobf("mezz.jei:jei-1.18.1:9.1.0.47:api")
 | 
			
		||||
    runtimeOnly fg.deobf("mezz.jei:jei-1.18.1:9.1.0.47")
 | 
			
		||||
 | 
			
		||||
    shade 'org.squiddev:Cobalt:0.5.2-SNAPSHOT'
 | 
			
		||||
 | 
			
		||||
@@ -170,7 +176,7 @@ dependencies {
 | 
			
		||||
        exclude group: "org.jetbrains", module: "annotations"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cctJavadoc 'cc.tweaked:cct-javadoc:1.4.2'
 | 
			
		||||
    cctJavadoc 'cc.tweaked:cct-javadoc:1.4.5'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compile tasks
 | 
			
		||||
@@ -210,6 +216,8 @@ jar {
 | 
			
		||||
            "Implementation-Version"  : "${mod_version}",
 | 
			
		||||
            "Implementation-Vendor"   : "SquidDev",
 | 
			
		||||
            "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
 | 
			
		||||
        ,
 | 
			
		||||
            "MixinConfigs"            : "computercraft.mixins.json",
 | 
			
		||||
        ])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -282,18 +290,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"
 | 
			
		||||
 | 
			
		||||
@@ -301,7 +298,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")
 | 
			
		||||
 | 
			
		||||
@@ -309,9 +306,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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -479,7 +480,7 @@ task checkRelease {
 | 
			
		||||
}
 | 
			
		||||
check.dependsOn checkRelease
 | 
			
		||||
 | 
			
		||||
def isStable = false
 | 
			
		||||
def isStable = true
 | 
			
		||||
 | 
			
		||||
curseforge {
 | 
			
		||||
    apiKey = project.hasProperty('curseForgeApiKey') ? project.curseForgeApiKey : ''
 | 
			
		||||
 
 | 
			
		||||
@@ -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,10 +1,10 @@
 | 
			
		||||
org.gradle.jvmargs=-Xmx3G
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
mod_version=1.99.0
 | 
			
		||||
mod_version=1.100.0
 | 
			
		||||
 | 
			
		||||
# Minecraft properties (update mods.toml when changing)
 | 
			
		||||
mc_version=1.18
 | 
			
		||||
mc_version=1.18.1
 | 
			
		||||
mapping_version=2021.09.05
 | 
			
		||||
forge_version=38.0.0
 | 
			
		||||
forge_version=39.0.0
 | 
			
		||||
# 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;
 | 
			
		||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -183,6 +183,24 @@ public interface IArguments
 | 
			
		||||
        return (Map<?, ?>) value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a table in an unsafe manner.
 | 
			
		||||
     *
 | 
			
		||||
     * Classes implementing this interface may choose to implement a more optimised version which does not copy the
 | 
			
		||||
     * table, instead returning a wrapper version, making it more efficient. However, the caller must guarantee that
 | 
			
		||||
     * they do not access off the computer thread (and so should not be used with main-thread functions) or once the
 | 
			
		||||
     * function call has finished (for instance, in callbacks).
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @return The argument's value.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     */
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    default LuaTable<?, ?> getTableUnsafe( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        return new ObjectLuaTable( getTable( index ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a double.
 | 
			
		||||
     *
 | 
			
		||||
@@ -314,6 +332,27 @@ public interface IArguments
 | 
			
		||||
        return Optional.of( (Map<?, ?>) value );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a table in an unsafe manner.
 | 
			
		||||
     *
 | 
			
		||||
     * Classes implementing this interface may choose to implement a more optimised version which does not copy the
 | 
			
		||||
     * table, instead returning a wrapper version, making it more efficient. However, the caller must guarantee that
 | 
			
		||||
     * they do not access off the computer thread (and so should not be used with main-thread functions) or once the
 | 
			
		||||
     * function call has finished (for instance, in callbacks).
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @return The argument's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     */
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    default Optional<LuaTable<?, ?>> optTableUnsafe( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        Object value = get( index );
 | 
			
		||||
        if( value == null ) return Optional.empty();
 | 
			
		||||
        if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "map", value );
 | 
			
		||||
        return Optional.of( new ObjectLuaTable( (Map<?, ?>) value ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a double.
 | 
			
		||||
     *
 | 
			
		||||
@@ -404,4 +443,13 @@ public interface IArguments
 | 
			
		||||
    {
 | 
			
		||||
        return optTable( index ).orElse( def );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This is called when the current function finishes, before any main thread tasks have run.
 | 
			
		||||
     *
 | 
			
		||||
     * Called when the current function returns, and so some values are no longer guaranteed to be safe to access.
 | 
			
		||||
     */
 | 
			
		||||
    default void releaseImmediate()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,8 +51,17 @@ public @interface LuaFunction
 | 
			
		||||
     * Run this function on the main server thread. This should be specified for any method which interacts with
 | 
			
		||||
     * Minecraft in a thread-unsafe manner.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this functi
 | 
			
		||||
     * @return Whether this function should be run on the main thread.
 | 
			
		||||
     * @see ILuaContext#issueMainThreadTask(ILuaTask)
 | 
			
		||||
     */
 | 
			
		||||
    boolean mainThread() default false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Allow using "unsafe" arguments, such {@link IArguments#getTableUnsafe(int)}.
 | 
			
		||||
     *
 | 
			
		||||
     * This is incompatible with {@link #mainThread()}.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this function supports unsafe arguments.
 | 
			
		||||
     */
 | 
			
		||||
    boolean unsafe() default false;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								src/main/java/dan200/computercraft/api/lua/LuaTable.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/main/java/dan200/computercraft/api/lua/LuaTable.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of the public ComputerCraft API - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only.
 | 
			
		||||
 * For help using the API, and posting your mods, visit the forums at computercraft.info.
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.api.lua.LuaValues.*;
 | 
			
		||||
 | 
			
		||||
public interface LuaTable<K, V> extends Map<K, V>
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Compute the length of the array part of this table.
 | 
			
		||||
     *
 | 
			
		||||
     * @return This table's length.
 | 
			
		||||
     */
 | 
			
		||||
    default int length()
 | 
			
		||||
    {
 | 
			
		||||
        int size = 0;
 | 
			
		||||
        while( containsKey( (double) (size + 1) ) ) size++;
 | 
			
		||||
        return size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The table's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    default long getLong( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        Object value = get( (double) index );
 | 
			
		||||
        if( !(value instanceof Number) ) throw badTableItem( index, "number", getType( value ) );
 | 
			
		||||
 | 
			
		||||
        Number number = (Number) value;
 | 
			
		||||
        double asDouble = number.doubleValue();
 | 
			
		||||
        if( !Double.isFinite( asDouble ) ) throw badTableItem( index, "number", getNumericType( asDouble ) );
 | 
			
		||||
        return number.longValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The table's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    default long getLong( String key ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        Object value = get( key );
 | 
			
		||||
        if( !(value instanceof Number) ) throw badField( key, "number", getType( value ) );
 | 
			
		||||
 | 
			
		||||
        Number number = (Number) value;
 | 
			
		||||
        double asDouble = number.doubleValue();
 | 
			
		||||
        if( !Double.isFinite( asDouble ) ) throw badField( key, "number", getNumericType( asDouble ) );
 | 
			
		||||
        return number.longValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The table's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    default int getInt( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        return (int) getLong( index );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The table's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    default int getInt( String key ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        return (int) getLong( key );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    default V put( K o, V o2 )
 | 
			
		||||
    {
 | 
			
		||||
        throw new UnsupportedOperationException( "Cannot modify LuaTable" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    default V remove( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        throw new UnsupportedOperationException( "Cannot modify LuaTable" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    default void putAll( @Nonnull Map<? extends K, ? extends V> map )
 | 
			
		||||
    {
 | 
			
		||||
        throw new UnsupportedOperationException( "Cannot modify LuaTable" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    default void clear()
 | 
			
		||||
    {
 | 
			
		||||
        throw new UnsupportedOperationException( "Cannot modify LuaTable" );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -102,6 +102,34 @@ public final class LuaValues
 | 
			
		||||
        return new LuaException( "bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Construct a table item exception, from an expected and actual type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index    The index into the table, starting from 1.
 | 
			
		||||
     * @param expected The expected type for this table item.
 | 
			
		||||
     * @param actual   The provided type for this table item.
 | 
			
		||||
     * @return The constructed exception, which should be thrown immediately.
 | 
			
		||||
     */
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    public static LuaException badTableItem( int index, @Nonnull String expected, @Nonnull String actual )
 | 
			
		||||
    {
 | 
			
		||||
        return new LuaException( "table item #" + index + " is not " + expected + " (got " + actual + ")" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Construct a field exception, from an expected and actual type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key      The name of the field.
 | 
			
		||||
     * @param expected The expected type for this table item.
 | 
			
		||||
     * @param actual   The provided type for this table item.
 | 
			
		||||
     * @return The constructed exception, which should be thrown immediately.
 | 
			
		||||
     */
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    public static LuaException badField( String key, @Nonnull String expected, @Nonnull String actual )
 | 
			
		||||
    {
 | 
			
		||||
        return new LuaException( "field " + key + " is not " + expected + " (got " + actual + ")" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -98,7 +98,10 @@ public final class MethodResult
 | 
			
		||||
    {
 | 
			
		||||
        Objects.requireNonNull( callback, "callback cannot be null" );
 | 
			
		||||
        return new MethodResult( new Object[] { filter }, results -> {
 | 
			
		||||
            if( results.length >= 1 && results[0].equals( "terminate" ) ) throw new LuaException( "Terminated", 0 );
 | 
			
		||||
            if( results.length >= 1 && Objects.equals( results[0], "terminate" ) )
 | 
			
		||||
            {
 | 
			
		||||
                throw new LuaException( "Terminated", 0 );
 | 
			
		||||
            }
 | 
			
		||||
            return callback.resume( results );
 | 
			
		||||
        } );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,12 @@
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An implementation of {@link IArguments} which wraps an array of {@link Object}.
 | 
			
		||||
@@ -16,6 +18,8 @@ import java.util.Objects;
 | 
			
		||||
public final class ObjectArguments implements IArguments
 | 
			
		||||
{
 | 
			
		||||
    private static final IArguments EMPTY = new ObjectArguments();
 | 
			
		||||
 | 
			
		||||
    private boolean released = false;
 | 
			
		||||
    private final List<Object> args;
 | 
			
		||||
 | 
			
		||||
    @Deprecated
 | 
			
		||||
@@ -63,4 +67,34 @@ public final class ObjectArguments implements IArguments
 | 
			
		||||
    {
 | 
			
		||||
        return args.toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public LuaTable<?, ?> getTableUnsafe( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        if( released )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( "Cannot use getTableUnsafe after IArguments has been released" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return IArguments.super.getTableUnsafe( index );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<LuaTable<?, ?>> optTableUnsafe( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        if( released )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( "Cannot use optTableUnsafe after IArguments has been released" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return IArguments.super.optTableUnsafe( index );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void releaseImmediate()
 | 
			
		||||
    {
 | 
			
		||||
        released = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,73 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of the public ComputerCraft API - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only.
 | 
			
		||||
 * For help using the API, and posting your mods, visit the forums at computercraft.info.
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
public class ObjectLuaTable implements LuaTable<Object, Object>
 | 
			
		||||
{
 | 
			
		||||
    private final Map<Object, Object> map;
 | 
			
		||||
 | 
			
		||||
    public ObjectLuaTable( Map<?, ?> map )
 | 
			
		||||
    {
 | 
			
		||||
        this.map = Collections.unmodifiableMap( map );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int size()
 | 
			
		||||
    {
 | 
			
		||||
        return map.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isEmpty()
 | 
			
		||||
    {
 | 
			
		||||
        return map.isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean containsKey( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        return map.containsKey( o );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean containsValue( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        return map.containsKey( o );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object get( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        return map.get( o );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Set<Object> keySet()
 | 
			
		||||
    {
 | 
			
		||||
        return map.keySet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<Object> values()
 | 
			
		||||
    {
 | 
			
		||||
        return map.values();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Set<Entry<Object, Object>> entrySet()
 | 
			
		||||
    {
 | 
			
		||||
        return map.entrySet();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +23,7 @@ public interface GenericPeripheral extends GenericSource
 | 
			
		||||
     * Get the type of the exposed peripheral.
 | 
			
		||||
     *
 | 
			
		||||
     * Unlike normal {@link IPeripheral}s, {@link GenericPeripheral} do not have to have a type. By default, the
 | 
			
		||||
     * resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@literal minecraft:chest}).
 | 
			
		||||
     * resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@code minecraft:chest}).
 | 
			
		||||
     *
 | 
			
		||||
     * However, in some cases it may be more appropriate to specify a more readable name. Overriding this method allows
 | 
			
		||||
     * you to do so.
 | 
			
		||||
 
 | 
			
		||||
@@ -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,84 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.client;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
 | 
			
		||||
import net.minecraft.client.resources.sounds.SoundInstance;
 | 
			
		||||
import net.minecraft.client.resources.sounds.TickableSoundInstance;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundSource;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
public class SoundManager
 | 
			
		||||
{
 | 
			
		||||
    private static final Map<UUID, MoveableSound> sounds = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
    public static void playSound( UUID source, Vec3 position, ResourceLocation event, float volume, float pitch )
 | 
			
		||||
    {
 | 
			
		||||
        var soundManager = Minecraft.getInstance().getSoundManager();
 | 
			
		||||
 | 
			
		||||
        MoveableSound oldSound = sounds.get( source );
 | 
			
		||||
        if( oldSound != null ) soundManager.stop( oldSound );
 | 
			
		||||
 | 
			
		||||
        MoveableSound newSound = new MoveableSound( event, position, volume, pitch );
 | 
			
		||||
        sounds.put( source, newSound );
 | 
			
		||||
        soundManager.play( newSound );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void stopSound( UUID source )
 | 
			
		||||
    {
 | 
			
		||||
        SoundInstance sound = sounds.remove( source );
 | 
			
		||||
        if( sound == null ) return;
 | 
			
		||||
 | 
			
		||||
        Minecraft.getInstance().getSoundManager().stop( sound );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void moveSound( UUID source, Vec3 position )
 | 
			
		||||
    {
 | 
			
		||||
        MoveableSound sound = sounds.get( source );
 | 
			
		||||
        if( sound != null ) sound.setPosition( position );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void reset()
 | 
			
		||||
    {
 | 
			
		||||
        sounds.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static class MoveableSound extends AbstractSoundInstance implements TickableSoundInstance
 | 
			
		||||
    {
 | 
			
		||||
        protected MoveableSound( ResourceLocation sound, Vec3 position, float volume, float pitch )
 | 
			
		||||
        {
 | 
			
		||||
            super( sound, SoundSource.RECORDS );
 | 
			
		||||
            setPosition( position );
 | 
			
		||||
            this.volume = volume;
 | 
			
		||||
            this.pitch = pitch;
 | 
			
		||||
            attenuation = Attenuation.LINEAR;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void setPosition( Vec3 position )
 | 
			
		||||
        {
 | 
			
		||||
            x = (float) position.x();
 | 
			
		||||
            y = (float) position.y();
 | 
			
		||||
            z = (float) position.z();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean isStopped()
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void tick()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ package dan200.computercraft.client.gui;
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import dan200.computercraft.ComputerCraft;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.WidgetTerminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ClientComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
@@ -102,6 +103,16 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
 | 
			
		||||
        renderTooltip( stack, mouseX, mouseY );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean mouseClicked( double x, double y, int button )
 | 
			
		||||
    {
 | 
			
		||||
        boolean changed = super.mouseClicked( x, y, button );
 | 
			
		||||
        // Clicking the terminate/shutdown button steals focus, which means then pressing "enter" will click the button
 | 
			
		||||
        // again. Restore the focus to the terminal in these cases.
 | 
			
		||||
        if( getFocused() instanceof DynamicImageButton ) setFocused( terminal );
 | 
			
		||||
        return changed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY )
 | 
			
		||||
    {
 | 
			
		||||
@@ -162,7 +173,6 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                buffer.rewind();
 | 
			
		||||
                toUpload.add( new FileUpload( name, buffer, digest ) );
 | 
			
		||||
            }
 | 
			
		||||
            catch( IOException e )
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import dan200.computercraft.ComputerCraft;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.WidgetTerminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ClientComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.ContainerComputerBase;
 | 
			
		||||
import net.minecraft.client.KeyMapping;
 | 
			
		||||
import net.minecraft.client.gui.Font;
 | 
			
		||||
import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
 | 
			
		||||
@@ -44,8 +45,11 @@ public class NoTermComputerScreen<T extends ContainerComputerBase> extends Scree
 | 
			
		||||
    protected void init()
 | 
			
		||||
    {
 | 
			
		||||
        passEvents = true; // Pass mouse vents through to the game's mouse handler.
 | 
			
		||||
        // First ensure we're still grabbing the mouse, so the user can look around. Then reset bits of state that
 | 
			
		||||
        // grabbing unsets.
 | 
			
		||||
        minecraft.mouseHandler.grabMouse();
 | 
			
		||||
        minecraft.screen = this;
 | 
			
		||||
        KeyMapping.releaseAll();
 | 
			
		||||
 | 
			
		||||
        super.init();
 | 
			
		||||
        minecraft.keyboardHandler.setSendRepeatsToGui( true );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										133
									
								
								src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import net.minecraft.client.sounds.AudioStream;
 | 
			
		||||
import org.lwjgl.BufferUtils;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.sound.sampled.AudioFormat;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.ByteOrder;
 | 
			
		||||
import java.util.ArrayDeque;
 | 
			
		||||
import java.util.Queue;
 | 
			
		||||
 | 
			
		||||
class DfpwmStream implements AudioStream
 | 
			
		||||
{
 | 
			
		||||
    public static final int SAMPLE_RATE = SpeakerPeripheral.SAMPLE_RATE;
 | 
			
		||||
 | 
			
		||||
    private static final int PREC = 10;
 | 
			
		||||
    private static final int LPF_STRENGTH = 140;
 | 
			
		||||
 | 
			
		||||
    private static final AudioFormat MONO_16 = new AudioFormat( SAMPLE_RATE, 16, 1, true, false );
 | 
			
		||||
 | 
			
		||||
    private final Queue<ByteBuffer> buffers = new ArrayDeque<>( 2 );
 | 
			
		||||
 | 
			
		||||
    private int charge = 0; // q
 | 
			
		||||
    private int strength = 0; // s
 | 
			
		||||
    private int lowPassCharge;
 | 
			
		||||
    private boolean previousBit = false;
 | 
			
		||||
 | 
			
		||||
    DfpwmStream()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void push( @Nonnull ByteBuf input )
 | 
			
		||||
    {
 | 
			
		||||
        int readable = input.readableBytes();
 | 
			
		||||
        ByteBuffer output = ByteBuffer.allocate( readable * 16 ).order( ByteOrder.nativeOrder() );
 | 
			
		||||
 | 
			
		||||
        for( int i = 0; i < readable; i++ )
 | 
			
		||||
        {
 | 
			
		||||
            byte inputByte = input.readByte();
 | 
			
		||||
            for( int j = 0; j < 8; j++ )
 | 
			
		||||
            {
 | 
			
		||||
                boolean currentBit = (inputByte & 1) != 0;
 | 
			
		||||
                int target = currentBit ? 127 : -128;
 | 
			
		||||
 | 
			
		||||
                // q' <- q + (s * (t - q) + 128)/256
 | 
			
		||||
                int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
 | 
			
		||||
                if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1;
 | 
			
		||||
 | 
			
		||||
                int z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
 | 
			
		||||
 | 
			
		||||
                int nextStrength = strength;
 | 
			
		||||
                if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1;
 | 
			
		||||
                if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8);
 | 
			
		||||
 | 
			
		||||
                // Apply antijerk
 | 
			
		||||
                int chargeWithAntijerk = currentBit == previousBit
 | 
			
		||||
                    ? nextCharge
 | 
			
		||||
                    : nextCharge + charge + 1 >> 1;
 | 
			
		||||
 | 
			
		||||
                // And low pass filter: outQ <- outQ + ((expectedOutput - outQ) x 140 / 256)
 | 
			
		||||
                lowPassCharge += ((chargeWithAntijerk - lowPassCharge) * LPF_STRENGTH + 0x80) >> 8;
 | 
			
		||||
 | 
			
		||||
                charge = nextCharge;
 | 
			
		||||
                strength = nextStrength;
 | 
			
		||||
                previousBit = currentBit;
 | 
			
		||||
 | 
			
		||||
                // Ideally we'd generate an 8-bit audio buffer. However, as we're piggybacking on top of another
 | 
			
		||||
                // audio stream (which uses 16 bit audio), we need to keep in the same format.
 | 
			
		||||
                output.putShort( (short) ((byte) (lowPassCharge & 0xFF) << 8) );
 | 
			
		||||
 | 
			
		||||
                inputByte >>= 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        output.flip();
 | 
			
		||||
        synchronized( this )
 | 
			
		||||
        {
 | 
			
		||||
            buffers.add( output );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public AudioFormat getFormat()
 | 
			
		||||
    {
 | 
			
		||||
        return MONO_16;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public synchronized ByteBuffer read( int capacity )
 | 
			
		||||
    {
 | 
			
		||||
        ByteBuffer result = BufferUtils.createByteBuffer( capacity );
 | 
			
		||||
        while( result.hasRemaining() )
 | 
			
		||||
        {
 | 
			
		||||
            ByteBuffer head = buffers.peek();
 | 
			
		||||
            if( head == null ) break;
 | 
			
		||||
 | 
			
		||||
            int toRead = Math.min( head.remaining(), result.remaining() );
 | 
			
		||||
            result.put( result.position(), head, head.position(), toRead );
 | 
			
		||||
            result.position( result.position() + toRead );
 | 
			
		||||
            head.position( head.position() + toRead );
 | 
			
		||||
 | 
			
		||||
            if( head.hasRemaining() ) break;
 | 
			
		||||
            buffers.remove();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result.flip();
 | 
			
		||||
 | 
			
		||||
        // This is naughty, but ensures we're not enqueuing empty buffers when the stream is exhausted.
 | 
			
		||||
        return result.remaining() == 0 ? null : result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() throws IOException
 | 
			
		||||
    {
 | 
			
		||||
        buffers.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isEmpty()
 | 
			
		||||
    {
 | 
			
		||||
        return buffers.isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,92 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.ComputerCraft;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
 | 
			
		||||
 */
 | 
			
		||||
public class SpeakerInstance
 | 
			
		||||
{
 | 
			
		||||
    public static final ResourceLocation DFPWM_STREAM = new ResourceLocation( ComputerCraft.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played" );
 | 
			
		||||
 | 
			
		||||
    private DfpwmStream currentStream;
 | 
			
		||||
    private SpeakerSound sound;
 | 
			
		||||
 | 
			
		||||
    SpeakerInstance()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized void pushAudio( ByteBuf buffer )
 | 
			
		||||
    {
 | 
			
		||||
        SpeakerSound sound = this.sound;
 | 
			
		||||
 | 
			
		||||
        DfpwmStream stream = currentStream;
 | 
			
		||||
        if( stream == null ) stream = currentStream = new DfpwmStream();
 | 
			
		||||
        boolean exhausted = stream.isEmpty();
 | 
			
		||||
        currentStream.push( buffer );
 | 
			
		||||
 | 
			
		||||
        // If we've got nothing left in the buffer, enqueue an additional one just in case.
 | 
			
		||||
        if( exhausted && sound != null && sound.stream == stream && sound.channel != null )
 | 
			
		||||
        {
 | 
			
		||||
            sound.executor.execute( () -> {
 | 
			
		||||
                if( !sound.channel.stopped() ) sound.channel.pumpBuffers( 1 );
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void playAudio( Vec3 position, float volume )
 | 
			
		||||
    {
 | 
			
		||||
        var soundManager = Minecraft.getInstance().getSoundManager();
 | 
			
		||||
 | 
			
		||||
        if( sound != null && sound.stream != currentStream )
 | 
			
		||||
        {
 | 
			
		||||
            soundManager.stop( sound );
 | 
			
		||||
            sound = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if( sound != null && !soundManager.isActive( sound ) ) sound = null;
 | 
			
		||||
 | 
			
		||||
        if( sound == null && currentStream != null )
 | 
			
		||||
        {
 | 
			
		||||
            sound = new SpeakerSound( DFPWM_STREAM, currentStream, position, volume, 1.0f );
 | 
			
		||||
            soundManager.play( sound );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void playSound( Vec3 position, ResourceLocation location, float volume, float pitch )
 | 
			
		||||
    {
 | 
			
		||||
        var soundManager = Minecraft.getInstance().getSoundManager();
 | 
			
		||||
        currentStream = null;
 | 
			
		||||
 | 
			
		||||
        if( sound != null )
 | 
			
		||||
        {
 | 
			
		||||
            soundManager.stop( sound );
 | 
			
		||||
            sound = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sound = new SpeakerSound( location, null, position, volume, pitch );
 | 
			
		||||
        soundManager.play( sound );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setPosition( Vec3 position )
 | 
			
		||||
    {
 | 
			
		||||
        if( sound != null ) sound.setPosition( position );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void stop()
 | 
			
		||||
    {
 | 
			
		||||
        if( sound != null ) Minecraft.getInstance().getSoundManager().stop( sound );
 | 
			
		||||
 | 
			
		||||
        currentStream = null;
 | 
			
		||||
        sound = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
import net.minecraftforge.api.distmarker.Dist;
 | 
			
		||||
import net.minecraftforge.client.event.sound.PlayStreamingSourceEvent;
 | 
			
		||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
 | 
			
		||||
import net.minecraftforge.fml.common.Mod;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Maps speakers source IDs to a {@link SpeakerInstance}.
 | 
			
		||||
 */
 | 
			
		||||
@Mod.EventBusSubscriber( Dist.CLIENT )
 | 
			
		||||
public class SpeakerManager
 | 
			
		||||
{
 | 
			
		||||
    private static final Map<UUID, SpeakerInstance> sounds = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    @SubscribeEvent
 | 
			
		||||
    public static void playStreaming( PlayStreamingSourceEvent event )
 | 
			
		||||
    {
 | 
			
		||||
        if( !(event.getSound() instanceof SpeakerSound sound) ) return;
 | 
			
		||||
        if( sound.stream == null ) return;
 | 
			
		||||
 | 
			
		||||
        event.getChannel().attachBufferStream( sound.stream );
 | 
			
		||||
        event.getChannel().play();
 | 
			
		||||
 | 
			
		||||
        sound.channel = event.getChannel();
 | 
			
		||||
        sound.executor = event.getEngine().executor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static SpeakerInstance getSound( UUID source )
 | 
			
		||||
    {
 | 
			
		||||
        return sounds.computeIfAbsent( source, x -> new SpeakerInstance() );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void stopSound( UUID source )
 | 
			
		||||
    {
 | 
			
		||||
        SpeakerInstance sound = sounds.remove( source );
 | 
			
		||||
        if( sound != null ) sound.stop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void moveSound( UUID source, Vec3 position )
 | 
			
		||||
    {
 | 
			
		||||
        SpeakerInstance sound = sounds.get( source );
 | 
			
		||||
        if( sound != null ) sound.setPosition( position );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void reset()
 | 
			
		||||
    {
 | 
			
		||||
        sounds.clear();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.client.sound;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.audio.Channel;
 | 
			
		||||
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
 | 
			
		||||
import net.minecraft.client.resources.sounds.TickableSoundInstance;
 | 
			
		||||
import net.minecraft.client.sounds.AudioStream;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundSource;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
 | 
			
		||||
public class SpeakerSound extends AbstractSoundInstance implements TickableSoundInstance
 | 
			
		||||
{
 | 
			
		||||
    Channel channel;
 | 
			
		||||
    Executor executor;
 | 
			
		||||
    DfpwmStream stream;
 | 
			
		||||
 | 
			
		||||
    SpeakerSound( ResourceLocation sound, DfpwmStream stream, Vec3 position, float volume, float pitch )
 | 
			
		||||
    {
 | 
			
		||||
        super( sound, SoundSource.RECORDS );
 | 
			
		||||
        setPosition( position );
 | 
			
		||||
        this.stream = stream;
 | 
			
		||||
        this.volume = volume;
 | 
			
		||||
        this.pitch = pitch;
 | 
			
		||||
        attenuation = Attenuation.LINEAR;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setPosition( Vec3 position )
 | 
			
		||||
    {
 | 
			
		||||
        x = (float) position.x();
 | 
			
		||||
        y = (float) position.y();
 | 
			
		||||
        z = (float) position.z();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isStopped()
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void tick()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public AudioStream getStream()
 | 
			
		||||
    {
 | 
			
		||||
        return stream;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -130,8 +130,6 @@ public final class Generator<T>
 | 
			
		||||
 | 
			
		||||
    private void addMethod( List<NamedMethod<T>> methods, Method method, LuaFunction annotation, PeripheralType genericType, T instance )
 | 
			
		||||
    {
 | 
			
		||||
        if( annotation.mainThread() ) instance = wrap.apply( instance );
 | 
			
		||||
 | 
			
		||||
        String[] names = annotation.value();
 | 
			
		||||
        boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
 | 
			
		||||
        if( names.length == 0 )
 | 
			
		||||
@@ -183,6 +181,13 @@ public final class Generator<T>
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LuaFunction annotation = method.getAnnotation( LuaFunction.class );
 | 
			
		||||
        if( annotation.unsafe() && annotation.mainThread() )
 | 
			
		||||
        {
 | 
			
		||||
            ComputerCraft.log.error( "Lua Method {} cannot use unsafe and mainThread", name );
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We have some rather ugly handling of static methods in both here and the main generate function. Static methods
 | 
			
		||||
        // only come from generic sources, so this should be safe.
 | 
			
		||||
        Class<?> target = Modifier.isStatic( modifiers ) ? method.getParameterTypes()[0] : method.getDeclaringClass();
 | 
			
		||||
@@ -190,11 +195,13 @@ public final class Generator<T>
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement();
 | 
			
		||||
            byte[] bytes = generate( className, target, method );
 | 
			
		||||
            byte[] bytes = generate( className, target, method, annotation.unsafe() );
 | 
			
		||||
            if( bytes == null ) return Optional.empty();
 | 
			
		||||
 | 
			
		||||
            Class<?> klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() );
 | 
			
		||||
            return Optional.of( klass.asSubclass( base ).getDeclaredConstructor().newInstance() );
 | 
			
		||||
 | 
			
		||||
            T instance = klass.asSubclass( base ).getDeclaredConstructor().newInstance();
 | 
			
		||||
            return Optional.of( annotation.mainThread() ? wrap.apply( instance ) : instance );
 | 
			
		||||
        }
 | 
			
		||||
        catch( ReflectiveOperationException | ClassFormatError | RuntimeException e )
 | 
			
		||||
        {
 | 
			
		||||
@@ -205,7 +212,7 @@ public final class Generator<T>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private byte[] generate( String className, Class<?> target, Method method )
 | 
			
		||||
    private byte[] generate( String className, Class<?> target, Method method, boolean unsafe )
 | 
			
		||||
    {
 | 
			
		||||
        String internalName = className.replace( ".", "/" );
 | 
			
		||||
 | 
			
		||||
@@ -238,7 +245,7 @@ public final class Generator<T>
 | 
			
		||||
            int argIndex = 0;
 | 
			
		||||
            for( java.lang.reflect.Type genericArg : method.getGenericParameterTypes() )
 | 
			
		||||
            {
 | 
			
		||||
                Boolean loadedArg = loadArg( mw, target, method, genericArg, argIndex );
 | 
			
		||||
                Boolean loadedArg = loadArg( mw, target, method, unsafe, genericArg, argIndex );
 | 
			
		||||
                if( loadedArg == null ) return null;
 | 
			
		||||
                if( loadedArg ) argIndex++;
 | 
			
		||||
            }
 | 
			
		||||
@@ -285,7 +292,7 @@ public final class Generator<T>
 | 
			
		||||
        return cw.toByteArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Boolean loadArg( MethodVisitor mw, Class<?> target, Method method, java.lang.reflect.Type genericArg, int argIndex )
 | 
			
		||||
    private Boolean loadArg( MethodVisitor mw, Class<?> target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex )
 | 
			
		||||
    {
 | 
			
		||||
        if( genericArg == target )
 | 
			
		||||
        {
 | 
			
		||||
@@ -324,7 +331,7 @@ public final class Generator<T>
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            String name = Reflect.getLuaName( Primitives.unwrap( klass ) );
 | 
			
		||||
            String name = Reflect.getLuaName( Primitives.unwrap( klass ), unsafe );
 | 
			
		||||
            if( name != null )
 | 
			
		||||
            {
 | 
			
		||||
                mw.visitVarInsn( ALOAD, 2 + context.size() );
 | 
			
		||||
@@ -344,7 +351,7 @@ public final class Generator<T>
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String name = arg == Object.class ? "" : Reflect.getLuaName( arg );
 | 
			
		||||
        String name = arg == Object.class ? "" : Reflect.getLuaName( arg, unsafe );
 | 
			
		||||
        if( name != null )
 | 
			
		||||
        {
 | 
			
		||||
            if( Reflect.getRawType( method, genericArg, false ) == null ) return null;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
package dan200.computercraft.core.asm;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.ComputerCraft;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaTable;
 | 
			
		||||
import org.objectweb.asm.MethodVisitor;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -25,7 +26,7 @@ final class Reflect
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    static String getLuaName( Class<?> klass )
 | 
			
		||||
    static String getLuaName( Class<?> klass, boolean unsafe )
 | 
			
		||||
    {
 | 
			
		||||
        if( klass.isPrimitive() )
 | 
			
		||||
        {
 | 
			
		||||
@@ -39,6 +40,7 @@ final class Reflect
 | 
			
		||||
            if( klass == Map.class ) return "Table";
 | 
			
		||||
            if( klass == String.class ) return "String";
 | 
			
		||||
            if( klass == ByteBuffer.class ) return "Bytes";
 | 
			
		||||
            if( klass == LuaTable.class && unsafe ) return "TableUnsafe";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,10 @@ class BasicFunction extends VarArgFunction
 | 
			
		||||
            }
 | 
			
		||||
            throw new LuaError( "Java Exception Thrown: " + t, 0 );
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            arguments.releaseImmediate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if( results.getCallback() != null )
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import dan200.computercraft.core.computer.TimeoutState;
 | 
			
		||||
import dan200.computercraft.core.tracking.Tracking;
 | 
			
		||||
import dan200.computercraft.core.tracking.TrackingField;
 | 
			
		||||
import dan200.computercraft.shared.util.ThreadUtils;
 | 
			
		||||
import org.squiddev.cobalt.LuaTable;
 | 
			
		||||
import org.squiddev.cobalt.*;
 | 
			
		||||
import org.squiddev.cobalt.compiler.CompileException;
 | 
			
		||||
import org.squiddev.cobalt.compiler.LoadState;
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,10 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
 | 
			
		||||
            }
 | 
			
		||||
            throw new LuaError( "Java Exception Thrown: " + t, 0 );
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            arguments.releaseImmediate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ILuaCallback callback = results.getCallback();
 | 
			
		||||
        Varargs ret = machine.toValues( results.getResult() );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										141
									
								
								src/main/java/dan200/computercraft/core/lua/TableImpl.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/main/java/dan200/computercraft/core/lua/TableImpl.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.core.lua;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaValues;
 | 
			
		||||
import org.squiddev.cobalt.*;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.api.lua.LuaValues.badTableItem;
 | 
			
		||||
import static dan200.computercraft.api.lua.LuaValues.getNumericType;
 | 
			
		||||
 | 
			
		||||
class TableImpl implements dan200.computercraft.api.lua.LuaTable<Object, Object>
 | 
			
		||||
{
 | 
			
		||||
    private final VarargArguments arguments;
 | 
			
		||||
    private final LuaTable table;
 | 
			
		||||
    private Map<Object, Object> backingMap;
 | 
			
		||||
 | 
			
		||||
    TableImpl( VarargArguments arguments, LuaTable table )
 | 
			
		||||
    {
 | 
			
		||||
        this.arguments = arguments;
 | 
			
		||||
        this.table = table;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int size()
 | 
			
		||||
    {
 | 
			
		||||
        checkValid();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return table.keyCount();
 | 
			
		||||
        }
 | 
			
		||||
        catch( LuaError e )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( e );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int length()
 | 
			
		||||
    {
 | 
			
		||||
        return table.length();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public long getLong( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        LuaValue value = table.rawget( index );
 | 
			
		||||
        if( !(value instanceof LuaNumber) ) throw LuaValues.badTableItem( index, "number", value.typeName() );
 | 
			
		||||
        if( value instanceof LuaInteger ) return value.toInteger();
 | 
			
		||||
 | 
			
		||||
        double number = value.toDouble();
 | 
			
		||||
        if( !Double.isFinite( number ) ) throw badTableItem( index, "number", getNumericType( number ) );
 | 
			
		||||
        return (long) number;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isEmpty()
 | 
			
		||||
    {
 | 
			
		||||
        checkValid();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return table.next( Constants.NIL ).first().isNil();
 | 
			
		||||
        }
 | 
			
		||||
        catch( LuaError e )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( e );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    private LuaValue getImpl( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        checkValid();
 | 
			
		||||
        if( o instanceof String ) return table.rawget( (String) o );
 | 
			
		||||
        if( o instanceof Integer ) return table.rawget( (Integer) o );
 | 
			
		||||
        return Constants.NIL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean containsKey( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        return !getImpl( o ).isNil();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object get( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        return CobaltLuaMachine.toObject( getImpl( o ), null );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    private Map<Object, Object> getBackingMap()
 | 
			
		||||
    {
 | 
			
		||||
        checkValid();
 | 
			
		||||
        if( backingMap != null ) return backingMap;
 | 
			
		||||
        return backingMap = Collections.unmodifiableMap(
 | 
			
		||||
            Objects.requireNonNull( (Map<?, ?>) CobaltLuaMachine.toObject( table, null ) )
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean containsValue( Object o )
 | 
			
		||||
    {
 | 
			
		||||
        return getBackingMap().containsKey( o );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Set<Object> keySet()
 | 
			
		||||
    {
 | 
			
		||||
        return getBackingMap().keySet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<Object> values()
 | 
			
		||||
    {
 | 
			
		||||
        return getBackingMap().values();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Set<Entry<Object, Object>> entrySet()
 | 
			
		||||
    {
 | 
			
		||||
        return getBackingMap().entrySet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void checkValid()
 | 
			
		||||
    {
 | 
			
		||||
        if( arguments.released )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( "Cannot use LuaTable after IArguments has been released" );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -19,6 +19,7 @@ class VarargArguments implements IArguments
 | 
			
		||||
{
 | 
			
		||||
    static final IArguments EMPTY = new VarargArguments( Constants.NONE );
 | 
			
		||||
 | 
			
		||||
    boolean released;
 | 
			
		||||
    private final Varargs varargs;
 | 
			
		||||
    private Object[] cache;
 | 
			
		||||
 | 
			
		||||
@@ -98,4 +99,39 @@ class VarargArguments implements IArguments
 | 
			
		||||
        LuaString str = ((LuaBaseString) value).strvalue();
 | 
			
		||||
        return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer() );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public dan200.computercraft.api.lua.LuaTable<?, ?> getTableUnsafe( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        if( released )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( "Cannot use getTableUnsafe after IArguments has been released" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LuaValue value = varargs.arg( index + 1 );
 | 
			
		||||
        if( !(value instanceof LuaTable) ) throw LuaValues.badArgument( index, "table", value.typeName() );
 | 
			
		||||
        return new TableImpl( this, (LuaTable) value );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<dan200.computercraft.api.lua.LuaTable<?, ?>> optTableUnsafe( int index ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        if( released )
 | 
			
		||||
        {
 | 
			
		||||
            throw new IllegalStateException( "Cannot use optTableUnsafe after IArguments has been released" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LuaValue value = varargs.arg( index + 1 );
 | 
			
		||||
        if( value.isNil() ) return Optional.empty();
 | 
			
		||||
        if( !(value instanceof LuaTable) ) throw LuaValues.badArgument( index, "table", value.typeName() );
 | 
			
		||||
        return Optional.of( new TableImpl( this, (LuaTable) value ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void releaseImmediate()
 | 
			
		||||
    {
 | 
			
		||||
        released = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,92 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.mixin;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import dan200.computercraft.shared.Registry;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.BlockCable;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
 | 
			
		||||
import dan200.computercraft.shared.util.WorldUtil;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.renderer.block.BlockModelShaper;
 | 
			
		||||
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
 | 
			
		||||
import net.minecraft.client.renderer.block.ModelBlockRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.texture.OverlayTexture;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.world.level.BlockAndTintGetter;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.HitResult;
 | 
			
		||||
import net.minecraftforge.client.model.data.IModelData;
 | 
			
		||||
import org.spongepowered.asm.mixin.Mixin;
 | 
			
		||||
import org.spongepowered.asm.mixin.Shadow;
 | 
			
		||||
import org.spongepowered.asm.mixin.injection.At;
 | 
			
		||||
import org.spongepowered.asm.mixin.injection.Inject;
 | 
			
		||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 | 
			
		||||
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides custom block breaking progress for modems, so it only applies to the current part.
 | 
			
		||||
 *
 | 
			
		||||
 * @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer, IModelData)
 | 
			
		||||
 */
 | 
			
		||||
@Mixin( BlockRenderDispatcher.class )
 | 
			
		||||
public class BlockRenderDispatcherMixin
 | 
			
		||||
{
 | 
			
		||||
    @Shadow
 | 
			
		||||
    private final Random random;
 | 
			
		||||
    @Shadow
 | 
			
		||||
    private final BlockModelShaper blockModelShaper;
 | 
			
		||||
    @Shadow
 | 
			
		||||
    private final ModelBlockRenderer modelRenderer;
 | 
			
		||||
 | 
			
		||||
    public BlockRenderDispatcherMixin( Random random, BlockModelShaper blockModelShaper, ModelBlockRenderer modelRenderer )
 | 
			
		||||
    {
 | 
			
		||||
        this.random = random;
 | 
			
		||||
        this.blockModelShaper = blockModelShaper;
 | 
			
		||||
        this.modelRenderer = modelRenderer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Inject(
 | 
			
		||||
        method = "name=/^renderBreakingTexture/ desc=/IModelData;\\)V$/",
 | 
			
		||||
        at = @At( "HEAD" ),
 | 
			
		||||
        cancellable = true,
 | 
			
		||||
        require = 0 // This isn't critical functionality, so don't worry if we can't apply it.
 | 
			
		||||
    )
 | 
			
		||||
    public void renderBlockDamage(
 | 
			
		||||
        BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack pose, VertexConsumer buffers, IModelData modelData,
 | 
			
		||||
        CallbackInfo info
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        // Only apply to cables which have both a cable and modem
 | 
			
		||||
        if( state.getBlock() != Registry.ModBlocks.CABLE.get()
 | 
			
		||||
            || !state.getValue( BlockCable.CABLE )
 | 
			
		||||
            || state.getValue( BlockCable.MODEM ) == CableModemVariant.None
 | 
			
		||||
        )
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        HitResult hit = Minecraft.getInstance().hitResult;
 | 
			
		||||
        if( hit == null || hit.getType() != HitResult.Type.BLOCK ) return;
 | 
			
		||||
        BlockPos hitPos = ((BlockHitResult) hit).getBlockPos();
 | 
			
		||||
 | 
			
		||||
        if( !hitPos.equals( pos ) ) return;
 | 
			
		||||
 | 
			
		||||
        info.cancel();
 | 
			
		||||
        BlockState newState = WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getLocation().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
 | 
			
		||||
            ? state.getBlock().defaultBlockState().setValue( BlockCable.MODEM, state.getValue( BlockCable.MODEM ) )
 | 
			
		||||
            : state.setValue( BlockCable.MODEM, CableModemVariant.None );
 | 
			
		||||
 | 
			
		||||
        BakedModel model = blockModelShaper.getBlockModel( newState );
 | 
			
		||||
        long seed = newState.getSeed( pos );
 | 
			
		||||
        modelRenderer.tesselateBlock( world, model, newState, pos, pose, buffers, true, random, seed, OverlayTexture.NO_OVERLAY, modelData );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -400,9 +400,6 @@ public final class Registry
 | 
			
		||||
 | 
			
		||||
        CauldronInteraction.WATER.put( ModItems.TURTLE_NORMAL.get(), ItemTurtle.CAULDRON_INTERACTION );
 | 
			
		||||
        CauldronInteraction.WATER.put( ModItems.TURTLE_ADVANCED.get(), ItemTurtle.CAULDRON_INTERACTION );
 | 
			
		||||
 | 
			
		||||
        // Mod integration code.
 | 
			
		||||
        // TODO: if( ModList.get().isLoaded( MoreRedIntegration.MOD_ID ) ) MoreRedIntegration.initialise();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerLoot()
 | 
			
		||||
 
 | 
			
		||||
@@ -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,11 +53,6 @@ public class FileSlice
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        bytes.rewind();
 | 
			
		||||
        file.position( offset );
 | 
			
		||||
        file.put( bytes );
 | 
			
		||||
        file.rewind();
 | 
			
		||||
 | 
			
		||||
        if( bytes.remaining() != 0 ) throw new IllegalStateException( "Should have read the whole buffer" );
 | 
			
		||||
        file.put( offset, bytes, bytes.position(), bytes.remaining() );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,123 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.shared.integration.morered;
 | 
			
		||||
 | 
			
		||||
import commoble.morered.api.ChanneledPowerSupplier;
 | 
			
		||||
import commoble.morered.api.MoreRedAPI;
 | 
			
		||||
import dan200.computercraft.ComputerCraft;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
 | 
			
		||||
import dan200.computercraft.shared.util.CapabilityUtil;
 | 
			
		||||
import dan200.computercraft.shared.util.FixedPointTileEntityType;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraftforge.common.MinecraftForge;
 | 
			
		||||
import net.minecraftforge.common.capabilities.Capability;
 | 
			
		||||
import net.minecraftforge.common.capabilities.ICapabilityProvider;
 | 
			
		||||
import net.minecraftforge.common.util.LazyOptional;
 | 
			
		||||
import net.minecraftforge.event.AttachCapabilitiesEvent;
 | 
			
		||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class MoreRedIntegration
 | 
			
		||||
{
 | 
			
		||||
    public static final String MOD_ID = "morered";
 | 
			
		||||
 | 
			
		||||
    private static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "morered" );
 | 
			
		||||
 | 
			
		||||
    private static final class BundledPowerSupplier implements ICapabilityProvider, ChanneledPowerSupplier
 | 
			
		||||
    {
 | 
			
		||||
        private final IBundledRedstoneBlock block;
 | 
			
		||||
        private final BlockEntity tile;
 | 
			
		||||
        private LazyOptional<ChanneledPowerSupplier> instance;
 | 
			
		||||
 | 
			
		||||
        private BundledPowerSupplier( IBundledRedstoneBlock block, BlockEntity tile )
 | 
			
		||||
        {
 | 
			
		||||
            this.block = block;
 | 
			
		||||
            this.tile = tile;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Nonnull
 | 
			
		||||
        @Override
 | 
			
		||||
        public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> cap, @Nullable Direction side )
 | 
			
		||||
        {
 | 
			
		||||
            if( cap != MoreRedAPI.CHANNELED_POWER_CAPABILITY ) return LazyOptional.empty();
 | 
			
		||||
 | 
			
		||||
            if( tile.isRemoved() || !block.getBundledRedstoneConnectivity( tile.getLevel(), tile.getBlockPos(), side ) )
 | 
			
		||||
            {
 | 
			
		||||
                return LazyOptional.empty();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (instance == null ? (instance = LazyOptional.of( () -> this )) : instance).cast();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public int getPowerOnChannel( @Nonnull Level world, @Nonnull BlockPos wirePos, @Nonnull BlockState wireState, @Nullable Direction wireFace, int channel )
 | 
			
		||||
        {
 | 
			
		||||
            if( wireFace == null ) return 0;
 | 
			
		||||
 | 
			
		||||
            BlockPos pos = wirePos.relative( wireFace );
 | 
			
		||||
            BlockState state = world.getBlockState( pos );
 | 
			
		||||
            if( !(state.getBlock() instanceof IBundledRedstoneBlock block) ) return 0;
 | 
			
		||||
 | 
			
		||||
            return (block.getBundledRedstoneOutput( world, pos, wireFace.getOpposite() ) & (1 << channel)) != 0 ? 31 : 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void invalidate()
 | 
			
		||||
        {
 | 
			
		||||
            instance = CapabilityUtil.invalidate( instance );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SubscribeEvent
 | 
			
		||||
    public static void attachBlockCapabilities( AttachCapabilitiesEvent<BlockEntity> event )
 | 
			
		||||
    {
 | 
			
		||||
        BlockEntity tile = event.getObject();
 | 
			
		||||
        if( !(tile.getType() instanceof FixedPointTileEntityType) ) return;
 | 
			
		||||
 | 
			
		||||
        Block block = ((FixedPointTileEntityType<?>) tile.getType()).getBlock();
 | 
			
		||||
        if( !(block instanceof IBundledRedstoneBlock) ) return;
 | 
			
		||||
 | 
			
		||||
        BundledPowerSupplier provider = new BundledPowerSupplier( (IBundledRedstoneBlock) block, tile );
 | 
			
		||||
        event.addCapability( ID, provider );
 | 
			
		||||
        event.addListener( provider::invalidate );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void initialise()
 | 
			
		||||
    {
 | 
			
		||||
        MinecraftForge.EVENT_BUS.register( MoreRedIntegration.class );
 | 
			
		||||
        ComputerCraftAPI.registerBundledRedstoneProvider( MoreRedIntegration::getBundledPower );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getBundledPower( Level world, BlockPos pos, Direction side )
 | 
			
		||||
    {
 | 
			
		||||
        BlockEntity tile = world.getBlockEntity( pos );
 | 
			
		||||
        if( tile == null ) return -1;
 | 
			
		||||
 | 
			
		||||
        ChanneledPowerSupplier power = CapabilityUtil.unwrapUnsafe( tile.getCapability( MoreRedAPI.CHANNELED_POWER_CAPABILITY, side ) );
 | 
			
		||||
        if( power == null ) return -1;
 | 
			
		||||
 | 
			
		||||
        BlockState state = tile.getBlockState();
 | 
			
		||||
 | 
			
		||||
        // Skip ones already handled by CC. We can do this more efficiently.
 | 
			
		||||
        if( state.getBlock() instanceof IBundledRedstoneBlock ) return -1;
 | 
			
		||||
 | 
			
		||||
        int mask = 0;
 | 
			
		||||
        for( int i = 0; i < 16; i++ )
 | 
			
		||||
        {
 | 
			
		||||
            mask |= power.getPowerOnChannel( world, pos, state, side, i ) > 0 ? (1 << i) : 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return mask;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -57,10 +57,11 @@ public final class NetworkHandler
 | 
			
		||||
        registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new );
 | 
			
		||||
        registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new );
 | 
			
		||||
        registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new );
 | 
			
		||||
        registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
 | 
			
		||||
        registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
 | 
			
		||||
        registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
 | 
			
		||||
        registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new );
 | 
			
		||||
        registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new );
 | 
			
		||||
        registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
 | 
			
		||||
        registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
 | 
			
		||||
        registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
 | 
			
		||||
        registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new );
 | 
			
		||||
        registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.sound.SpeakerManager;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
import net.minecraftforge.api.distmarker.Dist;
 | 
			
		||||
import net.minecraftforge.api.distmarker.OnlyIn;
 | 
			
		||||
import net.minecraftforge.network.NetworkEvent;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Starts a sound on the client.
 | 
			
		||||
 *
 | 
			
		||||
 * Used by speakers to play sounds.
 | 
			
		||||
 *
 | 
			
		||||
 * @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker
 | 
			
		||||
 */
 | 
			
		||||
public class SpeakerAudioClientMessage implements NetworkMessage
 | 
			
		||||
{
 | 
			
		||||
    private final UUID source;
 | 
			
		||||
    private final Vec3 pos;
 | 
			
		||||
    private final ByteBuffer content;
 | 
			
		||||
    private final float volume;
 | 
			
		||||
 | 
			
		||||
    public SpeakerAudioClientMessage( UUID source, Vec3 pos, float volume, ByteBuffer content )
 | 
			
		||||
    {
 | 
			
		||||
        this.source = source;
 | 
			
		||||
        this.pos = pos;
 | 
			
		||||
        this.content = content;
 | 
			
		||||
        this.volume = volume;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SpeakerAudioClientMessage( FriendlyByteBuf buf )
 | 
			
		||||
    {
 | 
			
		||||
        source = buf.readUUID();
 | 
			
		||||
        pos = new Vec3( buf.readDouble(), buf.readDouble(), buf.readDouble() );
 | 
			
		||||
        volume = buf.readFloat();
 | 
			
		||||
 | 
			
		||||
        SpeakerManager.getSound( source ).pushAudio( buf );
 | 
			
		||||
        content = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void toBytes( @Nonnull FriendlyByteBuf buf )
 | 
			
		||||
    {
 | 
			
		||||
        buf.writeUUID( source );
 | 
			
		||||
        buf.writeDouble( pos.x() );
 | 
			
		||||
        buf.writeDouble( pos.y() );
 | 
			
		||||
        buf.writeDouble( pos.z() );
 | 
			
		||||
        buf.writeFloat( volume );
 | 
			
		||||
        buf.writeBytes( content.duplicate() );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    @OnlyIn( Dist.CLIENT )
 | 
			
		||||
    public void handle( NetworkEvent.Context context )
 | 
			
		||||
    {
 | 
			
		||||
        SpeakerManager.getSound( source ).playAudio( pos, volume );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.SoundManager;
 | 
			
		||||
import dan200.computercraft.client.sound.SpeakerManager;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
@@ -53,6 +53,6 @@ public class SpeakerMoveClientMessage implements NetworkMessage
 | 
			
		||||
    @OnlyIn( Dist.CLIENT )
 | 
			
		||||
    public void handle( NetworkEvent.Context context )
 | 
			
		||||
    {
 | 
			
		||||
        SoundManager.moveSound( source, pos );
 | 
			
		||||
        SpeakerManager.moveSound( source, pos );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.SoundManager;
 | 
			
		||||
import dan200.computercraft.client.sound.SpeakerManager;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
@@ -66,6 +66,6 @@ public class SpeakerPlayClientMessage implements NetworkMessage
 | 
			
		||||
    @OnlyIn( Dist.CLIENT )
 | 
			
		||||
    public void handle( NetworkEvent.Context context )
 | 
			
		||||
    {
 | 
			
		||||
        SoundManager.playSound( source, pos, sound, volume, pitch );
 | 
			
		||||
        SpeakerManager.getSound( source ).playSound( pos, sound, volume, pitch );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.shared.network.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.SoundManager;
 | 
			
		||||
import dan200.computercraft.client.sound.SpeakerManager;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkMessage;
 | 
			
		||||
import net.minecraft.network.FriendlyByteBuf;
 | 
			
		||||
import net.minecraftforge.api.distmarker.Dist;
 | 
			
		||||
@@ -46,6 +46,6 @@ public class SpeakerStopClientMessage implements NetworkMessage
 | 
			
		||||
    @OnlyIn( Dist.CLIENT )
 | 
			
		||||
    public void handle( NetworkEvent.Context context )
 | 
			
		||||
    {
 | 
			
		||||
        SoundManager.stopSound( source );
 | 
			
		||||
        SpeakerManager.stopSound( source );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -94,9 +94,12 @@ class Expander
 | 
			
		||||
        if( !isPositive )
 | 
			
		||||
        {
 | 
			
		||||
            BlockEntity otherOrigin = level.getBlockEntity( otherMonitor.toWorldPos( 0, 0 ) );
 | 
			
		||||
            if( otherOrigin == null || !origin.isCompatible( (TileMonitor) otherOrigin ) ) return false;
 | 
			
		||||
            if( !(otherOrigin instanceof TileMonitor originMonitor) || !origin.isCompatible( originMonitor ) )
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            origin = (TileMonitor) otherOrigin;
 | 
			
		||||
            origin = originMonitor;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.width = width;
 | 
			
		||||
 
 | 
			
		||||
@@ -140,13 +140,6 @@ public class TileMonitor extends TileGeneric
 | 
			
		||||
    @Override
 | 
			
		||||
    public void load( @Nonnull CompoundTag nbt )
 | 
			
		||||
    {
 | 
			
		||||
        if( level != null && level.isClientSide )
 | 
			
		||||
        {
 | 
			
		||||
            // TODO: Remove once https://github.com/MinecraftForge/MinecraftForge/pull/8237 is merged.
 | 
			
		||||
            handleUpdateTag( nbt );
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        super.load( nbt );
 | 
			
		||||
 | 
			
		||||
        xIndex = nbt.getInt( NBT_X );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of ComputerCraft - http://www.computercraft.info
 | 
			
		||||
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 | 
			
		||||
 * Send enquiries to dratcliffe@gmail.com
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.shared.peripheral.speaker;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaTable;
 | 
			
		||||
import dan200.computercraft.shared.util.PauseAwareTimer;
 | 
			
		||||
import net.minecraft.util.Mth;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral.SAMPLE_RATE;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Internal state of the DFPWM decoder and the state of playback.
 | 
			
		||||
 */
 | 
			
		||||
class DfpwmState
 | 
			
		||||
{
 | 
			
		||||
    private static final long SECOND = TimeUnit.SECONDS.toNanos( 1 );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The minimum size of the client's audio buffer. Once we have less than this on the client, we should send another
 | 
			
		||||
     * batch of audio.
 | 
			
		||||
     */
 | 
			
		||||
    private static final long CLIENT_BUFFER = (long) (SECOND * 0.5);
 | 
			
		||||
 | 
			
		||||
    private static final int PREC = 10;
 | 
			
		||||
 | 
			
		||||
    private int charge = 0; // q
 | 
			
		||||
    private int strength = 0; // s
 | 
			
		||||
    private boolean previousBit = false;
 | 
			
		||||
 | 
			
		||||
    private boolean unplayed = true;
 | 
			
		||||
    private long clientEndTime = PauseAwareTimer.getTime();
 | 
			
		||||
    private float pendingVolume = 1.0f;
 | 
			
		||||
    private ByteBuffer pendingAudio;
 | 
			
		||||
 | 
			
		||||
    synchronized boolean pushBuffer( LuaTable<?, ?> table, int size, @Nonnull Optional<Double> volume ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        if( pendingAudio != null ) return false;
 | 
			
		||||
 | 
			
		||||
        int outSize = size / 8;
 | 
			
		||||
        ByteBuffer buffer = ByteBuffer.allocate( outSize );
 | 
			
		||||
 | 
			
		||||
        for( int i = 0; i < outSize; i++ )
 | 
			
		||||
        {
 | 
			
		||||
            int thisByte = 0;
 | 
			
		||||
            for( int j = 1; j <= 8; j++ )
 | 
			
		||||
            {
 | 
			
		||||
                int level = table.getInt( i * 8 + j );
 | 
			
		||||
                if( level < -128 || level > 127 )
 | 
			
		||||
                {
 | 
			
		||||
                    throw new LuaException( "table item #" + (i * 8 + j) + " must be between -128 and 127" );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                boolean currentBit = level > charge || (level == charge && charge == 127);
 | 
			
		||||
 | 
			
		||||
                // Identical to DfpwmStream. Not happy with this, but saves some inheritance.
 | 
			
		||||
                int target = currentBit ? 127 : -128;
 | 
			
		||||
 | 
			
		||||
                // q' <- q + (s * (t - q) + 128)/256
 | 
			
		||||
                int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
 | 
			
		||||
                if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1;
 | 
			
		||||
 | 
			
		||||
                int z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
 | 
			
		||||
 | 
			
		||||
                int nextStrength = strength;
 | 
			
		||||
                if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1;
 | 
			
		||||
                if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8);
 | 
			
		||||
 | 
			
		||||
                charge = nextCharge;
 | 
			
		||||
                strength = nextStrength;
 | 
			
		||||
                previousBit = currentBit;
 | 
			
		||||
 | 
			
		||||
                thisByte = (thisByte >> 1) + (currentBit ? 128 : 0);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            buffer.put( (byte) thisByte );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        buffer.flip();
 | 
			
		||||
 | 
			
		||||
        pendingAudio = buffer;
 | 
			
		||||
        pendingVolume = Mth.clamp( volume.orElse( (double) pendingVolume ).floatValue(), 0.0f, 3.0f );
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean shouldSendPending( long now )
 | 
			
		||||
    {
 | 
			
		||||
        return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ByteBuffer pullPending( long now )
 | 
			
		||||
    {
 | 
			
		||||
        ByteBuffer audio = pendingAudio;
 | 
			
		||||
        pendingAudio = null;
 | 
			
		||||
        // Compute when we should consider sending the next packet.
 | 
			
		||||
        clientEndTime = Math.max( now, clientEndTime ) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
 | 
			
		||||
        unplayed = false;
 | 
			
		||||
        return audio;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean isPlaying()
 | 
			
		||||
    {
 | 
			
		||||
        return unplayed || clientEndTime >= PauseAwareTimer.getTime();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    float getVolume()
 | 
			
		||||
    {
 | 
			
		||||
        return pendingVolume;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,77 +9,177 @@ import dan200.computercraft.ComputerCraft;
 | 
			
		||||
import dan200.computercraft.api.lua.ILuaContext;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaTable;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IComputerAccess;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.network.NetworkHandler;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerAudioClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
 | 
			
		||||
import dan200.computercraft.shared.util.PauseAwareTimer;
 | 
			
		||||
import net.minecraft.ResourceLocationException;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.network.protocol.game.ClientboundCustomSoundPacket;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.sounds.SoundSource;
 | 
			
		||||
import net.minecraft.util.Mth;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Speakers allow playing notes and other sounds.
 | 
			
		||||
 * The speaker peirpheral allow your computer to play notes and other sounds.
 | 
			
		||||
 *
 | 
			
		||||
 * The speaker can play three kinds of sound, in increasing orders of complexity:
 | 
			
		||||
 * - {@link #playNote} allows you to play noteblock note.
 | 
			
		||||
 * - {@link #playSound} plays any built-in Minecraft sound, such as block sounds or mob noises.
 | 
			
		||||
 * - {@link #playAudio} can play arbitrary audio.
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.module speaker
 | 
			
		||||
 * @cc.since 1.80pr1
 | 
			
		||||
 */
 | 
			
		||||
public abstract class SpeakerPeripheral implements IPeripheral
 | 
			
		||||
{
 | 
			
		||||
    private static final int MIN_TICKS_BETWEEN_SOUNDS = 1;
 | 
			
		||||
    /**
 | 
			
		||||
     * Number of samples/s in a dfpwm1a audio track.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int SAMPLE_RATE = 48000;
 | 
			
		||||
 | 
			
		||||
    private final UUID source = UUID.randomUUID();
 | 
			
		||||
    private final Set<IComputerAccess> computers = Collections.newSetFromMap( new HashMap<>() );
 | 
			
		||||
 | 
			
		||||
    private long clock = 0;
 | 
			
		||||
    private long lastPlayTime = 0;
 | 
			
		||||
    private final AtomicInteger notesThisTick = new AtomicInteger();
 | 
			
		||||
 | 
			
		||||
    private long lastPositionTime;
 | 
			
		||||
    private Vec3 lastPosition;
 | 
			
		||||
 | 
			
		||||
    private long lastPlayTime;
 | 
			
		||||
 | 
			
		||||
    private final List<PendingSound> pendingNotes = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    private final Object lock = new Object();
 | 
			
		||||
    private boolean shouldStop;
 | 
			
		||||
    private PendingSound pendingSound = null;
 | 
			
		||||
    private DfpwmState dfpwmState;
 | 
			
		||||
 | 
			
		||||
    public void update()
 | 
			
		||||
    {
 | 
			
		||||
        clock++;
 | 
			
		||||
        notesThisTick.set( 0 );
 | 
			
		||||
 | 
			
		||||
        Vec3 pos = getPosition();
 | 
			
		||||
        Level level = getLevel();
 | 
			
		||||
        if( level == null ) return;
 | 
			
		||||
        MinecraftServer server = level.getServer();
 | 
			
		||||
 | 
			
		||||
        synchronized( pendingNotes )
 | 
			
		||||
        {
 | 
			
		||||
            for( PendingSound sound : pendingNotes )
 | 
			
		||||
            {
 | 
			
		||||
                lastPlayTime = clock;
 | 
			
		||||
                server.getPlayerList().broadcast(
 | 
			
		||||
                    null, pos.x, pos.y, pos.z, sound.volume * 16, level.dimension(),
 | 
			
		||||
                    new ClientboundCustomSoundPacket( sound.location, SoundSource.RECORDS, pos, sound.volume, sound.pitch )
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            pendingNotes.clear();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The audio dispatch logic here is pretty messy, which I'm not proud of. The general logic here is that we hold
 | 
			
		||||
        // the main "lock" when modifying the dfpwmState/pendingSound variables and no other time.
 | 
			
		||||
        // dfpwmState will only ever transition from having a buffer to not having a buffer on the main thread (so this
 | 
			
		||||
        // method), so we don't need to bother locking that.
 | 
			
		||||
        boolean shouldStop;
 | 
			
		||||
        PendingSound sound;
 | 
			
		||||
        DfpwmState dfpwmState;
 | 
			
		||||
        synchronized( lock )
 | 
			
		||||
        {
 | 
			
		||||
            sound = pendingSound;
 | 
			
		||||
            dfpwmState = this.dfpwmState;
 | 
			
		||||
            pendingSound = null;
 | 
			
		||||
 | 
			
		||||
            shouldStop = this.shouldStop;
 | 
			
		||||
            if( shouldStop )
 | 
			
		||||
            {
 | 
			
		||||
                dfpwmState = this.dfpwmState = null;
 | 
			
		||||
                sound = null;
 | 
			
		||||
                this.shouldStop = false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Stop the speaker and nuke the position, so we don't update it again.
 | 
			
		||||
        if( shouldStop && lastPosition != null )
 | 
			
		||||
        {
 | 
			
		||||
            lastPosition = null;
 | 
			
		||||
            NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) );
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        long now = PauseAwareTimer.getTime();
 | 
			
		||||
        if( sound != null )
 | 
			
		||||
        {
 | 
			
		||||
            lastPlayTime = clock;
 | 
			
		||||
            NetworkHandler.sendToAllAround(
 | 
			
		||||
                new SpeakerPlayClientMessage( getSource(), pos, sound.location, sound.volume, sound.pitch ),
 | 
			
		||||
                level, pos, sound.volume * 16
 | 
			
		||||
            );
 | 
			
		||||
            syncedPosition( pos );
 | 
			
		||||
        }
 | 
			
		||||
        else if( dfpwmState != null && dfpwmState.shouldSendPending( now ) )
 | 
			
		||||
        {
 | 
			
		||||
            // If clients need to receive another batch of audio, send it and then notify computers our internal buffer is
 | 
			
		||||
            // free again.
 | 
			
		||||
            NetworkHandler.sendToAllTracking(
 | 
			
		||||
                new SpeakerAudioClientMessage( getSource(), pos, dfpwmState.getVolume(), dfpwmState.pullPending( now ) ),
 | 
			
		||||
                getLevel().getChunkAt( new BlockPos( pos ) )
 | 
			
		||||
            );
 | 
			
		||||
            syncedPosition( pos );
 | 
			
		||||
 | 
			
		||||
            // And notify computers that we have space for more audio.
 | 
			
		||||
            for( IComputerAccess computer : computers )
 | 
			
		||||
            {
 | 
			
		||||
                computer.queueEvent( "speaker_audio_empty", computer.getAttachmentName() );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Push position updates to any speakers which have ever played a note,
 | 
			
		||||
        // have moved by a non-trivial amount and haven't had a position update
 | 
			
		||||
        // in the last second.
 | 
			
		||||
        if( lastPlayTime > 0 && (clock - lastPositionTime) >= 20 )
 | 
			
		||||
        if( lastPosition != null && (clock - lastPositionTime) >= 20 )
 | 
			
		||||
        {
 | 
			
		||||
            Vec3 position = getPosition();
 | 
			
		||||
            if( lastPosition == null || lastPosition.distanceToSqr( position ) >= 0.1 )
 | 
			
		||||
            if( lastPosition.distanceToSqr( position ) >= 0.1 )
 | 
			
		||||
            {
 | 
			
		||||
                lastPosition = position;
 | 
			
		||||
                lastPositionTime = clock;
 | 
			
		||||
                NetworkHandler.sendToAllTracking(
 | 
			
		||||
                    new SpeakerMoveClientMessage( getSource(), position ),
 | 
			
		||||
                    getLevel().getChunkAt( new BlockPos( position ) )
 | 
			
		||||
                );
 | 
			
		||||
                syncedPosition( position );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public abstract Level getLevel();
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    public abstract Vec3 getPosition();
 | 
			
		||||
 | 
			
		||||
    protected abstract UUID getSource();
 | 
			
		||||
 | 
			
		||||
    public boolean madeSound( long ticks )
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    public UUID getSource()
 | 
			
		||||
    {
 | 
			
		||||
        return clock - lastPlayTime <= ticks;
 | 
			
		||||
        return source;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean madeSound()
 | 
			
		||||
    {
 | 
			
		||||
        DfpwmState state = dfpwmState;
 | 
			
		||||
        return clock - lastPlayTime <= 20 || (state != null && state.isPlaying());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
@@ -90,18 +190,81 @@ public abstract class SpeakerPeripheral implements IPeripheral
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plays a sound through the speaker.
 | 
			
		||||
     * Plays a note block note through the speaker.
 | 
			
		||||
     *
 | 
			
		||||
     * This plays sounds similar to the {@code /playsound} command in Minecraft.
 | 
			
		||||
     * It takes the namespaced path of a sound (e.g. {@code minecraft:block.note_block.harp})
 | 
			
		||||
     * with an optional volume and speed multiplier, and plays it through the speaker.
 | 
			
		||||
     * This takes the name of a note to play, as well as optionally the volume
 | 
			
		||||
     * and pitch to play the note at.
 | 
			
		||||
     *
 | 
			
		||||
     * The pitch argument uses semitones as the unit. This directly maps to the
 | 
			
		||||
     * number of clicks on a note block. For reference, 0, 12, and 24 map to F#,
 | 
			
		||||
     * and 6 and 18 map to C.
 | 
			
		||||
     *
 | 
			
		||||
     * A maximum of 8 notes can be played in a single tick. If this limit is hit, this function will return
 | 
			
		||||
     * {@literal false}.
 | 
			
		||||
     *
 | 
			
		||||
     * ### Valid instruments
 | 
			
		||||
     * The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments).
 | 
			
		||||
     * These are:
 | 
			
		||||
     *
 | 
			
		||||
     * {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, @code "flute"},
 | 
			
		||||
     * {@code "bell"}, {@code "guitar"}, {@code "chime"}, {@code "xylophone"}, {@code "iron_xylophone"},
 | 
			
		||||
     * {@code "cow_bell"}, {@code "didgeridoo"}, {@code "bit"}, {@code "banjo"} and {@code "pling"}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context     The Lua context
 | 
			
		||||
     * @param instrumentA The instrument to use to play this note.
 | 
			
		||||
     * @param volumeA     The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0.
 | 
			
		||||
     * @param pitchA      The pitch to play the note at in semitones, from 0 to 24. Defaults to 12.
 | 
			
		||||
     * @return Whether the note could be played as the limit was reached.
 | 
			
		||||
     * @throws LuaException If the instrument doesn't exist.
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final boolean playNote( ILuaContext context, String instrumentA, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) );
 | 
			
		||||
        float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) );
 | 
			
		||||
 | 
			
		||||
        NoteBlockInstrument instrument = null;
 | 
			
		||||
        for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
 | 
			
		||||
        {
 | 
			
		||||
            if( testInstrument.getSerializedName().equalsIgnoreCase( instrumentA ) )
 | 
			
		||||
            {
 | 
			
		||||
                instrument = testInstrument;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the note exists
 | 
			
		||||
        if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + instrument + "\"!" );
 | 
			
		||||
 | 
			
		||||
        synchronized( pendingNotes )
 | 
			
		||||
        {
 | 
			
		||||
            if( pendingNotes.size() >= ComputerCraft.maxNotesPerTick ) return false;
 | 
			
		||||
            pendingNotes.add( new PendingSound( instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ) ) );
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plays a Minecraft sound through the speaker.
 | 
			
		||||
     *
 | 
			
		||||
     * This takes the [name of a Minecraft sound](https://minecraft.fandom.com/wiki/Sounds.json), such as
 | 
			
		||||
     * {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch.
 | 
			
		||||
     *
 | 
			
		||||
     * Only one sound can be played at once. This function will return {@literal false} if another sound was started
 | 
			
		||||
     * this tick, or if some {@link #playAudio audio} is still playing.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The Lua context
 | 
			
		||||
     * @param name    The name of the sound to play.
 | 
			
		||||
     * @param volumeA The volume to play the sound at, from 0.0 to 3.0. Defaults to 1.0.
 | 
			
		||||
     * @param pitchA  The speed to play the sound at, from 0.5 to 2.0. Defaults to 1.0.
 | 
			
		||||
     * @return Whether the sound could be played.
 | 
			
		||||
     * @throws LuaException If the sound name couldn't be decoded.
 | 
			
		||||
     * @throws LuaException If the sound name was invalid.
 | 
			
		||||
     * @cc.usage Play a creeper hiss with the speaker.
 | 
			
		||||
     *
 | 
			
		||||
     * <pre>{@code
 | 
			
		||||
     * local speaker = peripheral.find("speaker")
 | 
			
		||||
     * speaker.playSound("entity.creeper.primed")
 | 
			
		||||
     * }</pre>
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final boolean playSound( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
 | 
			
		||||
@@ -119,89 +282,113 @@ public abstract class SpeakerPeripheral implements IPeripheral
 | 
			
		||||
            throw new LuaException( "Malformed sound name '" + name + "' " );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return playSound( context, identifier, volume, pitch, false );
 | 
			
		||||
        synchronized( lock )
 | 
			
		||||
        {
 | 
			
		||||
            if( dfpwmState != null && dfpwmState.isPlaying() ) return false;
 | 
			
		||||
            dfpwmState = null;
 | 
			
		||||
            pendingSound = new PendingSound( identifier, volume, pitch );
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plays a note block note through the speaker.
 | 
			
		||||
     * Attempt to stream some audio data to the speaker.
 | 
			
		||||
     *
 | 
			
		||||
     * This takes the name of a note to play, as well as optionally the volume
 | 
			
		||||
     * and pitch to play the note at.
 | 
			
		||||
     * This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer
 | 
			
		||||
     * and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for
 | 
			
		||||
     * a @{speaker_audio_empty} event before trying again.
 | 
			
		||||
     *
 | 
			
		||||
     * The pitch argument uses semitones as the unit. This directly maps to the
 | 
			
		||||
     * number of clicks on a note block. For reference, 0, 12, and 24 map to F#,
 | 
			
		||||
     * and 6 and 18 map to C.
 | 
			
		||||
     * :::note
 | 
			
		||||
     * The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
 | 
			
		||||
     * number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
 | 
			
		||||
     * (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
 | 
			
		||||
     * computer is lagging.
 | 
			
		||||
     * :::
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The Lua context
 | 
			
		||||
     * @param name    The name of the note to play.
 | 
			
		||||
     * @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0.
 | 
			
		||||
     * @param pitchA  The pitch to play the note at in semitones, from 0 to 24. Defaults to 12.
 | 
			
		||||
     * @return Whether the note could be played.
 | 
			
		||||
     * @throws LuaException If the instrument doesn't exist.
 | 
			
		||||
     * {@literal @}{speaker_audio} provides a more complete guide in to using speakers
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The Lua context.
 | 
			
		||||
     * @param audio   The audio data to play.
 | 
			
		||||
     * @param volume  The volume to play this audio at.
 | 
			
		||||
     * @return If there was room to accept this audio data.
 | 
			
		||||
     * @throws LuaException If the audio data is malformed.
 | 
			
		||||
     * @cc.tparam {number...} audio A list of amplitudes.
 | 
			
		||||
     * @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume
 | 
			
		||||
     * given to {@link #playAudio}.
 | 
			
		||||
     * @cc.since 1.100
 | 
			
		||||
     * @cc.usage Read an audio file, decode it using @{cc.audio.dfpwm}, and play it using the speaker.
 | 
			
		||||
     *
 | 
			
		||||
     * <pre>{@code
 | 
			
		||||
     * local dfpwm = require("cc.audio.dfpwm")
 | 
			
		||||
     * local speaker = peripheral.find("speaker")
 | 
			
		||||
     *
 | 
			
		||||
     * local decoder = dfpwm.make_decoder()
 | 
			
		||||
     * for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
 | 
			
		||||
     *     local buffer = decoder(chunk)
 | 
			
		||||
     *
 | 
			
		||||
     *     while not speaker.playAudio(buffer) do
 | 
			
		||||
     *         os.pullEvent("speaker_audio_empty")
 | 
			
		||||
     *     end
 | 
			
		||||
     * end
 | 
			
		||||
     * }</pre>
 | 
			
		||||
     * @cc.see cc.audio.dfpwm Provides utilities for decoding DFPWM audio files into a format which can be played by
 | 
			
		||||
     * the speaker.
 | 
			
		||||
     * @cc.see speaker_audio For a more complete introduction to the {@link #playAudio} function.
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction( unsafe = true )
 | 
			
		||||
    public final boolean playAudio( ILuaContext context, LuaTable<?, ?> audio, Optional<Double> volume ) throws LuaException
 | 
			
		||||
    {
 | 
			
		||||
        checkFinite( 1, volume.orElse( 0.0 ) );
 | 
			
		||||
 | 
			
		||||
        // TODO: Use ArgumentHelpers instead?
 | 
			
		||||
        int length = audio.length();
 | 
			
		||||
        if( length <= 0 ) throw new LuaException( "Cannot play empty audio" );
 | 
			
		||||
        if( length > 128 * 1024 ) throw new LuaException( "Audio data is too large" );
 | 
			
		||||
 | 
			
		||||
        DfpwmState state;
 | 
			
		||||
        synchronized( lock )
 | 
			
		||||
        {
 | 
			
		||||
            if( dfpwmState == null || !dfpwmState.isPlaying() ) dfpwmState = new DfpwmState();
 | 
			
		||||
            state = dfpwmState;
 | 
			
		||||
 | 
			
		||||
            pendingSound = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return state.pushBuffer( audio, length, volume );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Stop all audio being played by this speaker.
 | 
			
		||||
     *
 | 
			
		||||
     * This clears any audio that {@link #playAudio} had queued and stops the latest sound played by {@link #playSound}.
 | 
			
		||||
     *
 | 
			
		||||
     * @cc.since 1.100
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final synchronized boolean playNote( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
 | 
			
		||||
    public final void stop()
 | 
			
		||||
    {
 | 
			
		||||
        float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) );
 | 
			
		||||
        float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) );
 | 
			
		||||
 | 
			
		||||
        NoteBlockInstrument instrument = null;
 | 
			
		||||
        for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
 | 
			
		||||
        {
 | 
			
		||||
            if( testInstrument.getSerializedName().equalsIgnoreCase( name ) )
 | 
			
		||||
            {
 | 
			
		||||
                instrument = testInstrument;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the note exists
 | 
			
		||||
        if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + name + "\"!" );
 | 
			
		||||
 | 
			
		||||
        // If the resource location for note block notes changes, this method call will need to be updated
 | 
			
		||||
        boolean success = playSound( context, instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true );
 | 
			
		||||
        if( success ) notesThisTick.incrementAndGet();
 | 
			
		||||
        return success;
 | 
			
		||||
        shouldStop = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException
 | 
			
		||||
    private void syncedPosition( Vec3 position )
 | 
			
		||||
    {
 | 
			
		||||
        if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS )
 | 
			
		||||
        {
 | 
			
		||||
            // Rate limiting occurs when we've already played a sound within the last tick.
 | 
			
		||||
            if( !isNote ) return false;
 | 
			
		||||
            // Or we've played more notes than allowable within the current tick.
 | 
			
		||||
            if( clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick ) return false;
 | 
			
		||||
        }
 | 
			
		||||
        lastPosition = position;
 | 
			
		||||
        lastPositionTime = clock;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        Level world = getLevel();
 | 
			
		||||
        Vec3 pos = getPosition();
 | 
			
		||||
    @Override
 | 
			
		||||
    public void attach( @Nonnull IComputerAccess computer )
 | 
			
		||||
    {
 | 
			
		||||
        computers.add( computer );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        float actualVolume = Mth.clamp( volume, 0.0f, 3.0f );
 | 
			
		||||
        float range = actualVolume * 16;
 | 
			
		||||
    @Override
 | 
			
		||||
    public void detach( @Nonnull IComputerAccess computer )
 | 
			
		||||
    {
 | 
			
		||||
        computers.remove( computer );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        context.issueMainThreadTask( () -> {
 | 
			
		||||
            MinecraftServer server = world.getServer();
 | 
			
		||||
            if( server == null ) return null;
 | 
			
		||||
 | 
			
		||||
            if( isNote )
 | 
			
		||||
            {
 | 
			
		||||
                server.getPlayerList().broadcast(
 | 
			
		||||
                    null, pos.x, pos.y, pos.z, range, world.dimension(),
 | 
			
		||||
                    new ClientboundCustomSoundPacket( name, SoundSource.RECORDS, pos, actualVolume, pitch )
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                NetworkHandler.sendToAllAround(
 | 
			
		||||
                    new SpeakerPlayClientMessage( getSource(), pos, name, actualVolume, pitch ),
 | 
			
		||||
                    world, pos, range
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
        } );
 | 
			
		||||
 | 
			
		||||
        lastPlayTime = clock;
 | 
			
		||||
        return true;
 | 
			
		||||
    private record PendingSound(ResourceLocation location, float volume, float pitch)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ import net.minecraftforge.common.util.LazyOptional;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL;
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +28,6 @@ public class TileSpeaker extends TileGeneric
 | 
			
		||||
{
 | 
			
		||||
    private final SpeakerPeripheral peripheral;
 | 
			
		||||
    private LazyOptional<IPeripheral> peripheralCap;
 | 
			
		||||
    private final UUID source = UUID.randomUUID();
 | 
			
		||||
 | 
			
		||||
    public TileSpeaker( BlockEntityType<TileSpeaker> type, BlockPos pos, BlockState state )
 | 
			
		||||
    {
 | 
			
		||||
@@ -48,7 +46,7 @@ public class TileSpeaker extends TileGeneric
 | 
			
		||||
        super.setRemoved();
 | 
			
		||||
        if( level != null && !level.isClientSide )
 | 
			
		||||
        {
 | 
			
		||||
            NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
 | 
			
		||||
            NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( peripheral.getSource() ) );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -87,6 +85,7 @@ public class TileSpeaker extends TileGeneric
 | 
			
		||||
            return speaker.getLevel();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Nonnull
 | 
			
		||||
        @Override
 | 
			
		||||
        public Vec3 getPosition()
 | 
			
		||||
        {
 | 
			
		||||
@@ -94,12 +93,6 @@ public class TileSpeaker extends TileGeneric
 | 
			
		||||
            return new Vec3( pos.getX(), pos.getY(), pos.getZ() );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        protected UUID getSource()
 | 
			
		||||
        {
 | 
			
		||||
            return speaker.source;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean equals( @Nullable IPeripheral other )
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraftforge.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.
 | 
			
		||||
@@ -21,14 +20,6 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral
 | 
			
		||||
{
 | 
			
		||||
    public static final String ADJECTIVE = "upgrade.computercraft.speaker.adjective";
 | 
			
		||||
 | 
			
		||||
    private final UUID source = UUID.randomUUID();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected final UUID getSource()
 | 
			
		||||
    {
 | 
			
		||||
        return source;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void detach( @Nonnull IComputerAccess computer )
 | 
			
		||||
    {
 | 
			
		||||
@@ -36,6 +27,6 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral
 | 
			
		||||
        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;
 | 
			
		||||
@@ -34,6 +33,7 @@ import net.minecraft.world.InteractionHand;
 | 
			
		||||
import net.minecraft.world.InteractionResult;
 | 
			
		||||
import net.minecraft.world.InteractionResultHolder;
 | 
			
		||||
import net.minecraft.world.entity.Entity;
 | 
			
		||||
import net.minecraft.world.entity.item.ItemEntity;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.CreativeModeTab;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
@@ -80,53 +80,65 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        PocketUpgrades.getVanillaUpgrades().map( x -> create( -1, null, -1, x ) ).forEach( stacks::add );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean tick( @Nonnull ItemStack stack, @Nonnull Level world, @Nonnull Entity entity, @Nonnull PocketServerComputer computer )
 | 
			
		||||
    {
 | 
			
		||||
        IPocketUpgrade upgrade = getUpgrade( stack );
 | 
			
		||||
 | 
			
		||||
        computer.setLevel( world );
 | 
			
		||||
        computer.updateValues( entity, stack, upgrade );
 | 
			
		||||
 | 
			
		||||
        boolean changed = false;
 | 
			
		||||
 | 
			
		||||
        // Sync ID
 | 
			
		||||
        int id = computer.getID();
 | 
			
		||||
        if( id != getComputerID( stack ) )
 | 
			
		||||
        {
 | 
			
		||||
            changed = true;
 | 
			
		||||
            setComputerID( stack, id );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sync label
 | 
			
		||||
        String label = computer.getLabel();
 | 
			
		||||
        if( !Objects.equal( label, getLabel( stack ) ) )
 | 
			
		||||
        {
 | 
			
		||||
            changed = true;
 | 
			
		||||
            setLabel( stack, label );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update pocket upgrade
 | 
			
		||||
        if( upgrade != null ) upgrade.update( computer, computer.getPeripheral( ComputerSide.BACK ) );
 | 
			
		||||
 | 
			
		||||
        return changed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void inventoryTick( @Nonnull ItemStack stack, Level world, @Nonnull Entity entity, int slotNum, boolean selected )
 | 
			
		||||
    {
 | 
			
		||||
        if( !world.isClientSide )
 | 
			
		||||
        {
 | 
			
		||||
            // Server side
 | 
			
		||||
            Container inventory = entity instanceof Player ? ((Player) entity).getInventory() : null;
 | 
			
		||||
            PocketServerComputer computer = createServerComputer( world, inventory, entity, stack );
 | 
			
		||||
            if( computer != null )
 | 
			
		||||
            {
 | 
			
		||||
                IPocketUpgrade upgrade = getUpgrade( stack );
 | 
			
		||||
            Container inventory = entity instanceof Player player ? player.getInventory() : null;
 | 
			
		||||
            PocketServerComputer computer = createServerComputer( world, entity, inventory, stack );
 | 
			
		||||
            computer.keepAlive();
 | 
			
		||||
 | 
			
		||||
                // Ping computer
 | 
			
		||||
                computer.keepAlive();
 | 
			
		||||
                computer.setLevel( world );
 | 
			
		||||
                computer.updateValues( entity, stack, upgrade );
 | 
			
		||||
 | 
			
		||||
                // Sync ID
 | 
			
		||||
                int id = computer.getID();
 | 
			
		||||
                if( id != getComputerID( stack ) )
 | 
			
		||||
                {
 | 
			
		||||
                    setComputerID( stack, id );
 | 
			
		||||
                    if( inventory != null ) inventory.setChanged();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Sync label
 | 
			
		||||
                String label = computer.getLabel();
 | 
			
		||||
                if( !Objects.equal( label, getLabel( stack ) ) )
 | 
			
		||||
                {
 | 
			
		||||
                    setLabel( stack, label );
 | 
			
		||||
                    if( inventory != null ) inventory.setChanged();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Update pocket upgrade
 | 
			
		||||
                if( upgrade != null )
 | 
			
		||||
                {
 | 
			
		||||
                    upgrade.update( computer, computer.getPeripheral( ComputerSide.BACK ) );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            boolean changed = tick( stack, world, entity, computer );
 | 
			
		||||
            if( changed && inventory != null ) inventory.setChanged();
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // Client side
 | 
			
		||||
            createClientComputer( stack );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onEntityItemUpdate( ItemStack stack, ItemEntity entity )
 | 
			
		||||
    {
 | 
			
		||||
        if( entity.level.isClientSide ) return false;
 | 
			
		||||
 | 
			
		||||
        PocketServerComputer computer = getServerComputer( stack );
 | 
			
		||||
        if( computer != null && tick( stack, entity.level, entity, computer ) ) entity.setItem( stack.copy() );
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public InteractionResultHolder<ItemStack> use( Level world, Player player, @Nonnull InteractionHand hand )
 | 
			
		||||
@@ -134,22 +146,18 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        ItemStack stack = player.getItemInHand( hand );
 | 
			
		||||
        if( !world.isClientSide )
 | 
			
		||||
        {
 | 
			
		||||
            PocketServerComputer computer = createServerComputer( world, player.getInventory(), player, stack );
 | 
			
		||||
            PocketServerComputer computer = createServerComputer( world, player, player.getInventory(), stack );
 | 
			
		||||
            computer.turnOn();
 | 
			
		||||
 | 
			
		||||
            boolean stop = false;
 | 
			
		||||
            if( computer != null )
 | 
			
		||||
            IPocketUpgrade upgrade = getUpgrade( stack );
 | 
			
		||||
            if( upgrade != null )
 | 
			
		||||
            {
 | 
			
		||||
                computer.turnOn();
 | 
			
		||||
 | 
			
		||||
                IPocketUpgrade upgrade = getUpgrade( stack );
 | 
			
		||||
                if( upgrade != null )
 | 
			
		||||
                {
 | 
			
		||||
                    computer.updateValues( player, stack, upgrade );
 | 
			
		||||
                    stop = upgrade.onRightClick( world, computer, computer.getPeripheral( ComputerSide.BACK ) );
 | 
			
		||||
                }
 | 
			
		||||
                computer.updateValues( player, stack, upgrade );
 | 
			
		||||
                stop = upgrade.onRightClick( world, computer, computer.getPeripheral( ComputerSide.BACK ) );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if( !stop && computer != null )
 | 
			
		||||
            if( !stop )
 | 
			
		||||
            {
 | 
			
		||||
                boolean isTypingOnly = hand == InteractionHand.OFF_HAND;
 | 
			
		||||
                new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
 | 
			
		||||
@@ -207,17 +215,17 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        return super.getCreatorModId( stack );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PocketServerComputer createServerComputer( final Level world, Container inventory, Entity entity, @Nonnull ItemStack stack )
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    public PocketServerComputer createServerComputer( Level world, Entity entity, @Nullable Container inventory, @Nonnull ItemStack stack )
 | 
			
		||||
    {
 | 
			
		||||
        if( world.isClientSide ) return null;
 | 
			
		||||
        if( world.isClientSide ) throw new IllegalStateException( "Cannot call createServerComputer on the client" );
 | 
			
		||||
 | 
			
		||||
        PocketServerComputer computer;
 | 
			
		||||
        int instanceID = getInstanceID( stack );
 | 
			
		||||
        int sessionID = getSessionID( stack );
 | 
			
		||||
        int correctSessionID = ComputerCraft.serverComputerRegistry.getSessionID();
 | 
			
		||||
 | 
			
		||||
        if( instanceID >= 0 && sessionID == correctSessionID &&
 | 
			
		||||
            ComputerCraft.serverComputerRegistry.contains( instanceID ) )
 | 
			
		||||
        if( instanceID >= 0 && sessionID == correctSessionID && ComputerCraft.serverComputerRegistry.contains( instanceID ) )
 | 
			
		||||
        {
 | 
			
		||||
            computer = (PocketServerComputer) ComputerCraft.serverComputerRegistry.get( instanceID );
 | 
			
		||||
        }
 | 
			
		||||
@@ -235,13 +243,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
                computerID = ComputerCraftAPI.createUniqueNumberedSaveDir( world, "computer" );
 | 
			
		||||
                setComputerID( stack, computerID );
 | 
			
		||||
            }
 | 
			
		||||
            computer = new PocketServerComputer(
 | 
			
		||||
                world,
 | 
			
		||||
                computerID,
 | 
			
		||||
                getLabel( stack ),
 | 
			
		||||
                instanceID,
 | 
			
		||||
                getFamily()
 | 
			
		||||
            );
 | 
			
		||||
            computer = new PocketServerComputer( world, computerID, getLabel( stack ), instanceID, getFamily() );
 | 
			
		||||
            computer.updateValues( entity, stack, getUpgrade( stack ) );
 | 
			
		||||
            computer.addAPI( new PocketAPI( computer ) );
 | 
			
		||||
            ComputerCraft.serverComputerRegistry.add( instanceID, computer );
 | 
			
		||||
@@ -251,15 +253,17 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        return computer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ServerComputer getServerComputer( @Nonnull ItemStack stack )
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static PocketServerComputer getServerComputer( @Nonnull ItemStack stack )
 | 
			
		||||
    {
 | 
			
		||||
        int session = getSessionID( stack );
 | 
			
		||||
        if( session != ComputerCraft.serverComputerRegistry.getSessionID() ) return null;
 | 
			
		||||
 | 
			
		||||
        int instanceID = getInstanceID( stack );
 | 
			
		||||
        return instanceID >= 0 ? ComputerCraft.serverComputerRegistry.get( instanceID ) : null;
 | 
			
		||||
        return instanceID >= 0 ? (PocketServerComputer) ComputerCraft.serverComputerRegistry.get( instanceID ) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static ClientComputer createClientComputer( @Nonnull ItemStack stack )
 | 
			
		||||
    {
 | 
			
		||||
        int instanceID = getInstanceID( stack );
 | 
			
		||||
@@ -274,6 +278,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private static ClientComputer getClientComputer( @Nonnull ItemStack stack )
 | 
			
		||||
    {
 | 
			
		||||
        int instanceID = getInstanceID( stack );
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,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.world.level.Level;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
 | 
			
		||||
public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
 | 
			
		||||
{
 | 
			
		||||
    private Level world = null;
 | 
			
		||||
@@ -27,6 +29,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
 | 
			
		||||
        return world;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Vec3 getPosition()
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,8 +22,50 @@ import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The turtle API allows you to control your turtle.
 | 
			
		||||
 * Turtles are a robotic device, which can break and place blocks, attack mobs, and move about the world. They have
 | 
			
		||||
 * an internal inventory of 16 slots, allowing them to store blocks they have broken or would like to place.
 | 
			
		||||
 *
 | 
			
		||||
 * ## Movement
 | 
			
		||||
 * Turtles are capable of moving throug the world. As turtles are blocks themselves, they are confined to Minecraft's
 | 
			
		||||
 * grid, moving a single block at a time.
 | 
			
		||||
 *
 | 
			
		||||
 * {@literal @}{turtle.forward} and @{turtle.back} move the turtle in the direction it is facing, while @{turtle.up} and
 | 
			
		||||
 * {@literal @}{turtle.down} move it up and down (as one might expect!). In order to move left or right, you first need
 | 
			
		||||
 * to turn the turtle using @{turtle.turnLeft}/@{turtle.turnRight} and then move forward or backwards.
 | 
			
		||||
 *
 | 
			
		||||
 * :::info
 | 
			
		||||
 * The name "turtle" comes from [Turtle graphics], which originated from the Logo programming language. Here you'd move
 | 
			
		||||
 * a turtle with various commands like "move 10" and "turn left", much like ComputerCraft's turtles!
 | 
			
		||||
 * :::
 | 
			
		||||
 *
 | 
			
		||||
 * Moving a turtle (though not turning it) consumes *fuel*. If a turtle does not have any @{turtle.refuel|fuel}, it
 | 
			
		||||
 * won't move, and the movement functions will return @{false}. If your turtle isn't going anywhere, the first thing to
 | 
			
		||||
 * check is if you've fuelled your turtle.
 | 
			
		||||
 *
 | 
			
		||||
 * :::tip Handling errors
 | 
			
		||||
 * Many turtle functions can fail in various ways. For instance, a turtle cannot move forward if there's already a block
 | 
			
		||||
 * there. Instead of erroring, functions which can fail either return @{true} if they succeed, or @{false} and some
 | 
			
		||||
 * error message if they fail.
 | 
			
		||||
 *
 | 
			
		||||
 * Unexpected failures can often lead to strange behaviour. It's often a good idea to check the return values of these
 | 
			
		||||
 * functions, or wrap them in @{assert} (for instance, use `assert(turtle.forward())` rather than `turtle.forward()`),
 | 
			
		||||
 * so the program doesn't misbehave.
 | 
			
		||||
 * :::
 | 
			
		||||
 *
 | 
			
		||||
 * ## Turtle upgrades
 | 
			
		||||
 * While a normal turtle can move about the world and place blocks, its functionality is limited. Thankfully, turtles
 | 
			
		||||
 * can be upgraded with *tools* and @{peripheral|peripherals}. Turtles have two upgrade slots, one on the left and right
 | 
			
		||||
 * sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the @{turtle.equipLeft}/@{turtle.equipRight}
 | 
			
		||||
 * functions.
 | 
			
		||||
 *
 | 
			
		||||
 * Turtle tools allow you to break blocks (@{turtle.dig}) and attack entities (@{turtle.attack}). Some tools are more
 | 
			
		||||
 * suitable to a task than others. For instance, a diamond pickaxe can break every block, while a sword does more
 | 
			
		||||
 * damage. Other tools have more niche use-cases, for instance hoes can til dirt.
 | 
			
		||||
 *
 | 
			
		||||
 * Peripherals (such as the @{modem|wireless modem} or @{speaker}) can also be equipped as upgrades. These are then
 | 
			
		||||
 * accessible by accessing the `"left"` or `"right"` peripheral.
 | 
			
		||||
 *
 | 
			
		||||
 * [Turtle Graphics]: https://en.wikipedia.org/wiki/Turtle_graphics "Turtle graphics"
 | 
			
		||||
 * @cc.module turtle
 | 
			
		||||
 * @cc.since 1.3
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,6 @@ import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.tags.FluidTags;
 | 
			
		||||
import net.minecraft.world.Container;
 | 
			
		||||
import net.minecraft.world.entity.Entity;
 | 
			
		||||
import net.minecraft.world.entity.EntitySelector;
 | 
			
		||||
import net.minecraft.world.entity.MoverType;
 | 
			
		||||
import net.minecraft.world.item.DyeColor;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
@@ -39,6 +38,7 @@ import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.level.material.FluidState;
 | 
			
		||||
import net.minecraft.world.level.material.PushReaction;
 | 
			
		||||
import net.minecraft.world.phys.AABB;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
import net.minecraftforge.items.IItemHandlerModifiable;
 | 
			
		||||
@@ -48,6 +48,7 @@ import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR;
 | 
			
		||||
import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED;
 | 
			
		||||
@@ -65,6 +66,8 @@ public class TurtleBrain implements ITurtleAccess
 | 
			
		||||
 | 
			
		||||
    private static final int ANIM_DURATION = 8;
 | 
			
		||||
 | 
			
		||||
    public static final Predicate<Entity> PUSHABLE_ENTITY = entity -> !entity.isSpectator() && entity.getPistonPushReaction() != PushReaction.IGNORE;
 | 
			
		||||
 | 
			
		||||
    private TileTurtle owner;
 | 
			
		||||
    private ComputerProxy proxy;
 | 
			
		||||
    private GameProfile owningPlayer;
 | 
			
		||||
@@ -886,7 +889,7 @@ public class TurtleBrain implements ITurtleAccess
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    AABB aabb = new AABB( minX, minY, minZ, maxX, maxY, maxZ );
 | 
			
		||||
                    List<Entity> list = world.getEntitiesOfClass( Entity.class, aabb, EntitySelector.NO_SPECTATORS );
 | 
			
		||||
                    List<Entity> list = world.getEntitiesOfClass( Entity.class, aabb, PUSHABLE_ENTITY );
 | 
			
		||||
                    if( !list.isEmpty() )
 | 
			
		||||
                    {
 | 
			
		||||
                        double pushStep = 1.0f / ANIM_DURATION;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade
 | 
			
		||||
            return turtle.getLevel();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Nonnull
 | 
			
		||||
        @Override
 | 
			
		||||
        public Vec3 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.Util;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraftforge.api.distmarker.Dist;
 | 
			
		||||
import net.minecraftforge.event.TickEvent;
 | 
			
		||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
 | 
			
		||||
import net.minecraftforge.fml.common.Mod;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A monotonically increasing clock which accounts for the game being paused.
 | 
			
		||||
 */
 | 
			
		||||
@Mod.EventBusSubscriber( Dist.CLIENT )
 | 
			
		||||
public final class PauseAwareTimer
 | 
			
		||||
{
 | 
			
		||||
    private static boolean paused;
 | 
			
		||||
    private static long pauseTime;
 | 
			
		||||
    private static long pauseOffset;
 | 
			
		||||
 | 
			
		||||
    private PauseAwareTimer()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static long getTime()
 | 
			
		||||
    {
 | 
			
		||||
        return (paused ? pauseTime : Util.getNanos()) - pauseOffset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SubscribeEvent
 | 
			
		||||
    public static void tick( TickEvent.RenderTickEvent event )
 | 
			
		||||
    {
 | 
			
		||||
        if( event.phase != TickEvent.Phase.START ) return;
 | 
			
		||||
 | 
			
		||||
        boolean isPaused = Minecraft.getInstance().isPaused();
 | 
			
		||||
        if( isPaused == paused ) return;
 | 
			
		||||
 | 
			
		||||
        if( isPaused )
 | 
			
		||||
        {
 | 
			
		||||
            pauseTime = Util.getNanos();
 | 
			
		||||
            paused = true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            pauseOffset += Util.getNanos() - pauseTime;
 | 
			
		||||
            paused = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,3 +7,7 @@ public net.minecraft.client.gui.components.ChatComponent m_93787_(Lnet/minecraft
 | 
			
		||||
public net.minecraft.client.gui.components.ChatComponent m_93803_(I)V # removeById
 | 
			
		||||
# NoTermComputerScreen
 | 
			
		||||
public net.minecraft.client.Minecraft f_91080_ # screen
 | 
			
		||||
 | 
			
		||||
# SpeakerInstance/SpeakerManager
 | 
			
		||||
public com.mojang.blaze3d.audio.Channel m_83652_(I)V # pumpBuffers
 | 
			
		||||
public net.minecraft.client.sounds.SoundEngine f_120223_ # executor
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
modLoader="javafml"
 | 
			
		||||
loaderVersion="[38,39)"
 | 
			
		||||
loaderVersion="[39,40)"
 | 
			
		||||
 | 
			
		||||
issueTrackerURL="https://github.com/cc-tweaked/CC-Tweaked/issues"
 | 
			
		||||
logoFile="pack.png"
 | 
			
		||||
@@ -21,6 +21,6 @@ CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles a
 | 
			
		||||
[[dependencies.computercraft]]
 | 
			
		||||
    modId="forge"
 | 
			
		||||
    mandatory=true
 | 
			
		||||
    versionRange="[38.0.0,39)"
 | 
			
		||||
    versionRange="[39.0.0,40)"
 | 
			
		||||
    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": [
 | 
			
		||||
        "BlockRenderDispatcherMixin"
 | 
			
		||||
    ],
 | 
			
		||||
    "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.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,11 @@ Once you have the name of a peripheral, you can call functions on it using the
 | 
			
		||||
@{peripheral.call} function. This takes the name of our peripheral, the name of
 | 
			
		||||
the function we want to call, and then its arguments.
 | 
			
		||||
 | 
			
		||||
> Some bits of the peripheral API call peripheral functions *methods* instead
 | 
			
		||||
> (for example, the @{peripheral.getMethods} function). Don't worry, they're the
 | 
			
		||||
> same thing!
 | 
			
		||||
:::info
 | 
			
		||||
Some bits of the peripheral API call peripheral functions *methods* instead
 | 
			
		||||
(for example, the @{peripheral.getMethods} function). Don't worry, they're the
 | 
			
		||||
same thing!
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
Let's say we have a monitor above our computer (and so "top") and want to
 | 
			
		||||
@{monitor.write|write some text to it}. We'd write the following:
 | 
			
		||||
@@ -64,7 +66,7 @@ type.
 | 
			
		||||
What is a peripheral type though? This is a string which describes what a
 | 
			
		||||
peripheral is, and so what functions are available on it. For instance, speakers
 | 
			
		||||
are just called `"speaker"`, and monitors `"monitor"`. Some peripherals might
 | 
			
		||||
have more than one type; a Minecraft chest is both a `"minecraft:chest"` and
 | 
			
		||||
have more than one type - a Minecraft chest is both a `"minecraft:chest"` and
 | 
			
		||||
`"inventory"`.
 | 
			
		||||
 | 
			
		||||
You can get all the types a peripheral has with @{peripheral.getType}, and check
 | 
			
		||||
 
 | 
			
		||||
@@ -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,26 @@
 | 
			
		||||
# New features in CC: Tweaked 1.100.0
 | 
			
		||||
 | 
			
		||||
* Speakers can now play arbitrary PCM audio.
 | 
			
		||||
* Add support for encoding and decoding DFPWM streams, with the `cc.audio.dfpwm` module.
 | 
			
		||||
* Wired modems now only render breaking progress for the part which is being broken.
 | 
			
		||||
* Various documentation improvements.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix the "repeat" program not repeating broadcast rednet messages.
 | 
			
		||||
* Fix the drag-and-drop upload functionality writing empty files.
 | 
			
		||||
* Prevent turtles from pushing non-pushable entities.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.99.1
 | 
			
		||||
 | 
			
		||||
* Add package.searchpath to the cc.require API. (MCJack123)
 | 
			
		||||
* Provide a more efficient way for the Java API to consume Lua tables in certain restricted cases.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix keys being "sticky" when opening the off-hand pocket computer GUI.
 | 
			
		||||
* Correctly handle broken coroutine managers resuming Java code with a `nil` event.
 | 
			
		||||
* Prevent computer buttons stealing focus from the terminal.
 | 
			
		||||
* Fix a class cast exception when a monitor is malformed in ways I do not quite understand.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.99.0
 | 
			
		||||
 | 
			
		||||
* Pocket computers in their offhand will open without showing a terminal. You can look around and interact with the world, but your keyboard will be forwarded to the computer. (Wojbie, MagGen-hub).
 | 
			
		||||
 
 | 
			
		||||
@@ -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,32 +1,13 @@
 | 
			
		||||
New features in CC: Tweaked 1.99.0
 | 
			
		||||
New features in CC: Tweaked 1.100.0
 | 
			
		||||
 | 
			
		||||
* Pocket computers in their offhand will open without showing a terminal. You can look around and interact with the world, but your keyboard will be forwarded to the computer. (Wojbie, MagGen-hub).
 | 
			
		||||
* Peripherals can now have multiple types. `peripheral.getType` now returns multiple values, and `peripheral.hasType` checks if a peripheral has a specific type.
 | 
			
		||||
* Add several missing keys to the `keys` table. (ralphgod3)
 | 
			
		||||
* Add feature introduction/changed version information to the documentation. (MCJack123)
 | 
			
		||||
* Increase the file upload limit to 512KiB.
 | 
			
		||||
* Rednet can now handle computer IDs larger than 65535. (Ale32bit)
 | 
			
		||||
* Optimise deduplication of rednet messages (MCJack123)
 | 
			
		||||
* Make `term.blit` colours case insensitive. (Ocawesome101)
 | 
			
		||||
* Add a new `about` program for easier version identification. (MCJack123)
 | 
			
		||||
* Optimise peripheral calls in `rednet.run`. (xAnavrins)
 | 
			
		||||
* Add dimension parameter to `commands.getBlockInfo`.
 | 
			
		||||
* Add `cc.pretty.pretty_print` helper function (Lupus590).
 | 
			
		||||
* Add back JEI integration.
 | 
			
		||||
* Turtle and pocket computer upgrades can now be added and modified with data packs.
 | 
			
		||||
* Various translation updates (MORIMORI3017, Ale2Bit, mindy15963)
 | 
			
		||||
* Speakers can now play arbitrary PCM audio.
 | 
			
		||||
* Add support for encoding and decoding DFPWM streams, with the `cc.audio.dfpwm` module.
 | 
			
		||||
* Wired modems now only render breaking progress for the part which is being broken.
 | 
			
		||||
* Various documentation improvements.
 | 
			
		||||
 | 
			
		||||
And several bug fixes:
 | 
			
		||||
* Fix various computer commands failing when OP level was 4.
 | 
			
		||||
* Various documentation fixes. (xXTurnerLP, MCJack123)
 | 
			
		||||
* Fix `textutils.serialize` not serialising infinity and nan values. (Wojbie)
 | 
			
		||||
* Wired modems now correctly clean up mounts when a peripheral is detached.
 | 
			
		||||
* Fix incorrect turtle and pocket computer upgrade recipes in the recipe book.
 | 
			
		||||
* Fix speakers not playing sounds added via resource packs which are not registered in-game.
 | 
			
		||||
* Fix speaker upgrades sending packets after the server has stopped.
 | 
			
		||||
* Monitor sizing has been rewritten, hopefully making it more stable.
 | 
			
		||||
* Peripherals are now invalidated when the computer ticks, rather than when the peripheral changes.
 | 
			
		||||
* Fix printouts and pocket computers rendering at fullbright when in item frames.
 | 
			
		||||
* All mod blocks now have an effective tool (pickaxe).
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix the "repeat" program not repeating broadcast rednet messages.
 | 
			
		||||
* Fix the drag-and-drop upload functionality writing empty files.
 | 
			
		||||
* Prevent turtles from pushing non-pushable entities.
 | 
			
		||||
 | 
			
		||||
Type "help changelog" to see the full version history.
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -31,22 +33,37 @@ local function preload(package)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function from_file(package, env, dir)
 | 
			
		||||
local function from_file(package, env)
 | 
			
		||||
    return function(name)
 | 
			
		||||
        local fname = string.gsub(name, "%.", "/")
 | 
			
		||||
        local sPath, sError = package.searchpath(name, package.path)
 | 
			
		||||
        if not sPath then
 | 
			
		||||
            return nil, sError
 | 
			
		||||
        end
 | 
			
		||||
        local fnFile, sError = loadfile(sPath, nil, env)
 | 
			
		||||
        if fnFile then
 | 
			
		||||
            return fnFile, sPath
 | 
			
		||||
        else
 | 
			
		||||
            return nil, sError
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function make_searchpath(dir)
 | 
			
		||||
    return function(name, path, sep, rep)
 | 
			
		||||
        expect(1, name, "string")
 | 
			
		||||
        expect(2, path, "string")
 | 
			
		||||
        sep = expect(3, sep, "string", "nil") or "."
 | 
			
		||||
        rep = expect(4, rep, "string", "nil") or "/"
 | 
			
		||||
 | 
			
		||||
        local fname = string.gsub(name, sep:gsub("%.", "%%%."), rep)
 | 
			
		||||
        local sError = ""
 | 
			
		||||
        for pattern in string.gmatch(package.path, "[^;]+") do
 | 
			
		||||
        for pattern in string.gmatch(path, "[^;]+") do
 | 
			
		||||
            local sPath = string.gsub(pattern, "%?", fname)
 | 
			
		||||
            if sPath:sub(1, 1) ~= "/" then
 | 
			
		||||
                sPath = fs.combine(dir, sPath)
 | 
			
		||||
            end
 | 
			
		||||
            if fs.exists(sPath) and not fs.isDir(sPath) then
 | 
			
		||||
                local fnFile, sError = loadfile(sPath, nil, env)
 | 
			
		||||
                if fnFile then
 | 
			
		||||
                    return fnFile, sPath
 | 
			
		||||
                else
 | 
			
		||||
                    return nil, sError
 | 
			
		||||
                end
 | 
			
		||||
                return sPath
 | 
			
		||||
            else
 | 
			
		||||
                if #sError > 0 then
 | 
			
		||||
                    sError = sError .. "\n  "
 | 
			
		||||
@@ -118,7 +135,8 @@ local function make_package(env, dir)
 | 
			
		||||
    end
 | 
			
		||||
    package.config = "/\n;\n?\n!\n-"
 | 
			
		||||
    package.preload = {}
 | 
			
		||||
    package.loaders = { preload(package), from_file(package, env, dir) }
 | 
			
		||||
    package.loaders = { preload(package), from_file(package, env) }
 | 
			
		||||
    package.searchpath = make_searchpath(dir)
 | 
			
		||||
 | 
			
		||||
    return make_require(package), package
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -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 " } },
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,7 @@ public class ComputerTestDelegate
 | 
			
		||||
 | 
			
		||||
        if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." );
 | 
			
		||||
 | 
			
		||||
        Terminal term = new Terminal( 80, 30 );
 | 
			
		||||
        Terminal term = new Terminal( 80, 100 );
 | 
			
		||||
        IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 );
 | 
			
		||||
 | 
			
		||||
        // Remove any existing files
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,13 @@ public class GeneratorTest
 | 
			
		||||
            contramap( notNullValue(), "callback", MethodResult::getCallback ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUnsafe()
 | 
			
		||||
    {
 | 
			
		||||
        List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( Unsafe.class );
 | 
			
		||||
        assertThat( methods, contains( named( "withUnsafe" ) ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Basic
 | 
			
		||||
    {
 | 
			
		||||
        @LuaFunction
 | 
			
		||||
@@ -222,6 +229,21 @@ public class GeneratorTest
 | 
			
		||||
        {}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static class Unsafe
 | 
			
		||||
    {
 | 
			
		||||
        @LuaFunction( unsafe = true )
 | 
			
		||||
        public final void withUnsafe( LuaTable<?, ?> table )
 | 
			
		||||
        {}
 | 
			
		||||
 | 
			
		||||
        @LuaFunction
 | 
			
		||||
        public final void withoutUnsafe( LuaTable<?, ?> table )
 | 
			
		||||
        {}
 | 
			
		||||
 | 
			
		||||
        @LuaFunction( unsafe = true, mainThread = true )
 | 
			
		||||
        public final void invalid( LuaTable<?, ?> table )
 | 
			
		||||
        {}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static <T> T find( Collection<NamedMethod<T>> methods, String name )
 | 
			
		||||
    {
 | 
			
		||||
        return methods.stream()
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user