1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-15 22:17:39 +00:00

Compare commits

..

7 Commits

Author SHA1 Message Date
SquidDev
8819f2559d Add some tests for the new executor system
And fix a couple of bugs picked up by said tests
2018-09-18 18:21:00 +01:00
SquidDev
4b741739e8 Update the executor to work with the new calling system 2018-09-13 10:32:45 +01:00
SquidDev
f23acef2dd Merge remote-tracking branch 'SquidDev-CC-ComputerCraft/feature/method-future' into feature/onethread-cobalt 2018-09-09 22:13:45 +01:00
SquidDev
ac8444b364 Migrate ComputerCraft's peripherals/APIs to use the non-blocking API
This is definitely a little ugly in places, mostly due to how we've
handled backwards compatibility.
2018-09-09 17:06:34 +01:00
SquidDev
3b4c1eac1c Proposal for replacing ILuaContext with a non-blocking alternative
This is an initial prototype for ways we could better integrate Lua's
asynchronous functionality (coroutines) with peripherals and other
Lua APIs.

The existing system assumes that coroutines each have a backing thread,
and so yields will suspect the current thread. This takes inspiration
from various "promise" libraries - asynchronous operations (such as
executing on the main thread or waiting for an event) return a promise -
the result of which can be consumed with `.then`.

While I am not aware of plans to change the Lua implementation, this
does give us greater flexibility in the future, leading the way for
Rembulan, single-threaded Cobalt, and even possibly OC support.
2018-09-09 17:04:07 +01:00
SquidDev
7cc77cb1ed Add a basic implementation of the single-thread Lua 2018-09-01 18:40:10 +01:00
SquidDev
0f70d68d0d A minor refactor to computer method calls
We move several rather complex classes into a separate file.
2018-09-01 17:34:26 +01:00
3316 changed files with 113859 additions and 127433 deletions

View File

@@ -5,25 +5,13 @@ indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
# Sadly too many files have whitespace errors, so we leave this as is for
# now and just make sure we don't introduce any more.
# trim_trailing_whitespace = true
insert_final_newline = true
ij_continuation_indent_size = 4
ij_any_do_while_brace_force = if_multiline
ij_any_if_brace_force = if_multiline
ij_any_for_brace_force = if_multiline
ij_any_spaces_within_array_initializer_braces = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_method_parameters_wrap = off
ij_kotlin_call_parameters_wrap = off
[*.md]
trim_trailing_whitespace = false
[*.sexp]
indent_size = 2
[*.yml]
indent_size = 2
[*.properties]
insert_final_newline = false

View File

@@ -1,2 +0,0 @@
# Reformat everything
f478c4ffc4fb9fc2200ec9b0bc751d047057ce81

16
.gitattributes vendored
View File

@@ -1,16 +0,0 @@
# Ignore changes in generated files
projects/*/src/generated/** linguist-generated
projects/common/src/testMod/resources/data/cctest/structures/* linguist-generated
* text=auto
*.gradle eol=lf diff=java
*.java eol=lf diff=java
*.kt eol=lf diff=java
*.lua eol=lf
*.md eol=lf diff=markdown
*.txt eol=lf
*.png binary
*.jar binary
*.dfpwm binary

16
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Bug report
about: Report some misbehaviour in the mod
---
<!--
## Before reporting
- Search for the bug both here and [on the ComputerCraft issues page](https://github.com/dan200/ComputerCraft/issues?utf8=%E2%9C%93&q=is%3Aissue+)
- If possible, try to reproduce on vanilla ComputerCraft. If it still occurs, [report on the ComputerCraft repo](https://github.com/dan200/ComputerCraft/issues/new) instead.
-->
## Useful information to include:
- Minecraft version
- CC: Tweaked version
- Detailed reproduction steps!** Sometimes I can spot a bug pretty easily, but often it's much more obscure. Anything you can give which will help reproduce it means it'll get fixed quicker.

View File

@@ -1,34 +0,0 @@
name: Bug report
description: Report some misbehaviour in the mod
labels: [ bug ]
body:
- type: dropdown
id: mc-version
attributes:
label: Minecraft Version
description: What version of Minecraft are you using?
options:
- 1.16.x
- 1.18.x
- 1.19.x
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: "What version of CC: Tweaked are you using?"
placeholder: "e.g. 1.96.0"
validations:
required: true
- type: textarea
id: details
attributes:
label: Details
description: |
Description of the bug. Please include the following:
- Logs: These will be located in the `logs/` directory of your Minecraft
instance. Please upload them as a gist or directly into this editor.
- Detailed reproduction steps: sometimes I can spot a bug pretty easily,
but often it's much more obscure. The more information I have to help
reproduce it, the quicker it'll get fixed.

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/cc-tweaked/CC-Tweaked/discussions
about: Ask questions on GitHub Discussions.

View File

@@ -1,14 +1,15 @@
---
name: Feature request
about: Suggest an idea or improvement
labels: enhancement
---
<!--
## Before reporting
- Search for the suggestion here. It's possible someone's suggested it before!
- Search for the suggestion both here and [on the ComputerCraft issues page](https://github.com/dan200/ComputerCraft/issues?utf8=%E2%9C%93&q=is%3Aissue+). It's possible someone's suggested it before!
- Unless something is specific to CC:Tweaked, try to [suggest them on the ComputerCraft repo](https://github.com/dan200/ComputerCraft/issues/new). There's a lot more people watching it, so it allows the wider community to contribute.
-->
## Useful information to include:
- Explanation of how the feature/change should work.
- Some rationale/use case for a feature. My general approach to designing new features is to ask yourself "what issue are we trying to solve" and _then_ "is this the best way to solve this issue?".
- Explanation of how the feature/change chould work.
- Some rationale/use case for a feature. I'd like to keep CC:T as minimal

View File

@@ -1,4 +0,0 @@
---
name: Something else
about: An issue about something else.
---

View File

@@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "checkstyle",
"pattern": [
{
"regexp": "^([a-z]+) ([\\w./-]+):(\\d+):(\\d+): (.*)$",
"severity": 1,
"file": 2,
"line": 3,
"column": 4,
"message": 5
}
]
}
]
}

View File

@@ -1,18 +0,0 @@
{
"problemMatcher": [
{
"owner": "illuaminate",
"severity": "warning",
"pattern": [
{
"regexp": "^([\\w./-]+):\\[(\\d+):(\\d+)\\-(?:\\d+):(?:\\d+)\\]: (.*) \\[([a-z:-]+)\\]$",
"file": 1,
"line": 2,
"column": 3,
"message": 4,
"code": 5
}
]
}
]
}

View File

@@ -1,15 +0,0 @@
{
"problemMatcher": [
{
"owner": "junit",
"pattern": [
{
"regexp": "^## ([\\w./-]+):(\\d+): (.*)$",
"file": 1,
"line": 2,
"message": 3
}
]
}
]
}

View File

@@ -1,3 +1,9 @@
## A quick checklist
- If there's a existing issue, please link to it. If not, provide fill out the same information you would in a normal issue - reproduction steps for bugs, rationale for use-case.
- If you're working on CraftOS, try to write a few test cases so we can ensure everything continues to work in the future. Tests live in `src/test/resources/test-rom/spec` and can be run with `./gradlew check`.
<!--
Unless this feature is specific to CC:Tweaked, try to [target the original ComputerCraft repo](https://github.com/dan200/ComputerCraft/) instead. There's a lot more people watching it, so it allows the wider community to contribute.
-->
## Useful information to include:
- Brief explanation of the changes you've made.
- Rationale of why this change has been made/reasoning behind it.
The more information you can provide, the easier it is to review something now _and_ to see why a change was made, when the code needs updating in the future.

View File

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

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -eu
DEST="${GITHUB_REF#refs/*/}"
echo "Uploading docs to https://tweaked.cc/$DEST"
# Setup ssh key
mkdir -p "$HOME/.ssh/"
echo "$SSH_KEY" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
# And upload
rsync -avc -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no -p $SSH_PORT" \
"$GITHUB_WORKSPACE/projects/web/build/site/" \
"$SSH_USER@$SSH_HOST:/$DEST"
rsync -avc -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no -p $SSH_PORT" \
"$GITHUB_WORKSPACE/projects/common-api/build/docs/javadoc/" \
"$SSH_USER@$SSH_HOST:/$DEST/javadoc"

View File

@@ -1,40 +0,0 @@
name: Build documentation
on:
push:
branches:
- mc-1.19.x
jobs:
make_doc:
name: Build
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v1
with:
java-version: 17
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}
- name: Build with Gradle
run: ./gradlew compileJava --no-daemon || ./gradlew compileJava --no-daemon
- name: Generate documentation
run: ./gradlew docWebsite :common-api:javadoc --no-daemon
- name: Upload documentation
run: .github/workflows/make-doc.sh 2> /dev/null
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT }}

30
.gitignore vendored
View File

@@ -1,30 +1,12 @@
# Build directories
/classes
/logs
/build
/projects/*/logs
/projects/*/build
/buildSrc/build
/out
/doc/out/
/node_modules
.jqwik-database
# Runtime directories
/run
/projects/*/run
build
out
run
deploy
*.ipr
*.iws
*.iml
.idea
.gradle
luaj-2.0.3/lib
luaj-2.0.3/*.jar
*.DS_Store
/.classpath
/.project
/.settings
/.vscode
*.launch
/projects/*/src/generated/resources/.cache

View File

@@ -1,22 +0,0 @@
image:
file: config/gitpod/Dockerfile
ports:
- port: 25565
onOpen: notify
vscode:
extensions:
- eamodio.gitlens
- github.vscode-pull-request-github
- ms-azuretools.vscode-docker
- redhat.java
- richardwillis.vscode-gradle
- vscjava.vscode-java-debug
- vscode.github
tasks:
- name: Setup pre-commit hool
init: pre-commit install --allow-missing-config
- name: Install npm packages
init: npm ci

View File

@@ -1,54 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
# Quick syntax checkers
- id: check-xml
- id: check-yaml
- id: check-toml
- id: check-json
exclude: "tsconfig\\.json$"
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.3.54
hooks:
- id: editorconfig-checker
args: ['-disable-indentation']
exclude: "^(.*\\.(bat)|LICENSE)$"
- repo: local
hooks:
- id: license
name: Spotless
files: ".*\\.(java|kt|kts)$"
language: system
entry: ./gradlew spotlessApply
pass_filenames: false
require_serial: true
- id: checkstyle
name: Check Java codestyle
files: ".*\\.java$"
language: system
entry: ./gradlew checkstyleMain checkstyleTest
pass_filenames: false
require_serial: true
- id: illuaminate
name: Check Lua code
files: ".*\\.(lua|java|md)"
language: system
entry: ./gradlew lintLua
pass_filenames: false
require_serial: true
exclude: |
(?x)^(
projects/[a-z]+/src/generated|
projects/core/src/test/resources/test-rom/data/json-parsing/|
.*\.dfpwm
)

14
.travis.yml Normal file
View File

@@ -0,0 +1,14 @@
language: java
script: ./gradlew build --no-daemon
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/s
jdk:
- oraclejdk8

View File

@@ -1,133 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
"conduct AT squiddev DOT cc". All complaints will be reviewed and investigated
promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,105 +0,0 @@
# Contributing to CC: Tweaked
As with many open source projects, CC: Tweaked thrives on contributions from other people! This document (hopefully)
provides an introduction as to how to get started in helping out.
If you've any other questions, [just ask the community][community] or [open an issue][new-issue].
## Table of Contents
- [Reporting issues](#reporting-issues)
- [Translations](#translations)
- [Setting up a development environment](#setting-up-a-development-environment)
- [Developing CC: Tweaked](#developing-cc-tweaked)
- [Writing documentation](#writing-documentation)
## Reporting issues
If you have a bug, suggestion, or other feedback, the best thing to do is [file an issue][new-issue]. When doing so, do
use the issue templates - they provide a useful hint on what information to provide.
## Translations
Translations are managed through [Weblate], an online interface for managing language strings. This is synced
automatically with GitHub, so please don't submit PRs adding/changing translations!
## Setting up a development environment
In order to develop CC: Tweaked, you'll need to download the source code and then run it.
- Make sure you've got the following software instealled:
- Java Development Kit (JDK) installed. This can be downloaded from [Adoptium].
- [Git](https://git-scm.com/).
- If you want to work on documentation, [NodeJS][node].
- Download CC: Tweaked's source code:
```
git clone https://github.com/cc-tweaked/CC-Tweaked.git
cd CC-Tweaked
```
- Build CC: Tweaked with `./gradlew build`. This will be very slow the first time it runs, as it needs to download a
lot of dependencies (and decompile Minecraft several times). Subsequent runs should be much faster!
- You're now ready to start developing CC: Tweaked. Running `./gradlew :forge:runClient` or
`./gradle :fabric:runClient` will start Minecraft under Forge and Fabric respectively.
If you want to run CC:T in a normal Minecraft instance, run `./gradlew assemble` and copy the `.jar` from
`projects/forge/build/libs` (for Forge) or `projects/fabric/build/libs` (for Fabric).
## Developing CC: Tweaked
Before making any major changes to CC: Tweaked, I'd recommend you have a read of the [the architecture
document][architecture] first. While it's not a comprehensive document, it gives a good hint of where you should start
looking to make your changes. As always, if you're not sure [do ask the community][community]!
### Testing
When making larger changes, it's may be useful to write a test to make sure your code works as expected.
CC: Tweaked has several test suites, each designed to test something different:
- In order to test CraftOS and its builtin APIs, we have a test suite written in Lua located at
`projects/core/src/test/resources/test-rom/`. These don't rely on any Minecraft code, which means they can run on
emulators, acting as a sort of compliance test.
These tests are written using a test system called "mcfly", heavily inspired by [busted]. Groups of tests go inside
`describe` blocks, and a single test goes inside `it`. Assertions are generally written using `expect` (inspired by
Hamcrest and the like). For instance, `expect(foo):eq("bar")` asserts that your variable `foo` is equal to the
expected value `"bar"`.
These tests can be run with `./gradlew :core:test`.
- In-game functionality, such as the behaviour of blocks and items, is tested using [Minecraft's gametest
system][mc-test] (`projects/common/src/testMod`). These tests spin up a server, spawn a structure for each test, and
then run some code on the blocks defined in that structure.
These tests can be run with `./gradlew runGametest` (or `./gradle :forge:runGametest`/`./gradlew :fabric:runGametest`
for a single loader).
For more information, [see the architecture document][architecture].
## Writing documentation
When writing documentation for [CC: Tweaked's documentation website][docs], it may be useful to build the documentation
and preview it yourself before submitting a PR.
You'll first need to [set up a development environment as above](#setting-up-a-development-environment).
Once this is set up, you can now run `./gradlew docWebsite`. This generates documentation from our Lua and Java code,
writing the resulting HTML into `./projects/web/build/site`, which can then be opened in a browser. When iterating on
documentation, you can instead run `./gradlew docWebsite -t`, which will rebuild documentation every time you change a
file.
Documentation is built using [illuaminate] which, while not currently documented (somewhat ironic), is largely the same
as [ldoc][ldoc]. Documentation comments are written in Markdown, though note that we do not support many GitHub-specific
markdown features - if you can, do check what the documentation looks like locally!
When writing long-form documentation (such as the guides in [doc/guides](doc/guides)), I find it useful to tell a
narrative. Think of what you want the user to learn or achieve, then start introducing a simple concept and then talk
about how you can build on that, until you've covered everything!
[new-issue]: https://github.com/cc-tweaked/CC-Tweaked/issues/new/choose "Create a new issue"
[community]: README.md#community "Get in touch with the community."
[Adoptium]: https://adoptium.net/temurin/releases?version=17 "Download OpenJDK 17"
[checkstyle]: https://checkstyle.org/
[illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub"
[weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance"
[docs]: https://tweaked.cc/ "CC: Tweaked documentation"
[ldoc]: http://stevedonovan.github.io/ldoc/ "ldoc, a Lua documentation generator."
[mc-test]: https://www.youtube.com/watch?v=vXaWOJTCYNg
[busted]: https://github.com/Olivine-Labs/busted "busted: Elegant Lua unit testing."
[node]: https://nodejs.org/en/ "Node.js"
[architecture]: projects/ARCHITECTURE.md

14
LICENSE
View File

@@ -19,14 +19,14 @@ Mod: The mod code designated by the present license, in source form, binary
form, as obtained standalone, as part of a wider distribution or resulting from
the compilation of the original or modified sources.
Dependency: Code required for the mod to work properly. This includes
Dependency: Code required for the mod to work properly. This includes
dependencies required to compile the code as well as any file or modification
that is explicitly or implicitly required for the mod to be working.
1. Scope
--------
The present license is granted to any user of the mod. As a prerequisite,
The present license is granted to any user of the mod. As a prerequisite,
a user must own a legally acquired copy of Minecraft
2. Liability
@@ -41,13 +41,13 @@ or misuse of this mod fall on the user.
3. Play rights
--------------
The user is allowed to install this mod on a Minecraft client or server and to play
The user is allowed to install this mod on a Minecraft client or server and to play
without restriction.
4. Modification rights
----------------------
The user has the right to decompile the source code, look at either the
The user has the right to decompile the source code, look at either the
decompiled version or the original source code, and to modify it.
5. Distribution of original or modified copy rights
@@ -61,10 +61,10 @@ include:
- patch to its source or binary files
- any copy of a portion of its binary source files
The user is allowed to redistribute this mod partially, in totality, or
The user is allowed to redistribute this mod partially, in totality, or
included in a distribution.
When distributing binary files, the user must provide means to obtain its
When distributing binary files, the user must provide means to obtain its
entire set of sources or modified sources at no cost.
All distributions of this mod must remain licensed under the CCPL.
@@ -92,7 +92,7 @@ must be made available at no cost and remain licensed under the CCPL.
---------------
If you choose to contribute code or assets to be included in this mod, you
agree that, if added to to the main repository at
agree that, if added to to the main repository at
https://github.com/dan200/ComputerCraft, your contributions will be covered by
this license, and that Daniel Ratcliffe will retain the right to re-license the
mod, including your contributions, in part or in whole, under other licenses.

19
LICENSE-luaj Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2007 LuaJ. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,66 +1,47 @@
# ![CC: Tweaked](doc/logo.png)
[![Current build status](https://github.com/cc-tweaked/CC-Tweaked/workflows/Build/badge.svg)](https://github.com/cc-tweaked/CC-Tweaked/actions "Current build status") [![Download CC: Tweaked on CurseForge](http://cf.way2muchnoise.eu/title/cc-tweaked.svg)][CurseForge]
# ![CC: Tweaked](logo.png)
[![Build Status](https://travis-ci.org/SquidDev-CC/CC-Tweaked.svg?branch=master)](https://travis-ci.org/SquidDev-CC/CC-Tweaked)
CC: Tweaked is a mod for Minecraft which adds programmable computers, turtles and more to the game. A fork of the
much-beloved [ComputerCraft], it continues its legacy with better performance, stability, and a wealth of new features.
CC: Tweaked is a fork of ComputerCraft which aims to provide earlier access to the more experimental and in-development
features of the mod. For a more stable experience, I recommend checking out the
[original mod](https://github.com/dan200/ComputerCraft).
CC: Tweaked can be installed from [CurseForge] or [Modrinth]. It requires the [Minecraft Forge][forge] mod loader, but
[versions are available for Fabric][ccrestitched].
## What?
CC: Tweaked (or CC:T for short) does not aim to create a competing fork of ComputerCraft, nor am I planning to take it
in in a vastly different direction to the original mod. In fact, CC:T aims to be a nurturing ground for various
features, with a pull request against the original mod being the end goal.
CC:T also includes many pull requests from the community which have not yet been merged, offering a large number
of additional bug fixes and features over the original mod.
## Features
CC: Tweaked contains the all features of the latest alpha, as well as numerous fixes, performance improvements and
several additional features. I'd recommend checking out [the releases page](https://github.com/SquidDev-CC/CC-Tweaked/releases)
to see the full changes, but here's a couple of the more interesting changes:
- Replace LuaJ with Cobalt.
- Allow running multiple computers at the same time.
- Websocket support in the HTTP library.
- Wired modems and cables act more like multiparts.
- Add map-like rendering for pocket computers and printed pages/books.
- Adds the `/computercraft` command, offering various diagnostic tools for server owners. This allows operators to
track which computers are hogging resources, turn on and shutdown multiple computers at once and interact with
computers remotely.
- Add full-block wired modems, allowing one to wrap non-solid peripherals (such as turtles, or chests if Plethora is
installed).
## Relation to CCTweaks?
This mod has nothing to do with CCTweaks, though there is no denying the name is a throwback to it. That being said,
several features have been included, such as full block modems, the Cobalt runtime and map-like rendering for pocket
computers.
## Contributing
Any contribution is welcome, be that using the mod, reporting bugs or contributing code. If you want to get started
developing the mod, [check out the instructions here](CONTRIBUTING.md#developing).
Any contribution is welcome, be that using the mod, reporting bugs or contributing code. If you do wish to contribute
code, do consider submitting it to the ComputerCraft repository instead.
## Community
If you need help getting started with CC: Tweaked, want to show off your latest project, or just want to chat about
ComputerCraft, do check out our [forum] and [GitHub discussions page][GitHub discussions]! There's also a fairly
populated, albeit quiet [IRC channel][irc], if that's more your cup of tea.
That being said, in order to start helping develop CC:T, you'll need to follow these steps:
We also host fairly comprehensive documentation at [tweaked.cc](https://tweaked.cc/ "The CC: Tweaked website").
- **Clone the repository:** `git clone https://github.com/SquidDev-CC/CC-Tweaked.git && cd CC-Tweaked`
- **Setup Forge:** `./gradlew setupDecompWorkspace`
- **Test your changes:** `./gradlew runClient` (or run the `GradleStart` class from your IDE).
## Using
CC: Tweaked is hosted on my maven repo, and so is relatively simple to depend on. You may wish to add a soft (or hard)
dependency in your `mods.toml` file, with the appropriate version bounds, to ensure that API functionality you depend
on is present.
```groovy
repositories {
maven {
url "https://squiddev.cc/maven/"
content {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
includeModule("fuzs.forgeconfigapiport", "forgeconfigapiport-fabric")
}
}
}
dependencies {
// Vanilla (i.e. for multi-loader systems)
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api")
// Forge Gradle
compileOnly(fg.deobf("cc.tweaked:cc-tweaked-$mcVersion-forge-api:$cctVersion"))
runtimeOnly(fg.deobf("cc.tweaked:cc-tweaked-$mcVersion-forge:$cctVersion"))
// Fabric Loom
modCompileOnly("cc.tweaked:cc-tweaked-$mcVersion-fabric-api:$cctVersion")
modRuntimeOnly("cc.tweaked:cc-tweaked-$mcVersion-fabric:$cctVersion")
}
```
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into
exposing more features.
We bundle the API sources with the jar, so documentation should be easily viewable within your editor. Alternatively,
the generated documentation [can be browsed online](https://tweaked.cc/javadoc/).
[computercraft]: https://github.com/dan200/ComputerCraft "ComputerCraft on GitHub"
[curseforge]: https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked from CurseForge"
[modrinth]: https://modrinth.com/mod/gu7yAYhd "Download CC: Tweaked from Modrinth"
[forge]: https://files.minecraftforge.net/ "Download Minecraft Forge."
[ccrestitched]: https://www.curseforge.com/minecraft/mc-mods/cc-restitched "Download CC: Restitched from CurseForge"
[forum]: https://forums.computercraft.cc/
[GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions
[IRC]: https://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet"
If you want to run CC:T in a normal Minecraft instance, run `./gradlew build` and copy the `.jar` from `build/libs`.

205
build.gradle Normal file
View File

@@ -0,0 +1,205 @@
// For those who want the bleeding edge
buildscript {
repositories {
jcenter()
maven {
name = "forge"
url = "http://files.minecraftforge.net/maven"
}
}
dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT'
classpath 'org.ajoberstar:gradle-git:1.6.0'
}
}
plugins {
id 'com.matthewprenger.cursegradle' version '1.0.10'
}
apply plugin: 'net.minecraftforge.gradle.forge'
apply plugin: 'org.ajoberstar.grgit'
apply plugin: 'maven-publish'
apply plugin: 'maven'
version = "1.80pr1.8"
group = "org.squiddev"
archivesBaseName = "cc-tweaked"
minecraft {
version = "1.12.2-14.23.4.2749"
runDir = "run"
replace '${version}', project.version
// the mappings can be changed at any time, and must be in the following format.
// snapshot_YYYYMMDD snapshot are built nightly.
// stable_# stables are built at the discretion of the MCP team.
// Use non-default mappings at your own risk. they may not allways work.
// simply re-run your setup task after changing the mappings to update your workspace.
mappings = "snapshot_20180724"
// makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable.
}
repositories {
mavenLocal()
maven {
name = "JEI"
url = "http://dvs1.progwml6.com/files/maven"
}
maven {
name = "squiddev"
url = "https://dl.bintray.com/squiddev/maven"
}
ivy { artifactPattern "https://asie.pl/files/mods/Charset/LibOnly/[module]-[revision](-[classifier]).[ext]" }
}
configurations {
shade
compile.extendsFrom shade
deployerJars
}
dependencies {
deobfProvided "mezz.jei:jei_1.12.2:4.8.5.159:api"
deobfProvided "pl.asie:Charset-Lib:0.5.4.6"
runtime "mezz.jei:jei_1.12.2:4.8.5.159"
shade 'org.squiddev:Cobalt:0.3.2-nothread'
testCompile 'junit:junit:4.11'
deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0"
}
javadoc {
include "dan200/computercraft/api/**/*.java"
}
jar {
dependsOn javadoc
manifest {
attributes('FMLAT': 'computercraft_at.cfg')
}
into("docs", { from (javadoc.destinationDir) })
into("api", { from (sourceSets.main.allSource) {
include "dan200/computercraft/api/**/*.java"
}})
from configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
}
import org.ajoberstar.grgit.Grgit
processResources {
inputs.property "version", project.version
inputs.property "mcversion", project.minecraft.version
def grgit = Grgit.open(dir: '.')
inputs.property "commithash", grgit.head().id
def blacklist = ['GitHub', 'dan200', 'Daniel Ratcliffe']
Set<String> contributors = []
grgit.log().each {
if (!blacklist.contains(it.author.name)) contributors.add(it.author.name)
if (!blacklist.contains(it.committer.name)) contributors.add(it.committer.name)
}
from(sourceSets.main.resources.srcDirs) {
include 'mcmod.info'
include 'assets/computercraft/lua/rom/help/credits.txt'
expand 'version':project.version,
'mcversion':project.minecraft.version,
'gitcontributors':contributors.sort(false, String.CASE_INSENSITIVE_ORDER).join('\n')
}
from(sourceSets.main.resources.srcDirs) {
exclude 'mcmod.info'
exclude 'assets/computercraft/lua/rom/help/credits.txt'
}
}
curseforge {
apiKey = project.hasProperty('curseForgeApiKey') ? project.curseForgeApiKey : ''
project {
id = '282001'
releaseType = 'beta'
changelog = ''
}
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourceJar
}
}
}
uploadArchives {
repositories {
if(project.hasProperty('mavenUploadUrl')) {
mavenDeployer {
configuration = configurations.deployerJars
repository(url: project.property('mavenUploadUrl')) {
authentication(
userName: project.property('mavenUploadUser'),
privateKey: project.property('mavenUploadKey'))
}
pom.project {
name 'CC: Tweaked'
packaging 'jar'
description 'A fork of ComputerCraft which aims to provide earlier access to the more experimental and in-development features of the mod.'
url 'https://github.com/SquidDev-CC/CC-Tweaked'
scm {
url 'https://github.com/dan200/ComputerCraft.git'
}
issueManagement {
system 'github'
url 'https://github.com/dan200/ComputerCraft/issues'
}
licenses {
license {
name 'ComputerCraft Public License, Version 1.0'
url 'https://github.com/dan200/ComputerCraft/blob/master/LICENSE'
distribution 'repo'
}
}
}
pom.whenConfigured { pom ->
pom.dependencies.clear()
}
}
}
}
}
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint"
}
}
runClient.outputs.upToDateWhen { false }
runServer.outputs.upToDateWhen { false }
test {
testLogging {
events "failed", "standardOut", "standardError"
}
}

View File

@@ -1,52 +0,0 @@
import org.jetbrains.gradle.ext.compiler
import org.jetbrains.gradle.ext.settings
plugins {
publishing
alias(libs.plugins.taskTree)
alias(libs.plugins.githubRelease)
id("org.jetbrains.gradle.plugin.idea-ext")
id("cc-tweaked")
}
val isUnstable = project.properties["isUnstable"] == "true"
val modVersion: String by extra
val mcVersion: String by extra
githubRelease {
token(findProperty("githubApiKey") as String? ?: "")
owner.set("cc-tweaked")
repo.set("CC-Tweaked")
targetCommitish.set(cct.gitBranch)
tagName.set("v$mcVersion-$modVersion")
releaseName.set("[$mcVersion] $modVersion")
body.set(
provider {
"## " + project(":core").file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md")
.readLines()
.takeWhile { it != "Type \"help changelog\" to see the full version history." }
.joinToString("\n").trim()
},
)
prerelease.set(isUnstable)
}
tasks.publish { dependsOn(tasks.githubRelease) }
idea.project.settings.compiler.javac {
// We want ErrorProne to be present when compiling via IntelliJ, as it offers some helpful warnings
// and errors. Loop through our source sets and find the appropriate flags.
moduleJavacAdditionalOptions = subprojects
.asSequence()
.map { evaluationDependsOn(it.path) }
.flatMap { project ->
val sourceSets = project.extensions.findByType(SourceSetContainer::class) ?: return@flatMap sequenceOf()
sourceSets.asSequence().map { sourceSet ->
val name = "${idea.project.name}.${project.name}.${sourceSet.name}"
val compile = project.tasks.named(sourceSet.compileJavaTaskName, JavaCompile::class).get()
name to compile.options.allCompilerArgs.joinToString(" ") { if (it.contains(" ")) "\"$it\"" else it }
}
}
.toMap()
}

View File

@@ -1,70 +0,0 @@
plugins {
`java-gradle-plugin`
`kotlin-dsl`
}
// Duplicated in settings.gradle.kts
repositories {
mavenCentral()
gradlePluginPortal()
maven("https://maven.minecraftforge.net") {
name = "Forge"
content {
includeGroup("net.minecraftforge")
includeGroup("net.minecraftforge.gradle")
}
}
maven("https://maven.parchmentmc.org") {
name = "Librarian"
content {
includeGroupByRegex("^org\\.parchmentmc.*")
}
}
maven("https://repo.spongepowered.org/repository/maven-public/") {
name = "Sponge"
content {
includeGroup("org.spongepowered")
}
}
maven("https://maven.fabricmc.net/") {
name = "Fabric"
content {
includeGroup("net.fabricmc")
}
}
}
dependencies {
implementation(libs.errorProne.plugin)
implementation(libs.kotlin.plugin)
implementation(libs.spotless)
implementation(libs.fabric.loom)
implementation(libs.forgeGradle)
implementation(libs.librarian)
implementation(libs.quiltflower)
implementation(libs.vanillaGradle)
}
gradlePlugin {
plugins {
register("cc-tweaked") {
id = "cc-tweaked"
implementationClass = "cc.tweaked.gradle.CCTweakedPlugin"
}
register("cc-tweaked.illuaminate") {
id = "cc-tweaked.illuaminate"
implementationClass = "cc.tweaked.gradle.IlluaminatePlugin"
}
register("cc-tweaked.node") {
id = "cc-tweaked.node"
implementationClass = "cc.tweaked.gradle.NodePlugin"
}
}
}

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@@ -1,66 +0,0 @@
/** Default configuration for Fabric projects. */
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
`java-library`
id("fabric-loom")
id("io.github.juuxel.loom-quiltflower")
id("cc-tweaked.java-convention")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
repositories {
maven("https://maven.parchmentmc.org/") {
name = "Parchment"
content {
includeGroup("org.parchmentmc.data")
}
}
}
loom {
splitEnvironmentSourceSets()
splitModDependencies.set(true)
}
MinecraftConfigurations.setup(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = "fabric")
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
minecraft("com.mojang:minecraft:$mcVersion")
mappings(
loom.layered {
officialMojangMappings()
parchment(
project.dependencies.create(
group = "org.parchmentmc.data",
name = "parchment-${libs.findVersion("parchmentMc").get()}",
version = libs.findVersion("parchment").get().toString(),
ext = "zip",
),
)
},
)
modImplementation(libs.findLibrary("fabric-loader").get())
modImplementation(libs.findLibrary("fabric-api").get())
// Depend on error prone annotations to silence a lot of compile warnings.
compileOnlyApi(libs.findLibrary("errorProne.annotations").get())
}
tasks.ideaSyncTask {
doLast { IdeaRunConfigurations(project).patch() }
}

View File

@@ -1,39 +0,0 @@
/** Default configuration for Forge projects. */
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("cc-tweaked.java-convention")
id("net.minecraftforge.gradle")
id("org.parchmentmc.librarian.forgegradle")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
minecraft {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
mappings("parchment", "${libs.findVersion("parchmentMc").get()}-${libs.findVersion("parchment").get()}-$mcVersion")
accessTransformer(project(":forge").file("src/main/resources/META-INF/accesstransformer.cfg"))
}
MinecraftConfigurations.setup(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = "forge")
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
"minecraft"("net.minecraftforge:forge:$mcVersion-${libs.findVersion("forge").get()}")
}
tasks.configureEach {
// genIntellijRuns isn't registered until much later, so we need this silly hijinks.
if (name == "genIntellijRuns") doLast { IdeaRunConfigurations(project).patch() }
}

View File

@@ -1,60 +0,0 @@
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
/**
* Sets up the configurations for writing game tests.
*
* See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas.
*/
plugins {
id("cc-tweaked.kotlin-convention")
id("cc-tweaked.java-convention")
}
val main = sourceSets["main"]
val client = sourceSets["client"]
// Both testMod and testFixtures inherit from the main and client classpath, just so we have access to Minecraft classes.
val testMod by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
}
configurations {
named(testMod.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(compileClasspath.get())
}
named(testMod.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(runtimeClasspath.get())
}
}
// Like the main test configurations, we're safe to depend on source set outputs.
dependencies {
add(testMod.implementationConfigurationName, main.output)
add(testMod.implementationConfigurationName, client.output)
}
// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath.
val testFixtures by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath
}
java.registerFeature("testFixtures") {
usingSourceSet(testFixtures)
disablePublication()
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
add(testFixtures.apiConfigurationName, libs.findBundle("test").get())
// Consumers of this project already have the common and client classes on the classpath, so it's fine for these
// to be compile-only.
add(testFixtures.compileOnlyApiConfigurationName, commonClasses(project))
add(testFixtures.compileOnlyApiConfigurationName, clientClasses(project))
testImplementation(testFixtures(project))
}

View File

@@ -1,198 +0,0 @@
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.LicenseHeader
import com.diffplug.gradle.spotless.FormatExtension
import com.diffplug.spotless.LineEnding
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import java.nio.charset.StandardCharsets
plugins {
`java-library`
idea
jacoco
checkstyle
id("com.diffplug.spotless")
id("net.ltgt.errorprone")
}
val modVersion: String by extra
val mcVersion: String by extra
group = "cc.tweaked"
version = modVersion
base.archivesName.convention("cc-tweaked-$mcVersion-${project.name}")
java {
toolchain {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
}
withSourcesJar()
}
repositories {
mavenCentral()
maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
includeGroup("org.squiddev")
includeGroup("cc.tweaked")
// Things we mirror
includeGroup("dev.architectury")
includeGroup("maven.modrinth")
includeGroup("me.shedaniel")
includeGroup("me.shedaniel.cloth")
includeGroup("mezz.jei")
includeModule("com.terraformersmc", "modmenu")
includeModule("fuzs.forgeconfigapiport", "forgeconfigapiport-fabric")
// Until https://github.com/SpongePowered/Mixin/pull/593 is merged
includeModule("org.spongepowered", "mixin")
}
}
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
checkstyle(libs.findLibrary("checkstyle").get())
errorprone(libs.findLibrary("errorProne-core").get())
errorprone(libs.findLibrary("nullAway").get())
}
// Configure default JavaCompile tasks with our arguments.
sourceSets.all {
tasks.named(compileJavaTaskName, JavaCompile::class.java) {
// Processing just gives us "No processor claimed any of these annotations", so skip that!
options.compilerArgs.addAll(listOf("-Xlint", "-Xlint:-processing"))
options.errorprone {
check("InvalidBlockTag", CheckSeverity.OFF) // Broken by @cc.xyz
check("InvalidParam", CheckSeverity.OFF) // Broken by records.
check("InlineMeSuggester", CheckSeverity.OFF) // Minecraft uses @Deprecated liberally
// Too many false positives right now. Maybe we need an indirection for it later on.
check("ReferenceEquality", CheckSeverity.OFF)
check("UnusedVariable", CheckSeverity.OFF) // Too many false positives with records.
check("OperatorPrecedence", CheckSeverity.OFF) // For now.
check("AlreadyChecked", CheckSeverity.OFF) // Seems to be broken?
check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid
check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
check("NullAway", CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", listOf("dan200.computercraft", "net.fabricmc.fabric.api").joinToString(","))
option("NullAway:ExcludedFieldAnnotations", listOf("org.spongepowered.asm.mixin.Shadow").joinToString(","))
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
option("NullAway:CheckOptionalEmptiness")
option("NullAway:AcknowledgeRestrictiveAnnotations")
}
}
}
tasks.compileTestJava {
options.errorprone {
check("NullAway", CheckSeverity.OFF)
}
}
tasks.withType(JavaCompile::class.java).configureEach {
options.encoding = "UTF-8"
}
tasks.withType(AbstractArchiveTask::class.java).configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
dirMode = Integer.valueOf("755", 8)
fileMode = Integer.valueOf("664", 8)
}
tasks.jar {
manifest {
attributes(
"Specification-Title" to "computercraft",
"Specification-Vendor" to "SquidDev",
"Specification-Version" to "1",
"Implementation-Title" to "cctweaked-${project.name}",
"Implementation-Version" to modVersion,
"Implementation-Vendor" to "SquidDev",
)
}
}
tasks.javadoc {
options {
val stdOptions = this as StandardJavadocDocletOptions
stdOptions.addBooleanOption("Xdoclint:all,-missing", true)
stdOptions.links("https://docs.oracle.com/en/java/javase/17/docs/api/")
}
}
tasks.test {
finalizedBy("jacocoTestReport")
useJUnitPlatform()
testLogging {
events("skipped", "failed")
}
}
tasks.withType(JacocoReport::class.java).configureEach {
reports.xml.required.set(true)
reports.html.required.set(true)
}
project.plugins.withType(CCTweakedPlugin::class.java) {
// Set up jacoco to read from /all/ our source directories.
val cct = project.extensions.getByType<CCTweakedExtension>()
project.tasks.named("jacocoTestReport", JacocoReport::class.java) {
for (ref in cct.sourceSets.get()) sourceDirectories.from(ref.allSource.sourceDirectories)
}
}
spotless {
encoding = StandardCharsets.UTF_8
lineEndings = LineEnding.UNIX
fun FormatExtension.defaults() {
endWithNewline()
trimTrailingWhitespace()
indentWithSpaces(4)
}
val licenser = LicenseHeader.create(
api = rootProject.file("config/license/api.txt"),
main = rootProject.file("config/license/main.txt"),
)
java {
defaults()
addStep(licenser)
removeUnusedImports()
}
val ktlintConfig = mapOf(
"disabled_rules" to "no-wildcard-imports",
"ij_kotlin_allow_trailing_comma" to "true",
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
)
kotlinGradle {
defaults()
ktlint().editorConfigOverride(ktlintConfig)
}
kotlin {
defaults()
ktlint().editorConfigOverride(ktlintConfig)
}
}
idea.module {
excludeDirs.addAll(project.files("run", "out", "logs").files)
// Force Gradle to write to inherit the output directory from the parent, instead of writing to out/xxx/classes.
// This is required for Loom, and we patch Forge's run configurations to work there.
// TODO: Submit a patch to Forge to support ProjectRootManager.
inheritOutputDirs = true
}

View File

@@ -1,21 +0,0 @@
import cc.tweaked.gradle.CCTweakedPlugin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
}
kotlin {
jvmToolchain {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
}
}
tasks.withType(KotlinCompile::class.java).configureEach {
// So technically we shouldn't need to do this as the toolchain sets it above. However, the option only appears
// to be set when the task executes, so doesn't get picked up by IDEs.
kotlinOptions.jvmTarget = when {
CCTweakedPlugin.JAVA_VERSION.asInt() > 8 -> CCTweakedPlugin.JAVA_VERSION.toString()
else -> "1.${CCTweakedPlugin.JAVA_VERSION.asInt()}"
}
}

View File

@@ -1,45 +0,0 @@
import org.gradle.kotlin.dsl.`maven-publish`
plugins {
`java-library`
`maven-publish`
}
publishing {
publications {
register<MavenPublication>("maven") {
artifactId = base.archivesName.get()
from(components["java"])
pom {
name.set("CC: Tweaked")
description.set("CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles and more to Minecraft.")
url.set("https://github.com/cc-tweaked/CC-Tweaked")
scm {
url.set("https://github.com/cc-tweaked/CC-Tweaked.git")
}
issueManagement {
system.set("github")
url.set("https://github.com/cc-tweaked/CC-Tweaked/issues")
}
licenses {
license {
name.set("ComputerCraft Public License, Version 1.0")
url.set("https://github.com/cc-tweaked/CC-Tweaked/blob/HEAD/LICENSE")
}
}
}
}
}
repositories {
maven("https://squiddev.cc/maven") {
name = "SquidDev"
credentials(PasswordCredentials::class)
}
}
}

View File

@@ -1,31 +0,0 @@
/** Default configuration for non-modloader-specific Minecraft projects. */
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("cc-tweaked.java-convention")
id("org.spongepowered.gradle.vanilla")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
minecraft {
version(mcVersion)
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
// Depend on error prone annotations to silence a lot of compile warnings.
compileOnlyApi(libs.findLibrary("errorProne.annotations").get())
}
MinecraftConfigurations.setup(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = null)
}

View File

@@ -1,268 +0,0 @@
package cc.tweaked.gradle
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.attributes.TestSuiteType
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.reporting.ReportingExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.configurationcache.extensions.capitalized
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.process.JavaForkOptions
import org.gradle.testing.jacoco.plugins.JacocoCoverageReport
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.gradle.testing.jacoco.tasks.JacocoReport
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
import java.io.IOException
import java.net.URI
import java.net.URL
import java.util.regex.Pattern
abstract class CCTweakedExtension(
private val project: Project,
private val fs: FileSystemOperations,
) {
/** Get the hash of the latest git commit. */
val gitHash: Provider<String> = gitProvider(project, "<no git hash>") {
ProcessHelpers.captureOut("git", "-C", project.rootProject.projectDir.absolutePath, "rev-parse", "HEAD").trim()
}
/** Get the current git branch. */
val gitBranch: Provider<String> = gitProvider(project, "<no git branch>") {
ProcessHelpers.captureOut("git", "-C", project.rootProject.projectDir.absolutePath, "rev-parse", "--abbrev-ref", "HEAD")
.trim()
}
/** Get a list of all contributors to the project. */
val gitContributors: Provider<List<String>> = gitProvider(project, listOf()) {
ProcessHelpers.captureLines(
"git", "-C", project.rootProject.projectDir.absolutePath, "shortlog", "-ns",
"--group=author", "--group=trailer:co-authored-by", "HEAD",
)
.asSequence()
.map {
val matcher = COMMIT_COUNTS.matcher(it)
matcher.find()
matcher.group(1)
}
.filter { !IGNORED_USERS.contains(it) }
.toList()
.sortedWith(String.CASE_INSENSITIVE_ORDER)
}
/**
* References to other sources
*/
val sourceDirectories: SetProperty<SourceSetReference> = project.objects.setProperty(SourceSetReference::class.java)
/** All source sets referenced by this project. */
val sourceSets = sourceDirectories.map { x -> x.map { it.sourceSet } }
init {
sourceDirectories.finalizeValueOnRead()
project.afterEvaluate { sourceDirectories.disallowChanges() }
}
/**
* Mark this project as consuming another project. Its [sourceDirectories] are added, allowing easier configuration
* of run configurations and other tasks which consume sources/classes.
*/
fun externalSources(project: Project) {
val otherCct = project.extensions.getByType(CCTweakedExtension::class.java)
for (sourceSet in otherCct.sourceDirectories.get()) {
sourceDirectories.add(SourceSetReference(sourceSet.sourceSet, classes = sourceSet.classes, external = true))
}
}
/**
* Add a dependency on another project such that its sources and compiles are processed with this one.
*
* This is used when importing a common library into a loader-specific one, as we want to compile sources using
* the loader-specific sources.
*/
fun inlineProject(path: String) {
val otherProject = project.evaluationDependsOn(path)
val otherJava = otherProject.extensions.getByType(JavaPluginExtension::class.java)
val main = otherJava.sourceSets.getByName("main")
val client = otherJava.sourceSets.getByName("client")
val testMod = otherJava.sourceSets.findByName("testMod")
val testFixtures = otherJava.sourceSets.findByName("testFixtures")
// Pull in sources from the other project.
extendSourceSet(otherProject, main)
extendSourceSet(otherProject, client)
if (testMod != null) extendSourceSet(otherProject, testMod)
if (testFixtures != null) extendSourceSet(otherProject, testFixtures)
// The extra source-processing tasks should include these files too.
project.tasks.named(main.javadocTaskName, Javadoc::class.java) { source(main.allJava, client.allJava) }
project.tasks.named(main.sourcesJarTaskName, Jar::class.java) { from(main.allSource, client.allSource) }
sourceDirectories.addAll(SourceSetReference.inline(main), SourceSetReference.inline(client))
}
/**
* Extend a source set with files from another project.
*
* This actually extends the original compile tasks, as extending the source sets does not play well with IDEs.
*/
private fun extendSourceSet(otherProject: Project, sourceSet: SourceSet) {
project.tasks.named(sourceSet.compileJavaTaskName, JavaCompile::class.java) {
dependsOn(otherProject.tasks.named(sourceSet.compileJavaTaskName)) // Avoid duplicate compile errors
source(sourceSet.allJava)
}
project.tasks.named(sourceSet.processResourcesTaskName, ProcessResources::class.java) {
from(sourceSet.resources)
}
// Also try to depend on Kotlin if it exists
val kotlin = otherProject.extensions.findByType(KotlinProjectExtension::class.java)
if (kotlin != null) {
val compileKotlin = sourceSet.getCompileTaskName("kotlin")
project.tasks.named(compileKotlin, KotlinCompile::class.java) {
dependsOn(otherProject.tasks.named(compileKotlin))
source(kotlin.sourceSets.getByName(sourceSet.name).kotlin)
}
}
// If we're doing an IDE sync, add a fake dependency to ensure it's on the classpath.
if (isIdeSync) project.dependencies.add(sourceSet.apiConfigurationName, sourceSet.output)
}
fun linters(@Suppress("UNUSED_PARAMETER") vararg unused: UseNamedArgs, minecraft: Boolean, loader: String?) {
val java = project.extensions.getByType(JavaPluginExtension::class.java)
val sourceSets = java.sourceSets
project.dependencies.run { add("errorprone", project(mapOf("path" to ":lints"))) }
sourceSets.all {
val name = name
project.tasks.named(compileJavaTaskName, JavaCompile::class.java) {
options.errorprone {
// Only the main source set should run the side checker
check("SideChecker", if (minecraft && name == "main") CheckSeverity.DEFAULT else CheckSeverity.OFF)
// The MissingLoaderOverride check superseeds the MissingOverride one, so disable that.
if (loader != null) {
check("MissingOverride", CheckSeverity.OFF)
option("ModLoader", loader)
} else {
check("LoaderOverride", CheckSeverity.OFF)
check("MissingLoaderOverride", CheckSeverity.OFF)
}
}
}
}
}
fun <T> jacoco(task: NamedDomainObjectProvider<T>) where T : Task, T : JavaForkOptions {
val classDump = project.buildDir.resolve("jacocoClassDump/${task.name}")
val reportTaskName = "jacoco${task.name.capitalized()}Report"
val jacoco = project.extensions.getByType(JacocoPluginExtension::class.java)
task.configure {
finalizedBy(reportTaskName)
doFirst("Clean class dump directory") { fs.delete { delete(classDump) } }
jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
classDumpDir = classDump
// Older versions of modlauncher don't include a protection domain (and thus no code
// source). Jacoco skips such classes by default, so we need to explicitly include them.
isIncludeNoLocationClasses = true
}
}
project.tasks.register(reportTaskName, JacocoReport::class.java) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Generates code coverage report for the ${task.name} task."
executionData(task.get())
classDirectories.from(classDump)
// Don't want to use sourceSets(...) here as we have a custom class directory.
for (ref in sourceSets.get()) sourceDirectories.from(ref.allSource.sourceDirectories)
}
project.extensions.configure(ReportingExtension::class.java) {
reports.register("${task.name}CodeCoverageReport", JacocoCoverageReport::class.java) {
testType.set(TestSuiteType.INTEGRATION_TEST)
}
}
}
/**
* Download a file by creating a dummy Ivy repository.
*
* This should only be used for one-off downloads. Using a more conventional Ivy or Maven repository is preferred
* where possible.
*/
fun downloadFile(label: String, url: String): File {
val url = URL(url)
val path = File(url.path)
project.repositories.ivy {
name = label
setUrl(URI(url.protocol, url.userInfo, url.host, url.port, path.parent, null, null))
patternLayout {
artifact("[artifact].[ext]")
}
metadataSources {
artifact()
}
content {
includeModule("cc.tweaked.internal", path.nameWithoutExtension)
}
}
return project.configurations.detachedConfiguration(
project.dependencies.create(
mapOf(
"group" to "cc.tweaked.internal",
"name" to path.nameWithoutExtension,
"ext" to path.extension,
),
),
).resolve().single()
}
companion object {
private val COMMIT_COUNTS = Pattern.compile("""^\s*[0-9]+\s+(.*)$""")
private val IGNORED_USERS = setOf(
"GitHub", "Daniel Ratcliffe", "Weblate",
)
private fun <T> gitProvider(project: Project, default: T, supplier: () -> T): Provider<T> {
return project.provider {
try {
supplier()
} catch (e: IOException) {
project.logger.error("Cannot read Git repository: ${e.message}")
default
} catch (e: GradleException) {
project.logger.error("Cannot read Git repository: ${e.message}")
default
}
}
}
private val isIdeSync: Boolean
get() = java.lang.Boolean.parseBoolean(System.getProperty("idea.sync.active", "false"))
}
}

View File

@@ -1,25 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.jvm.toolchain.JavaLanguageVersion
/**
* Configures projects to match a shared configuration.
*/
class CCTweakedPlugin : Plugin<Project> {
override fun apply(project: Project) {
val cct = project.extensions.create("cct", CCTweakedExtension::class.java)
project.plugins.withType(JavaPlugin::class.java) {
val sourceSets = project.extensions.getByType(JavaPluginExtension::class.java).sourceSets
cct.sourceDirectories.add(SourceSetReference.internal(sourceSets.getByName("main")))
}
}
companion object {
val JAVA_VERSION = JavaLanguageVersion.of(17)
}
}

View File

@@ -1,64 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.language.base.plugins.LifecycleBasePlugin
/**
* Checks the `changelog.md` and `whatsnew.md` files are well-formed.
*/
@CacheableTask
abstract class CheckChangelog : DefaultTask() {
init {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Verifies the changelog and whatsnew file are consistent."
}
@get:Input
abstract val version: Property<String>
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val changelog: RegularFileProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val whatsNew: RegularFileProperty
@TaskAction
fun check() {
val version = version.get()
var ok = true
// Check we're targeting the current version
var whatsNew = whatsNew.get().asFile.readLines()
if (whatsNew[0] != "New features in CC: Tweaked $version") {
ok = false
logger.error("Expected `whatsnew.md' to target $version.")
}
// Check "read more" exists and trim it
val idx = whatsNew.indexOfFirst { it == "Type \"help changelog\" to see the full version history." }
if (idx == -1) {
ok = false
logger.error("Must mention the changelog in whatsnew.md")
} else {
whatsNew = whatsNew.slice(0 until idx)
}
// Check whatsnew and changelog match.
val expectedChangelog = sequenceOf("# ${whatsNew[0]}") + whatsNew.slice(1 until whatsNew.size).asSequence()
val changelog = changelog.get().asFile.readLines()
val mismatch = expectedChangelog.zip(changelog.asSequence()).filter { (a, b) -> a != b }.firstOrNull()
if (mismatch != null) {
ok = false
logger.error("whatsnew and changelog are not in sync")
}
if (!ok) throw GradleException("Could not check release")
}
}

View File

@@ -1,72 +0,0 @@
package cc.tweaked.gradle
import com.diffplug.spotless.FormatterFunc
import com.diffplug.spotless.FormatterStep
import com.diffplug.spotless.generic.LicenseHeaderStep
import java.io.File
import java.io.Serializable
/**
* Similar to [LicenseHeaderStep], but supports multiple licenses.
*/
object LicenseHeader {
/**
* The current year to use in templates. Intentionally not dynamic to avoid failing the build.
*/
private const val YEAR = 2022
private val COMMENT = Regex("""^/\*(.*?)\*/\n?""", RegexOption.DOT_MATCHES_ALL)
fun create(api: File, main: File): FormatterStep = FormatterStep.createLazy(
"license",
{ Licenses(getTemplateText(api), getTemplateText(main)) },
{ state -> FormatterFunc.NeedsFile { contents, file -> formatFile(state, contents, file) } },
)
private fun getTemplateText(file: File): String =
file.readText().trim().replace("\${year}", "$YEAR")
private fun formatFile(licenses: Licenses, contents: String, file: File): String {
val license = getLicense(contents)
val expectedLicense = getExpectedLicense(licenses, file.parentFile)
return when {
license == null -> setLicense(expectedLicense, contents)
license.second != expectedLicense -> setLicense(expectedLicense, contents, license.first)
else -> contents
}
}
private fun getExpectedLicense(licenses: Licenses, root: File): String {
var file: File? = root
while (file != null) {
if (file.name == "api" && file.parentFile?.name == "computercraft") return licenses.api
file = file.parentFile
}
return licenses.main
}
private fun getLicense(contents: String): Pair<Int, String>? {
val match = COMMENT.find(contents) ?: return null
val license = match.groups[1]!!.value
.trim().lineSequence()
.map { it.trimStart(' ', '*') }
.joinToString("\n")
return Pair(match.range.last + 1, license)
}
private fun setLicense(license: String, contents: String, start: Int = 0): String {
val out = StringBuilder()
out.append("/*\n")
for (line in license.lineSequence()) out.append(" * ").append(line).append("\n")
out.append(" */\n")
out.append(contents, start, contents.length)
return out.toString()
}
private data class Licenses(val api: String, val main: String) : Serializable {
companion object {
private const val serialVersionUID: Long = 7741106448372435662L
}
}
}

View File

@@ -1,11 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.provider.Property
import org.gradle.api.tasks.AbstractExecTask
import org.gradle.api.tasks.OutputDirectory
import java.io.File
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
@get:OutputDirectory
abstract val output: Property<File>
}

View File

@@ -1,117 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.tasks.JavaExec
import org.gradle.process.BaseExecSpec
import org.gradle.process.JavaExecSpec
import org.gradle.process.ProcessForkOptions
/**
* Add an annotation processor to all source sets.
*/
fun DependencyHandler.annotationProcessorEverywhere(dep: Any) {
add("compileOnly", dep)
add("annotationProcessor", dep)
add("clientCompileOnly", dep)
add("clientAnnotationProcessor", dep)
add("testCompileOnly", dep)
add("testAnnotationProcessor", dep)
}
/**
* A version of [JavaExecSpec.copyTo] which copies *all* properties.
*/
fun JavaExec.copyToFull(spec: JavaExec) {
copyTo(spec)
// Additional Java options
spec.jvmArgs = jvmArgs // Fabric overrides getJvmArgs so copyTo doesn't do the right thing.
spec.args = args
spec.argumentProviders.addAll(argumentProviders)
spec.mainClass.set(mainClass)
spec.classpath = classpath
spec.javaLauncher.set(javaLauncher)
if (executable != null) spec.setExecutable(executable!!)
// Additional ExecSpec options
copyToExec(spec)
}
/**
* Copy additional [BaseExecSpec] options which aren't handled by [ProcessForkOptions.copyTo].
*/
fun BaseExecSpec.copyToExec(spec: BaseExecSpec) {
spec.isIgnoreExitValue = isIgnoreExitValue
if (standardInput != null) spec.standardInput = standardInput
if (standardOutput != null) spec.standardOutput = standardOutput
if (errorOutput != null) spec.errorOutput = errorOutput
}
/**
* An alternative to [Nothing] with a more descriptive name. Use to enforce calling a function with named arguments:
*
* ```kotlin
* fun f(vararg unused: UseNamedArgs, arg1: Int, arg2: Int) {
* // ...
* }
* ```
*/
class UseNamedArgs private constructor()
/**
* An [AutoCloseable] implementation which can be used to combine other [AutoCloseable] instances.
*
* Values which implement [AutoCloseable] can be dynamically registered with [CloseScope.add]. When the scope is closed,
* each value is closed in the opposite order.
*
* This is largely intended for cases where it's not appropriate to nest [AutoCloseable.use], for instance when nested
* would be too deep.
*/
class CloseScope : AutoCloseable {
private val toClose = ArrayDeque<AutoCloseable>()
/**
* Add a value to be closed when this scope is closed.
*/
public fun add(value: AutoCloseable) {
toClose.addLast(value)
}
override fun close() {
close(null)
}
@PublishedApi
internal fun close(baseException: Throwable?) {
var exception = baseException
while (true) {
var toClose = toClose.removeLastOrNull() ?: break
try {
toClose.close()
} catch (e: Throwable) {
if (exception == null) {
exception = e
} else {
exception.addSuppressed(e)
}
}
}
if (exception != null) throw exception
}
inline fun <R> use(block: (CloseScope) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
close(exception)
}
}
}

View File

@@ -1,170 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.Project
import org.gradle.api.logging.Logging
import org.w3c.dom.Attr
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.xml.sax.InputSource
import java.nio.file.Files
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
/**
* Patches up run configurations from ForgeGradle and Loom.
*
* Would be good to PR some (or all) of these changes upstream at some point.
*
* @see net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask
* @see net.minecraftforge.gradle.common.util.runs.IntellijRunGenerator
*/
internal class IdeaRunConfigurations(project: Project) {
private val rootProject = project.rootProject
private val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
private val xpath = XPathFactory.newInstance().newXPath()
private val writer = TransformerFactory.newInstance().newTransformer()
private val ideaDir = rootProject.file(".idea/")
private val buildDir: Lazy<String?> = lazy {
val ideaMisc = ideaDir.resolve("misc.xml")
try {
val doc = Files.newBufferedReader(ideaMisc.toPath()).use {
documentBuilder.parse(InputSource(it))
}
val node =
xpath.evaluate("//component[@name=\"ProjectRootManager\"]/output", doc, XPathConstants.NODE) as Node
val attr = node.attributes.getNamedItem("url") as Attr
attr.value.removePrefix("file://")
} catch (e: Exception) {
LOGGER.error("Failed to find root directory", e)
null
}
}
fun patch() = synchronized(LOCK) {
val runConfigDir = ideaDir.resolve("runConfigurations")
if (!runConfigDir.isDirectory) return
Files.list(runConfigDir.toPath()).use {
for (configuration in it) {
val filename = configuration.fileName.toString();
when {
filename.endsWith("_fabric.xml") -> patchFabric(configuration)
filename.startsWith("forge_") && filename.endsWith(".xml") -> patchForge(configuration)
else -> {}
}
}
}
}
private fun patchFabric(path: Path) = withXml(path) {
setXml("//configuration", "folderName") { "Fabric" }
}
private fun patchForge(path: Path) = withXml(path) {
val configId = path.fileName.toString().removePrefix("forge_").removeSuffix(".xml")
val sourceSet = forgeConfigs[configId]
if (sourceSet == null) {
LOGGER.error("[{}] Cannot map run configuration to a known source set", path)
return@withXml
}
setXml("//configuration", "folderName") { "Forge" }
setXml("//configuration/module", "name") { "${rootProject.name}.forge.$sourceSet" }
if (buildDir.value == null) return@withXml
setXml("//configuration/envs/env[@name=\"MOD_CLASSES\"]", "value") { classpath ->
val classes = classpath!!.split(':')
val newClasses = mutableListOf<String>()
fun appendUnique(x: String) {
if (!newClasses.contains(x)) newClasses.add(x)
}
for (entry in classes) {
if (!entry.contains("/out/")) {
appendUnique(entry)
continue
}
val match = CLASSPATH_ENTRY.matchEntire(entry)
if (match != null) {
val modId = match.groups["modId"]!!.value
val proj = match.groups["proj"]!!.value
var component = match.groups["component"]!!.value
if (component == "production") component = "main"
appendUnique(forgeModEntry(modId, proj, component))
} else {
LOGGER.warn("[{}] Unknown classpath entry {}", path, entry)
appendUnique(entry)
}
}
// Ensure common code is on the classpath
for (proj in listOf("common", "common-api")) {
for (component in listOf("main", "client")) {
appendUnique(forgeModEntry("computercraft", proj, component))
}
}
if (newClasses.any { it.startsWith("cctest%%") }) {
appendUnique(forgeModEntry("cctest", "core", "testFixtures"))
appendUnique(forgeModEntry("cctest", "common", "testFixtures"))
appendUnique(forgeModEntry("cctest", "common", "testMod"))
}
newClasses.joinToString(":")
}
}
private fun forgeModEntry(mod: String, project: String, component: String) =
"$mod%%${buildDir.value}/production/${rootProject.name}.$project.$component"
private fun LocatedDocument.setXml(xpath: String, attribute: String, value: (String?) -> String) {
val node = this@IdeaRunConfigurations.xpath.evaluate(xpath, document, XPathConstants.NODE) as Node?
if (node == null) {
LOGGER.error("[{}] Cannot find {}", path.fileName, xpath)
return
}
val attr = node.attributes.getNamedItem(attribute) as Attr? ?: document.createAttribute(attribute)
val oldValue = attr.value
attr.value = value(attr.value)
node.attributes.setNamedItem(attr)
if (oldValue != attr.value) {
LOGGER.info("[{}] Setting {}@{}:\n Old: {}\n New: {}", path.fileName, xpath, attribute, oldValue, attr.value)
}
}
private fun withXml(path: Path, run: LocatedDocument.() -> Unit) {
val doc = Files.newBufferedReader(path).use { documentBuilder.parse(InputSource(it)) }
run(LocatedDocument(path, doc))
Files.newBufferedWriter(path).use { writer.transform(DOMSource(doc), StreamResult(it)) }
}
private class LocatedDocument(val path: Path, val document: Document)
companion object {
private val LOGGER = Logging.getLogger(IdeaRunConfigurations::class.java)
private val LOCK = Any()
private val CLASSPATH_ENTRY =
Regex("(?<modId>[a-z]+)%%\\\$PROJECT_DIR\\\$/projects/(?<proj>[a-z-]+)/out/(?<component>\\w+)/(?<type>[a-z]+)\$")
private val forgeConfigs = mapOf(
"runClient" to "client",
"runData" to "main",
"runGameTestServer" to "testMod",
"runServer" to "main",
"runTestClient" to "testMod",
)
}
}

View File

@@ -1,123 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.AbstractExecTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import java.io.File
abstract class IlluaminateExtension {
/** The version of illuaminate to use. */
abstract val version: Property<String>
/** The path to illuaminate. If not given, illuaminate will be downloaded automatically. */
abstract val file: Property<File>
}
class IlluaminatePlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("illuaminate", IlluaminateExtension::class.java)
extension.file.convention(setupDependency(project, extension.version))
project.tasks.register(SetupIlluaminate.NAME, SetupIlluaminate::class.java) {
file.set(extension.file.map { it.absolutePath })
}
}
/** Set up a repository for illuaminate and download our binary from it. */
private fun setupDependency(project: Project, version: Provider<String>): Provider<File> {
project.repositories.ivy {
name = "Illuaminate"
setUrl("https://squiddev.cc/illuaminate/bin/")
patternLayout {
artifact("[revision]/[artifact]-[ext]")
}
metadataSources {
artifact()
}
content {
includeModule("cc.squiddev", "illuaminate")
}
}
return version.map {
val dep = illuaminateArtifact(project, it)
val configuration = project.configurations.detachedConfiguration(dep)
configuration.isTransitive = false
configuration.resolve().single()
}
}
/** Define a dependency for illuaminate from a version number and the current operating system. */
private fun illuaminateArtifact(project: Project, version: String): Dependency {
val osName = System.getProperty("os.name").toLowerCase()
val (os, suffix) = when {
osName.contains("windows") -> Pair("windows", ".exe")
osName.contains("mac os") || osName.contains("darwin") -> Pair("macos", "")
osName.contains("linux") -> Pair("linux", "")
else -> error("Unsupported OS $osName for illuaminate")
}
val osArch = System.getProperty("os.arch").toLowerCase()
val arch = when {
// On macOS the x86_64 binary will work for both ARM and Intel Macs through Rosetta.
os == "macos" -> "x86_64"
osArch == "arm" || osArch.startsWith("aarch") -> error("Unsupported architecture '$osArch' for illuaminate")
osArch.contains("64") -> "x86_64"
else -> error("Unsupported architecture '$osArch' for illuaminate")
}
return project.dependencies.create(
mapOf(
"group" to "cc.squiddev",
"name" to "illuaminate",
"version" to version,
"ext" to "$os-$arch$suffix",
),
)
}
}
private val Task.illuaminatePath: String? // "?" needed to avoid overload ambiguity in setExecutable below.
get() = project.extensions.getByType(IlluaminateExtension::class.java).file.get().absolutePath
/** Prepares illuaminate for being run. This simply requests the dependency and then marks it as executable. */
abstract class SetupIlluaminate : DefaultTask() {
@get:Input
abstract val file: Property<String>
@TaskAction
fun setExecutable() {
val file = File(this.file.get())
if (file.canExecute()) {
didWork = false
return
}
file.setExecutable(true)
}
companion object {
const val NAME: String = "setupIlluaminate"
}
}
abstract class IlluaminateExec : AbstractExecTask<IlluaminateExec>(IlluaminateExec::class.java) {
init {
dependsOn(SetupIlluaminate.NAME)
executable = illuaminatePath
}
}
abstract class IlluaminateExecToDir : ExecToDir() {
init {
dependsOn(SetupIlluaminate.NAME)
executable = illuaminatePath
}
}

View File

@@ -1,62 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.specs.Spec
/**
* A dependency in a POM file.
*/
data class MavenDependency(val groupId: String?, val artifactId: String?, val version: String?, val scope: String?)
/**
* A spec specifying which dependencies to include/exclude.
*/
class MavenDependencySpec {
private val excludeSpecs = mutableListOf<Spec<MavenDependency>>()
fun exclude(spec: Spec<MavenDependency>) {
excludeSpecs.add(spec)
}
fun exclude(dep: Dependency) {
exclude {
(dep.group.isNullOrEmpty() || dep.group == it.groupId) &&
(dep.name.isNullOrEmpty() || dep.name == it.artifactId) &&
(dep.version.isNullOrEmpty() || dep.version == it.version)
}
}
fun exclude(dep: MinimalExternalModuleDependency) {
exclude {
dep.module.group == it.groupId && dep.module.name == it.artifactId
}
}
fun isIncluded(dep: MavenDependency) = !excludeSpecs.any { it.isSatisfiedBy(dep) }
}
/**
* Configure dependencies present in this publication's POM file.
*
* While this approach is very ugly, it's the easiest way to handle it!
*/
fun MavenPublication.mavenDependencies(action: MavenDependencySpec.() -> Unit) {
val spec = MavenDependencySpec()
action(spec)
pom.withXml {
val dependencies = XmlUtil.findChild(asNode(), "dependencies") ?: return@withXml
dependencies.children().map { it as groovy.util.Node }.forEach {
val dep = MavenDependency(
groupId = XmlUtil.findChild(it, "groupId")?.text(),
artifactId = XmlUtil.findChild(it, "artifactId")?.text(),
version = XmlUtil.findChild(it, "version")?.text(),
scope = XmlUtil.findChild(it, "scope")?.text(),
)
if (!spec.isIncluded(dep)) it.parent().remove(it)
}
}
}

View File

@@ -1,189 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.java.TargetJvmVersion
import org.gradle.api.capabilities.Capability
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.named
/**
* This sets up a separate client-only source set, and extends that and the main/common source set with additional
* metadata, to make it easier to consume jars downstream.
*/
class MinecraftConfigurations private constructor(private val project: Project) {
private val java = project.extensions.getByType(JavaPluginExtension::class.java)
private val sourceSets = java.sourceSets
private val configurations = project.configurations
private val objects = project.objects
private val main = sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
private val test = sourceSets[SourceSet.TEST_SOURCE_SET_NAME]
/**
* Performs the initial setup of our configurations.
*/
private fun setup() {
// Define a client source set.
val client = sourceSets.maybeCreate("client")
// Ensure the client classpaths behave the same as the main ones.
configurations.named(client.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
}
configurations.named(client.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
}
// Set up an API configuration for clients (to ensure it's consistent with the main source set).
val clientApi = configurations.maybeCreate(client.apiConfigurationName).apply {
isVisible = false
isCanBeConsumed = false
isCanBeResolved = false
}
configurations.named(client.implementationConfigurationName) { extendsFrom(clientApi) }
/*
Now add outgoing variants for the main and common source sets that we can consume downstream. This is possibly
the worst way to do things, but unfortunately the alternatives don't actually work very well:
- Just using source set outputs: This means dependencies don't propagate, which means when :fabric depends
on :fabric-api, we don't inherit the fake :common-api in IDEA.
- Having separate common/main jars: Nice in principle, but unfortunately Forge needs a separate deobf jar
task (as the original jar is obfuscated), and IDEA is not able to map its output back to a source set.
This works for now, but is incredibly brittle. It's part of the reason we can't use testFixtures inside our
MC projects, as that adds a project(self) -> test dependency, which would pull in the jar instead.
Note we register a fake client jar here. It's not actually needed, but is there to make sure IDEA has
a way to tell that client classes are needed at runtime.
I'm so sorry, deeply aware how cursed this is.
*/
setupOutgoing(main, "CommonOnly")
project.tasks.register(client.jarTaskName, Jar::class.java) {
description = "An empty jar standing in for the client classes."
group = BasePlugin.BUILD_GROUP
archiveClassifier.set("client")
}
setupOutgoing(client)
// Reset the client classpath (Loom configures it slightly differently to this) and add a main -> client
// dependency. Here we /can/ use source set outputs as we add transitive deps by patching the classpath. Nasty,
// but avoids accidentally pulling in Forge's obfuscated jar.
client.compileClasspath = client.compileClasspath + main.compileClasspath
client.runtimeClasspath = client.runtimeClasspath + main.runtimeClasspath
project.dependencies.add(client.apiConfigurationName, main.output)
// Also add client classes to the test classpath. We do the same nasty tricks as needed for main -> client.
test.compileClasspath += client.compileClasspath
test.runtimeClasspath += client.runtimeClasspath
project.dependencies.add(test.implementationConfigurationName, client.output)
// Configure some tasks to include our additional files.
project.tasks.named("javadoc", Javadoc::class.java) {
source(client.allJava)
classpath = main.compileClasspath + main.output + client.compileClasspath + client.output
}
// This are already done by Fabric, but we need it for Forge and vanilla. It shouldn't conflict at all.
project.tasks.named("jar", Jar::class.java) { from(client.output) }
project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) }
project.extensions.configure(CCTweakedExtension::class.java) {
sourceDirectories.add(SourceSetReference.internal(client))
}
}
private fun setupOutgoing(sourceSet: SourceSet, suffix: String = "") {
setupOutgoing("${sourceSet.apiElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_API)) {
description = "API elements for ${sourceSet.name}"
extendsFrom(configurations[sourceSet.apiConfigurationName])
}
setupOutgoing("${sourceSet.runtimeElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_RUNTIME)) {
description = "Runtime elements for ${sourceSet.name}"
extendsFrom(configurations[sourceSet.implementationConfigurationName], configurations[sourceSet.runtimeOnlyConfigurationName])
}
}
/**
* Set up an outgoing configuration for a specific source set. We set an additional "main" or "client" capability
* (depending on the source set name) which allows downstream projects to consume them separately (see
* [DependencyHandler.commonClasses] and [DependencyHandler.clientClasses]).
*/
private fun setupOutgoing(name: String, sourceSet: SourceSet, usage: Usage, configure: Configuration.() -> Unit) {
configurations.register(name) {
isVisible = false
isCanBeConsumed = true
isCanBeResolved = false
configure(this)
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, usage)
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attributeProvider(
TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE,
java.toolchain.languageVersion.map { it.asInt() },
)
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
}
outgoing {
capability(BasicOutgoingCapability(project, sourceSet.name))
// We have two outgoing variants here: the original jar and the classes.
artifact(project.tasks.named(sourceSet.jarTaskName))
variants.create("classes") {
attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
sourceSet.output.classesDirs.forEach { artifact(it) { builtBy(sourceSet.output) } }
}
}
}
}
companion object {
fun setup(project: Project) {
MinecraftConfigurations(project).setup()
}
}
}
private class BasicIncomingCapability(private val module: ModuleDependency, private val name: String) : Capability {
override fun getGroup(): String = module.group!!
override fun getName(): String = "${module.name}-$name"
override fun getVersion(): String? = null
}
private class BasicOutgoingCapability(private val project: Project, private val name: String) : Capability {
override fun getGroup(): String = project.group.toString()
override fun getName(): String = "${project.name}-$name"
override fun getVersion(): String = project.version.toString()
}
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency {
val dep = create(notation) as ModuleDependency
dep.capabilities { requireCapability(BasicIncomingCapability(dep, "client")) }
return dep
}
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency {
val dep = create(notation) as ModuleDependency
dep.capabilities { requireCapability(BasicIncomingCapability(dep, "main")) }
return dep
}

View File

@@ -1,191 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.GradleException
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.invocation.Gradle
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.*
import org.gradle.kotlin.dsl.getByName
import org.gradle.language.base.plugins.LifecycleBasePlugin
import java.io.File
import java.nio.file.Files
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
import javax.inject.Inject
import kotlin.random.Random
/**
* A [JavaExec] task for client-tests. This sets some common setup, and uses [MinecraftRunnerService] to ensure only one
* test runs at once.
*/
abstract class ClientJavaExec : JavaExec() {
private val clientRunner: Provider<MinecraftRunnerService> = MinecraftRunnerService.get(project.gradle)
init {
group = LifecycleBasePlugin.VERIFICATION_GROUP
usesService(clientRunner)
}
/**
* When [false], tests will not be run automatically, allowing the user to debug rendering.
*/
@get:Input
val clientDebug get() = project.hasProperty("clientDebug")
/**
* When [false], tests will not run under a framebuffer.
*/
@get:Input
val useFramebuffer get() = !clientDebug && !project.hasProperty("clientNoFramebuffer")
/**
* The path test results are written to.
*/
@get:OutputFile
val testResults = project.layout.buildDirectory.file("test-results/$name.xml")
/**
* Copy configuration from a task with the given name.
*/
fun copyFrom(path: String) = copyFrom(project.tasks.getByName(path, JavaExec::class))
/**
* Copy configuration from an existing [JavaExec] task.
*/
fun copyFrom(task: JavaExec) {
for (dep in task.dependsOn) dependsOn(dep)
task.copyToFull(this)
if (!clientDebug) systemProperty("cctest.client", "")
systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile)
workingDir(project.buildDir.resolve("gametest").resolve(name))
}
/**
* Only run tests with the given tags.
*/
fun tags(vararg tags: String) {
systemProperty("cctest.tags", tags.joinToString(","))
}
/**
* Write a file with the given contents before starting Minecraft. This may be useful for writing config files.
*/
fun withFileContents(path: Any, contents: Supplier<String>) {
val file = project.file(path).toPath()
doFirst {
Files.createDirectories(file.parent)
Files.writeString(file, contents.get())
}
}
/**
* Copy a file to the provided path before starting Minecraft. This copy only occurs if the file does not already
* exist.
*/
fun withFileFrom(path: Any, source: Supplier<File>) {
val file = project.file(path).toPath()
doFirst {
Files.createDirectories(file.parent)
if (!Files.exists(file)) Files.copy(source.get().toPath(), file)
}
}
@TaskAction
override fun exec() {
Files.createDirectories(workingDir.toPath())
fsOperations.delete { delete(workingDir.resolve("screenshots")) }
if (useFramebuffer) {
clientRunner.get().wrapClient(this) { super.exec() }
} else {
super.exec()
}
}
@get:Inject
protected abstract val fsOperations: FileSystemOperations
}
/**
* A service for [JavaExec] tasks which start Minecraft.
*
* Tasks may run `usesService(MinecraftRunnerService.get(gradle))` to ensure that only one Minecraft-related task runs
* at once.
*/
abstract class MinecraftRunnerService : BuildService<BuildServiceParameters.None> {
private val hasXvfb = lazy {
System.getProperty("os.name", "").equals("linux", ignoreCase = true) && ProcessHelpers.onPath("xvfb-run")
}
internal fun wrapClient(exec: JavaExec, run: () -> Unit) = when {
hasXvfb.value -> runXvfb(exec, run)
else -> run()
}
/**
* Run a program under Xvfb, preventing it spawning a window.
*/
private fun runXvfb(exec: JavaExec, run: () -> Unit) {
fun ProcessBuilder.startVerbose(): Process {
exec.logger.info("Running ${this.command()}")
return start()
}
CloseScope().use { scope ->
val dir = Files.createTempDirectory("cctweaked").toAbsolutePath()
scope.add { fsOperations.delete { delete(dir) } }
val authFile = Files.createTempFile(dir, "Xauthority", "").toAbsolutePath()
val cookie = StringBuilder().also {
for (i in 0..31) it.append("0123456789abcdef"[Random.nextInt(16)])
}.toString()
val xvfb =
ProcessBuilder("Xvfb", "-displayfd", "1", "-screen", "0", "640x480x24", "-nolisten", "tcp").also {
it.inheritIO()
it.environment()["XAUTHORITY"] = authFile.toString()
it.redirectOutput(ProcessBuilder.Redirect.PIPE)
}.startVerbose()
scope.add { xvfb.destroyForcibly().waitFor() }
val server = xvfb.inputReader().use { it.readLine().trim() }
exec.logger.info("Running at :$server (XAUTHORITY=$authFile.toA")
ProcessBuilder("xauth", "add", ":$server", ".", cookie).also {
it.inheritIO()
it.environment()["XAUTHORITY"] = authFile.toString()
}.startVerbose().waitForOrThrow("Failed to setup XAuthority file")
scope.add {
ProcessBuilder("xauth", "remove", ":$server").also {
it.inheritIO()
it.environment()["XAUTHORITY"] = authFile.toString()
}.startVerbose().waitFor()
}
// Wait a few seconds for Xvfb to start. Ugly, but identical to xvfb-run.
if (xvfb.waitFor(3, TimeUnit.SECONDS)) {
throw GradleException("Xvfb unexpectedly exited (with status code ${xvfb.exitValue()})")
}
exec.environment("XAUTHORITY", authFile.toString())
exec.environment("DISPLAY", ":$server")
run()
}
}
@get:Inject
protected abstract val fsOperations: FileSystemOperations
companion object {
fun get(gradle: Gradle): Provider<MinecraftRunnerService> =
gradle.sharedServices.registerIfAbsent("cc.tweaked.gradle.ClientJavaExec", MinecraftRunnerService::class.java) {
maxParallelUsages.set(1)
}
}
}

View File

@@ -1,60 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import java.io.File
class NodePlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("node", NodeExtension::class.java)
project.tasks.register(NpmInstall.TASK_NAME, NpmInstall::class.java) {
projectRoot.convention(extension.projectRoot)
}
}
}
abstract class NodeExtension(project: Project) {
/** The directory containing `package-lock.json` and `node_modules/`. */
abstract val projectRoot: DirectoryProperty
init {
projectRoot.convention(project.layout.projectDirectory)
}
}
/** Installs node modules as dependencies. */
abstract class NpmInstall : DefaultTask() {
@get:Internal
abstract val projectRoot: DirectoryProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
val packageLock: Provider<File> = projectRoot.file("package-lock.json").map { it.asFile }
@get:OutputDirectory
val nodeModules: Provider<Directory> = projectRoot.dir("node_modules")
@TaskAction
fun install() {
project.exec {
commandLine("npm", "ci")
workingDir = projectRoot.get().asFile
}
}
companion object {
internal const val TASK_NAME: String = "npmInstall"
}
}
abstract class NpxExecToDir : ExecToDir() {
init {
dependsOn(NpmInstall.TASK_NAME)
executable = "npx"
}
}

View File

@@ -1,50 +0,0 @@
package cc.tweaked.gradle
import org.codehaus.groovy.runtime.ProcessGroovyMethods
import org.gradle.api.GradleException
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
internal object ProcessHelpers {
fun startProcess(vararg command: String): Process {
// Something randomly passes in "GIT_DIR=" as an environment variable which clobbers everything else. Don't
// inherit the environment array!
return ProcessBuilder()
.command(*command)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.also { it.environment().clear() }
.start()
}
fun captureOut(vararg command: String): String {
val process = startProcess(*command)
process.outputStream.close()
val result = ProcessGroovyMethods.getText(process)
process.waitForOrThrow("Failed to run command")
return result
}
fun captureLines(vararg command: String): List<String> {
val process = startProcess(*command)
process.outputStream.close()
val out = BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
reader.lines().filter { it.isNotEmpty() }.toList()
}
ProcessGroovyMethods.closeStreams(process)
process.waitForOrThrow("Failed to run command")
return out
}
fun onPath(name: String): Boolean {
val path = System.getenv("PATH") ?: return false
return path.splitToSequence(File.pathSeparator).any { File(it, name).exists() }
}
}
internal fun Process.waitForOrThrow(message: String) {
val ret = waitFor()
if (ret != 0) throw GradleException("$message (exited with $ret)")
}

View File

@@ -1,20 +0,0 @@
package cc.tweaked.gradle
import org.gradle.api.tasks.SourceSet
data class SourceSetReference(
val sourceSet: SourceSet,
val classes: Boolean,
val external: Boolean,
) {
companion object {
/** A source set in the current project. */
fun internal(sourceSet: SourceSet) = SourceSetReference(sourceSet, classes = true, external = false)
/** A source set from another project. */
fun external(sourceSet: SourceSet) = SourceSetReference(sourceSet, classes = true, external = true)
/** A source set which is inlined into the current project. */
fun inline(sourceSet: SourceSet) = SourceSetReference(sourceSet, classes = false, external = false)
}
}

View File

@@ -1,12 +0,0 @@
package cc.tweaked.gradle
import groovy.util.Node
import groovy.util.NodeList
object XmlUtil {
fun findChild(node: Node, name: String): Node? = when (val child = node.get(name)) {
is Node -> child
is NodeList -> child.singleOrNull() as Node?
else -> null
}
}

12
build_luaj.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
cd luaj-2.0.3
echo "Building LuaJ..."
ant clean
ant
echo "Copying output to libs..."
rm ../libs/luaj-jse-2.0.3.jar
cp luaj-jse-2.0.3.jar ../libs
echo "Done."
cd ..

10
codesize.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
echo "Java code:"
cat `find src | grep \\.java$` | wc
echo "Lua code:"
cat `find src/main/resources/assets/computercraft/lua | grep \\.lua$` | wc
echo "JSON:"
cat `find src/main/resources/assets/computercraft | grep \\.json$` | wc

View File

@@ -1,167 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="tabWidth" value="4"/>
<property name="charset" value="UTF-8" />
<module name="SuppressionFilter">
<property name="file" value="${config_loc}/suppressions.xml" />
</module>
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="render_old"/>
</module>
<module name="TreeWalker">
<!-- Annotations -->
<module name="AnnotationLocation" />
<module name="AnnotationUseStyle">
<!-- We want trailing commas on multiline arrays. -->
<property name="trailingArrayComma" value="ignore" />
</module>
<module name="MissingDeprecated" />
<module name="MissingOverride" />
<!-- Blocks -->
<module name="EmptyBlock" />
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="ignored" />
</module>
<module name="LeftCurly" />
<module name="NeedBraces">
<property name="allowSingleLineStatement" value="true"/>
</module>
<module name="RightCurly" />
<!-- Class design. As if we've ever followed good practice here. -->
<module name="FinalClass" />
<module name="InterfaceIsType" />
<module name="MutableException" />
<module name="OneTopLevelClass" />
<!-- Coding -->
<module name="ArrayTrailingComma" />
<module name="EqualsHashCode" />
<!-- FallThrough does not handle unreachable code well -->
<module name="IllegalInstantiation" />
<module name="IllegalThrows" />
<module name="ModifiedControlVariable" />
<module name="NoClone" />
<module name="NoFinalizer" />
<module name="OneStatementPerLine" />
<module name="PackageDeclaration" />
<module name="SimplifyBooleanExpression" />
<module name="SimplifyBooleanReturn" />
<module name="StringLiteralEquality" />
<module name="UnnecessaryParentheses">
<!-- Default minus LAND. -->
<property name="tokens" value="EXPR,IDENT,NUM_DOUBLE,NUM_FLOAT,NUM_INT,NUM_LONG,STRING_LITERAL,LITERAL_NULL,LITERAL_FALSE,LITERAL_TRUE,ASSIGN,BAND_ASSIGN,BOR_ASSIGN,BSR_ASSIGN,BXOR_ASSIGN,DIV_ASSIGN,MINUS_ASSIGN,MOD_ASSIGN,PLUS_ASSIGN,SL_ASSIGN,SR_ASSIGN,STAR_ASSIGN,LAMBDA,TEXT_BLOCK_LITERAL_BEGIN,LITERAL_INSTANCEOF,GT,LT,GE,LE,EQUAL,NOT_EQUAL,UNARY_MINUS,UNARY_PLUS,INC,DEC,LNOT,BNOT,POST_INC,POST_DEC" />
</module>
<module name="UnnecessarySemicolonAfterTypeMemberDeclaration" />
<module name="UnnecessarySemicolonInTryWithResources" />
<module name="UnnecessarySemicolonInEnumeration" />
<!-- Imports -->
<module name="CustomImportOrder">
<property name="customImportOrderRules"
value="THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE###STATIC"
/>
</module>
<module name="IllegalImport" />
<module name="RedundantImport" />
<module name="UnusedImports" />
<!-- Javadoc -->
<!-- TODO: Missing* checks for the dan200.computercraft.api package? -->
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
</module>
<module name="InvalidJavadocPosition" />
<module name="JavadocBlockTagLocation" />
<module name="JavadocMethod"/>
<module name="JavadocType"/>
<module name="JavadocStyle">
<property name="checkHtml" value="false" />
</module>
<module name="NonEmptyAtclauseDescription" />
<module name="SingleLineJavadoc" />
<module name="SummaryJavadocCheck"/>
<!-- Misc -->
<module name="ArrayTypeStyle" />
<module name="CommentsIndentation" />
<module name="Indentation" />
<module name="OuterTypeFilename" />
<!-- Modifiers -->
<module name="ModifierOrder" />
<module name="RedundantModifier" />
<!-- Naming -->
<module name="ClassTypeParameterName" />
<module name="InterfaceTypeParameterName" />
<module name="LambdaParameterName" />
<module name="LocalFinalVariableName" />
<module name="LocalVariableName" />
<module name="MemberName" />
<module name="MethodName">
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
</module>
<module name="MethodTypeParameterName" />
<module name="PackageName">
<property name="format" value="^(dan200\.computercraft|cc\.tweaked)(\.[a-z][a-z0-9]*)*" />
</module>
<module name="ParameterName" />
<module name="StaticVariableName">
<property name="format" value="^[a-z][a-zA-Z0-9]*|CAPABILITY(_[A-Z_]+)?$" />
</module>
<module name="TypeName" />
<!-- Whitespace -->
<module name="EmptyForInitializerPad"/>
<module name="EmptyForIteratorPad">
<property name="option" value="space"/>
</module>
<module name="GenericWhitespace" />
<module name="MethodParamPad" />
<module name="NoLineWrap" />
<module name="NoWhitespaceAfter">
<property name="tokens" value="AT,INC,DEC,UNARY_MINUS,UNARY_PLUS,BNOT,LNOT,DOT,ARRAY_DECLARATOR,INDEX_OP,METHOD_REF" />
</module>
<module name="NoWhitespaceBefore" />
<!-- TODO: Decide on an OperatorWrap style. -->
<module name="ParenPad" />
<module name="SeparatorWrap">
<property name="option" value="eol" />
<property name="tokens" value="COMMA,SEMI,ELLIPSIS,ARRAY_DECLARATOR,RBRACK,METHOD_REF" />
</module>
<module name="SeparatorWrap">
<property name="option" value="nl" />
<property name="tokens" value="DOT,AT" />
</module>
<module name="SingleSpaceSeparator" />
<module name="TypecastParenPad" />
<module name="WhitespaceAfter">
<property name="tokens" value="COMMA" />
</module>
<module name="WhitespaceAround">
<property name="ignoreEnhancedForColon" value="false" />
<!-- Allow empty functions -->
<property name="allowEmptyLambdas" value="true" />
<property name="allowEmptyMethods" value="true" />
<property name="allowEmptyConstructors" value="true" />
<property name="allowEmptyTypes" value="true" />
<property name="tokens" value="ASSIGN,BAND,BAND_ASSIGN,BOR,BOR_ASSIGN,BSR,BSR_ASSIGN,BXOR,BXOR_ASSIGN,COLON,DIV,DIV_ASSIGN,EQUAL,GE,GT,LAMBDA,LAND,LCURLY,LE,LITERAL_RETURN,LOR,LT,MINUS,MINUS_ASSIGN,MOD,MOD_ASSIGN,NOT_EQUAL,PLUS,PLUS_ASSIGN,QUESTION,RCURLY,SL,SLIST,SL_ASSIGN,SR,SR_ASSIGN,STAR,STAR_ASSIGN,LITERAL_ASSERT,TYPE_EXTENSION_AND" />
</module>
</module>
<module name="FileTabCharacter" />
<module name="NewlineAtEndOfFile" />
<module name="RegexpSingleline">
<property name="format" value="\s+$"/>
<property name="message" value="Trailing whitespace"/>
</module>
</module>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<!-- All the config options and method fields. -->
<suppress checks="StaticVariableName" files=".*[\\/]ComputerCraft.java" />
<suppress checks="StaticVariableName" files=".*[\\/]ComputerCraftAPI.java" />
<!-- The commands API is documented in Lua. -->
<suppress checks="SummaryJavadocCheck" files=".*[\\/]CommandAPI.java" />
</suppressions>

View File

@@ -1,8 +0,0 @@
FROM gitpod/workspace-base
USER gitpod
RUN sudo apt-get -q update \
&& sudo apt-get install -yq openjdk-16-jdk python3-pip npm \
&& sudo pip3 install pre-commit \
&& sudo update-java-alternatives --set java-1.16.0-openjdk-amd64

View File

@@ -1,3 +0,0 @@
This file is part of the public ComputerCraft API - http://www.computercraft.info
Copyright Daniel Ratcliffe, 2011-${year}. 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.

View File

@@ -1,3 +0,0 @@
This file is part of ComputerCraft - http://www.computercraft.info
Copyright Daniel Ratcliffe, 2011-${year}. Do not distribute without permission.
Send enquiries to dratcliffe@gmail.com

21
deploy.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
echo "Building with gradle..."
rm -rf build/libs
rm -rf build/resources
rm -rf build/classes
chmod -R +rw src/main/resources
chmod +x gradlew
./gradlew build
echo "Deleting old deployment..."
rm -rf deploy
mkdir deploy
echo "Making new deployment..."
INPUTJAR=`ls -1 build/libs | grep -v sources`
OUTPUTJAR=`ls -1 build/libs | grep -v sources | sed s/\-//g`
FRIENDLYNAME=`ls -1 build/libs | grep -v sources | sed s/\-/\ /g | sed s/\.jar//g`
cp build/libs/$INPUTJAR deploy/$OUTPUTJAR
echo "Done."

View File

@@ -1,22 +0,0 @@
---
module: [kind=event] alarm
see: os.setAlarm To start an alarm.
---
The @{alarm} event is fired when an alarm started with @{os.setAlarm} completes.
## Return Values
1. @{string}: The event name.
2. @{number}: The ID of the alarm that finished.
## Example
Starts a timer and then waits for it to complete.
```lua
local alarm_id = os.setAlarm(os.time() + 0.05)
local event, id
repeat
event, id = os.pullEvent("alarm")
until id == alarm_id
print("Alarm with ID " .. id .. " was fired")
```

View File

@@ -1,25 +0,0 @@
---
module: [kind=event] char
see: key To listen to any key press.
---
The @{char} event is fired when a character is typed on the keyboard.
The @{char} event is different to a key press. Sometimes multiple key presses may result in one character being
typed (for instance, on some European keyboards). Similarly, some keys (e.g. <kbd>Ctrl</kbd>) do not have any
corresponding character. The @{key} should be used if you want to listen to key presses themselves.
## Return values
1. @{string}: The event name.
2. @{string}: The string representing the character that was pressed.
## Example
Prints each character the user presses:
```lua
while true do
local event, character = os.pullEvent("char")
print(character .. " was pressed.")
end
```

View File

@@ -1,18 +0,0 @@
---
module: [kind=event] computer_command
---
The @{computer_command} event is fired when the `/computercraft queue` command is run for the current computer.
## Return Values
1. @{string}: The event name.
2. @{string}<abbr title="Variable number of arguments">&hellip;</abbr>: The arguments passed to the command.
## Example
Prints the contents of messages sent:
```lua
while true do
local event = {os.pullEvent("computer_command")}
print("Received message:", table.unpack(event, 2))
end
```

View File

@@ -1,19 +0,0 @@
---
module: [kind=event] disk
see: disk_eject For the event sent when a disk is removed.
---
The @{disk} event is fired when a disk is inserted into an adjacent or networked disk drive.
## Return Values
1. @{string}: The event name.
2. @{string}: The side of the disk drive that had a disk inserted.
## Example
Prints a message when a disk is inserted:
```lua
while true do
local event, side = os.pullEvent("disk")
print("Inserted a disk on side " .. side)
end
```

View File

@@ -1,19 +0,0 @@
---
module: [kind=event] disk_eject
see: disk For the event sent when a disk is inserted.
---
The @{disk_eject} event is fired when a disk is removed from an adjacent or networked disk drive.
## Return Values
1. @{string}: The event name.
2. @{string}: The side of the disk drive that had a disk removed.
## Example
Prints a message when a disk is removed:
```lua
while true do
local event, side = os.pullEvent("disk_eject")
print("Removed a disk on side " .. side)
end
```

View File

@@ -1,42 +0,0 @@
---
module: [kind=event] file_transfer
since: 1.101.0
---
The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer.
This event contains a single argument of type @{TransferredFiles}, which can be used to @{TransferredFiles.getFiles|get
the files to be transferred}. Each file returned is a @{fs.BinaryReadHandle|binary file handle} with an additional
@{TransferredFile.getName|getName} method.
## Return values
1. @{string}: The event name
2. @{TransferredFiles}: The list of transferred files.
## Example
Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file.
```lua
local _, files = os.pullEvent("file_transfer")
for _, file in ipairs(files.getFiles()) do
-- Seek to the end of the file to get its size, then go back to the beginning.
local size = file.seek("end")
file.seek("set", 0)
print(file.getName() .. " " .. file.getSize())
end
```
## Example
Save each transferred file to the computer's storage.
```lua
local _, files = os.pullEvent("file_transfer")
for _, file in ipairs(files.getFiles()) do
local handle = fs.open(file.getName(), "wb")
handle.write(file.readAll())
handle.close()
file.close()
end
```

View File

@@ -1,14 +0,0 @@
---
module: [kind=event] http_check
see: http.checkURLAsync To check a URL asynchronously.
---
The @{http_check} event is fired when a URL check finishes.
This event is normally handled inside @{http.checkURL}, but it can still be seen when using @{http.checkURLAsync}.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL requested to be checked.
3. @{boolean}: Whether the check succeeded.
4. <span class="type">@{string}|@{nil}</span>: If the check failed, a reason explaining why the check failed.

View File

@@ -1,40 +0,0 @@
---
module: [kind=event] http_failure
see: http.request To send an HTTP request.
---
The @{http_failure} event is fired when an HTTP request fails.
This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the site requested.
3. @{string}: An error describing the failure.
4. <span class="type">@{http.Response}|@{nil}</span>: A response handle if the connection succeeded, but the server's
response indicated failure.
## Example
Prints an error why the website cannot be contacted:
```lua
local myURL = "https://does.not.exist.tweaked.cc"
http.request(myURL)
local event, url, err
repeat
event, url, err = os.pullEvent("http_failure")
until url == myURL
print("The URL " .. url .. " could not be reached: " .. err)
```
Prints the contents of a webpage that does not exist:
```lua
local myURL = "https://tweaked.cc/this/does/not/exist"
http.request(myURL)
local event, url, err, handle
repeat
event, url, err, handle = os.pullEvent("http_failure")
until url == myURL
print("The URL " .. url .. " could not be reached: " .. err)
print(handle.getResponseCode())
handle.close()
```

View File

@@ -1,27 +0,0 @@
---
module: [kind=event] http_success
see: http.request To make an HTTP request.
---
The @{http_success} event is fired when an HTTP request returns successfully.
This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the site requested.
3. @{http.Response}: The successful HTTP response.
## Example
Prints the content of a website (this may fail if the request fails):
```lua
local myURL = "https://tweaked.cc/"
http.request(myURL)
local event, url, handle
repeat
event, url, handle = os.pullEvent("http_success")
until url == myURL
print("Contents of " .. url .. ":")
print(handle.readAll())
handle.close()
```

View File

@@ -1,26 +0,0 @@
---
module: [kind=event] key
---
This event is fired when any key is pressed while the terminal is focused.
This event returns a numerical "key code" (for instance, <kbd>F1</kbd> is 290). This value may vary between versions and
so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values.
If the button pressed represented a printable character, then the @{key} event will be followed immediately by a @{char}
event. If you are consuming text input, use a @{char} event instead!
## Return values
1. @{string}: The event name.
2. @{number}: The numerical key value of the key pressed.
3. @{boolean}: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}).
## Example
Prints each key when the user presses it, and if the key is being held.
```lua
while true do
local event, key, is_held = os.pullEvent("key")
print(("%s held=%s"):format(keys.getName(key), is_held))
end
```

View File

@@ -1,24 +0,0 @@
---
module: [kind=event] key_up
see: keys For a lookup table of the given keys.
---
Fired whenever a key is released (or the terminal is closed while a key was being pressed).
This event returns a numerical "key code" (for instance, <kbd>F1</kbd> is 290). This value may vary between versions and
so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values.
## Return values
1. @{string}: The event name.
2. @{number}: The numerical key value of the key pressed.
## Example
Prints each key released on the keyboard whenever a @{key_up} event is fired.
```lua
while true do
local event, key = os.pullEvent("key_up")
local name = keys.getName(key) or "unknown key"
print(name .. " was released.")
end
```

View File

@@ -1,28 +0,0 @@
---
module: [kind=event] modem_message
---
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.
2. @{string}: The side of the modem that received the message.
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. <span class="type">@{number}|@{nil}</span>: The distance between the sender and the receiver in blocks, or @{nil} if the message was sent between dimensions.
## Example
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)
))
end
```

View File

@@ -1,19 +0,0 @@
---
module: [kind=event] monitor_resize
---
The @{monitor_resize} event is fired when an adjacent or networked monitor's size is changed.
## Return Values
1. @{string}: The event name.
2. @{string}: The side or network ID of the monitor that was resized.
## Example
Prints a message when a monitor is resized:
```lua
while true do
local event, side = os.pullEvent("monitor_resize")
print("The monitor on side " .. side .. " was resized.")
end
```

View File

@@ -1,20 +0,0 @@
---
module: [kind=event] monitor_touch
---
The @{monitor_touch} event is fired when an adjacent or networked Advanced Monitor is right-clicked.
## Return Values
1. @{string}: The event name.
2. @{string}: The side or network ID of the monitor that was touched.
3. @{number}: The X coordinate of the touch, in characters.
4. @{number}: The Y coordinate of the touch, in characters.
## Example
Prints a message when a monitor is touched:
```lua
while true do
local event, side, x, y = os.pullEvent("monitor_touch")
print("The monitor on side " .. side .. " was touched at (" .. x .. ", " .. y .. ")")
end
```

View File

@@ -1,32 +0,0 @@
---
module: [kind=event] mouse_click
---
This event is fired when the terminal is clicked with a mouse. This event is only fired on advanced computers (including
advanced turtles and pocket computers).
## Return values
1. @{string}: The event name.
2. @{number}: The mouse button that was clicked.
3. @{number}: The X-coordinate of the click.
4. @{number}: The Y-coordinate of the click.
## Mouse buttons
Several mouse events (@{mouse_click}, @{mouse_up}, @{mouse_scroll}) contain a "mouse button" code. This takes a
numerical value depending on which button on your mouse was last pressed when this event occurred.
| Button Code | Mouse Button |
|------------:|---------------|
| 1 | Left button |
| 2 | Right button |
| 3 | Middle button |
## Example
Print the button and the coordinates whenever the mouse is clicked.
```lua
while true do
local event, button, x, y = os.pullEvent("mouse_click")
print(("The mouse button %s was pressed at %d, %d"):format(button, x, y))
end
```

View File

@@ -1,22 +0,0 @@
---
module: [kind=event] mouse_drag
see: mouse_click For when a mouse button is initially pressed.
---
This event is fired every time the mouse is moved while a mouse button is being held.
## Return values
1. @{string}: The event name.
2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed.
3. @{number}: The X-coordinate of the mouse.
4. @{number}: The Y-coordinate of the mouse.
## Example
Print the button and the coordinates whenever the mouse is dragged.
```lua
while true do
local event, button, x, y = os.pullEvent("mouse_drag")
print(("The mouse button %s was dragged at %d, %d"):format(button, x, y))
end
```

View File

@@ -1,21 +0,0 @@
---
module: [kind=event] mouse_scroll
---
This event is fired when a mouse wheel is scrolled in the terminal.
## Return values
1. @{string}: The event name.
2. @{number}: The direction of the scroll. (-1 = up, 1 = down)
3. @{number}: The X-coordinate of the mouse when scrolling.
4. @{number}: The Y-coordinate of the mouse when scrolling.
## Example
Prints the direction of each scroll, and the position of the mouse at the time.
```lua
while true do
local event, dir, x, y = os.pullEvent("mouse_scroll")
print(("The mouse was scrolled in direction %s at %d, %d"):format(dir, x, y))
end
```

View File

@@ -1,21 +0,0 @@
---
module: [kind=event] mouse_up
---
This event is fired when a mouse button is released or a held mouse leaves the computer's terminal.
## Return values
1. @{string}: The event name.
2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that was released.
3. @{number}: The X-coordinate of the mouse.
4. @{number}: The Y-coordinate of the mouse.
## Example
Prints the coordinates and button number whenever the mouse is released.
```lua
while true do
local event, button, x, y = os.pullEvent("mouse_up")
print(("The mouse button %s was released at %d, %d"):format(button, x, y))
end
```

View File

@@ -1,18 +0,0 @@
---
module: [kind=event] paste
---
The @{paste} event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac).
## Return values
1. @{string}: The event name.
2. @{string} The text that was pasted.
## Example
Prints pasted text:
```lua
while true do
local event, text = os.pullEvent("paste")
print('"' .. text .. '" was pasted')
end
```

View File

@@ -1,19 +0,0 @@
---
module: [kind=event] peripheral
see: peripheral_detach For the event fired when a peripheral is detached.
---
The @{peripheral} event is fired when a peripheral is attached on a side or to a modem.
## Return Values
1. @{string}: The event name.
2. @{string}: The side the peripheral was attached to.
## Example
Prints a message when a peripheral is attached:
```lua
while true do
local event, side = os.pullEvent("peripheral")
print("A peripheral was attached on side " .. side)
end
```

View File

@@ -1,19 +0,0 @@
---
module: [kind=event] peripheral_detach
see: peripheral For the event fired when a peripheral is attached.
---
The @{peripheral_detach} event is fired when a peripheral is detached from a side or from a modem.
## Return Values
1. @{string}: The event name.
2. @{string}: The side the peripheral was detached from.
## Example
Prints a message when a peripheral is detached:
```lua
while true do
local event, side = os.pullEvent("peripheral_detach")
print("A peripheral was detached on side " .. side)
end
```

View File

@@ -1,30 +0,0 @@
---
module: [kind=event] rednet_message
see: modem_message For raw modem messages sent outside of Rednet.
see: rednet.receive To wait for a Rednet message with an optional timeout and protocol filter.
---
The @{rednet_message} event is fired when a message is sent over Rednet.
This event is usually handled by @{rednet.receive}, but it can also be pulled manually.
@{rednet_message} events are sent by @{rednet.run} in the top-level coroutine in response to @{modem_message} events. A @{rednet_message} event is always preceded by a @{modem_message} event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine.
## Return Values
1. @{string}: The event name.
2. @{number}: The ID of the sending computer.
3. @{any}: The message sent.
4. <span class="type">@{string}|@{nil}</span>: The protocol of the message, if provided.
## Example
Prints a message when one is sent:
```lua
while true do
local event, sender, message, protocol = os.pullEvent("rednet_message")
if protocol ~= nil then
print("Received message from " .. sender .. " with protocol " .. protocol .. " and message " .. tostring(message))
else
print("Received message from " .. sender .. " with message " .. tostring(message))
end
end
```

View File

@@ -1,17 +0,0 @@
---
module: [kind=event] redstone
---
The @{event!redstone} event is fired whenever any redstone inputs on the computer change.
## Return values
1. @{string}: The event name.
## Example
Prints a message when a redstone input changes:
```lua
while true do
os.pullEvent("redstone")
print("A redstone input has changed!")
end
```

View File

@@ -1,27 +0,0 @@
---
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
```

View File

@@ -1,28 +0,0 @@
---
module: [kind=event] task_complete
see: commands.execAsync To run a command which fires a task_complete event.
---
The @{task_complete} event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as @{commands.execAsync} return immediately so the user can wait for completion.
## Return Values
1. @{string}: The event name.
2. @{number}: The ID of the task that completed.
3. @{boolean}: Whether the command succeeded.
4. @{string}: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.)
5. <abbr title="Variable number of arguments">&hellip;</abbr>: Any parameters returned from the command.
## Example
Prints the results of an asynchronous command:
```lua
local taskID = commands.execAsync("say Hello")
local event
repeat
event = {os.pullEvent("task_complete")}
until event[2] == taskID
if event[3] == true then
print("Task " .. event[2] .. " succeeded:", table.unpack(event, 4))
else
print("Task " .. event[2] .. " failed: " .. event[4])
end
```

View File

@@ -1,24 +0,0 @@
---
module: [kind=event] term_resize
---
The @{term_resize} event is fired when the main terminal is resized. For instance:
- When a the tab bar is shown or hidden in @{multishell}.
- When the terminal is redirected to a monitor via the "monitor" program and the monitor is resized.
When this event fires, some parts of the terminal may have been moved or deleted. Simple terminal programs (those
not using @{term.setCursorPos}) can ignore this event, but more complex GUI programs should redraw the entire screen.
## Return values
1. @{string}: The event name.
## Example
Print a message each time the terminal is resized.
```lua
while true do
os.pullEvent("term_resize")
local w, h = term.getSize()
print("The term was resized to (" .. w .. ", " .. h .. ")")
end
```

View File

@@ -1,28 +0,0 @@
---
module: [kind=event] terminate
---
The @{terminate} event is fired when <kbd>Ctrl-T</kbd> is held down.
This event is normally handled by @{os.pullEvent}, and will not be returned. However, @{os.pullEventRaw} will return this event when fired.
@{terminate} will be sent even when a filter is provided to @{os.pullEventRaw}. When using @{os.pullEventRaw} with a filter, make sure to check that the event is not @{terminate}.
## Return values
1. @{string}: The event name.
## Example
Prints a message when Ctrl-T is held:
```lua
while true do
local event = os.pullEventRaw("terminate")
if event == "terminate" then print("Terminate requested!") end
end
```
Exits when Ctrl-T is held:
```lua
while true do
os.pullEvent()
end
```

View File

@@ -1,21 +0,0 @@
---
module: [kind=event] timer
see: os.startTimer To start a timer.
---
The @{timer} event is fired when a timer started with @{os.startTimer} completes.
## Return Values
1. @{string}: The event name.
2. @{number}: The ID of the timer that finished.
## Example
Start and wait for a timer to finish.
```lua
local timer_id = os.startTimer(2)
local event, id
repeat
event, id = os.pullEvent("timer")
until id == timer_id
print("Timer with ID " .. id .. " was fired")
```

View File

@@ -1,17 +0,0 @@
---
module: [kind=event] turtle_inventory
---
The @{turtle_inventory} event is fired when a turtle's inventory is changed.
## Return values
1. @{string}: The event name.
## Example
Prints a message when the inventory is changed:
```lua
while true do
os.pullEvent("turtle_inventory")
print("The inventory was changed.")
end
```

View File

@@ -1,21 +0,0 @@
---
module: [kind=event] websocket_closed
---
The @{websocket_closed} event is fired when an open WebSocket connection is closed.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the WebSocket that was closed.
## Example
Prints a message when a WebSocket is closed (this may take a minute):
```lua
local myURL = "wss://example.tweaked.cc/echo"
local ws = http.websocket(myURL)
local event, url
repeat
event, url = os.pullEvent("websocket_closed")
until url == myURL
print("The WebSocket at " .. url .. " was closed.")
```

View File

@@ -1,25 +0,0 @@
---
module: [kind=event] websocket_failure
see: http.websocketAsync To send an HTTP request.
---
The @{websocket_failure} event is fired when a WebSocket connection request fails.
This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the site requested.
3. @{string}: An error describing the failure.
## Example
Prints an error why the website cannot be contacted:
```lua
local myURL = "wss://example.tweaked.cc/not-a-websocket"
http.websocketAsync(myURL)
local event, url, err
repeat
event, url, err = os.pullEvent("websocket_failure")
until url == myURL
print("The URL " .. url .. " could not be reached: " .. err)
```

View File

@@ -1,27 +0,0 @@
---
module: [kind=event] websocket_message
---
The @{websocket_message} event is fired when a message is received on an open WebSocket connection.
This event is normally handled by @{http.Websocket.receive}, but it can also be pulled manually.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the WebSocket.
3. @{string}: The contents of the message.
4. @{boolean}: Whether this is a binary message.
## Example
Prints a message sent by a WebSocket:
```lua
local myURL = "wss://example.tweaked.cc/echo"
local ws = http.websocket(myURL)
ws.send("Hello!")
local event, url, message
repeat
event, url, message = os.pullEvent("websocket_message")
until url == myURL
print("Received message from " .. url .. " with contents " .. message)
ws.close()
```

View File

@@ -1,28 +0,0 @@
---
module: [kind=event] websocket_success
see: http.websocketAsync To open a WebSocket asynchronously.
---
The @{websocket_success} event is fired when a WebSocket connection request returns successfully.
This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}.
## Return Values
1. @{string}: The event name.
2. @{string}: The URL of the site.
3. @{http.Websocket}: The handle for the WebSocket.
## Example
Prints the content of a website (this may fail if the request fails):
```lua
local myURL = "wss://example.tweaked.cc/echo"
http.websocketAsync(myURL)
local event, url, handle
repeat
event, url, handle = os.pullEvent("websocket_success")
until url == myURL
print("Connected to " .. url)
handle.send("Hello!")
print(handle.receive())
handle.close()
```

View File

@@ -1,90 +0,0 @@
---
module: [kind=guide] gps_setup
---
# Setting up GPS
The @{gps} API allows computers and turtles to find their current position using wireless modems.
In order to use GPS, you'll need to set up multiple *GPS hosts*. These are computers running the special `gps host`
program, which tell other computers the host's position. Several hosts running together are known as a *GPS
constellation*.
In order to give the best results, a GPS constellation needs at least four computers. More than four GPS hosts per
constellation is redundant, but it does not cause problems.
## Building a GPS constellation
![An example GPS constellation.](/images/gps-constellation-example.png){.big-image}
We are going to build our GPS constellation as shown in the image above. You will need 4 computers and either 4 wireless
modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your
requesting computers are out of range.
:::tip Ender modems vs wireless modems
Ender modems have a very large range, which makes them very useful for setting up GPS hosts. If you do this then you
will likely only need one GPS constellation for the whole dimension (such as the Overworld or Nether).
If you do use wireless modems then you may find that you need multiple GPS constellations to cover your needs.
A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as it
to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance
parameter of @{modem_message|modem messages} and some maths.
:::
Locate where you want to place your GPS constellation. You will need an area at least 6 blocks high, 6 blocks wide, and
6 blocks deep (6x6x6). If you are using wireless modems then you may want to build your constellation as high as you can
because high altitude boosts modem message range and thus the radius that your constellation covers.
The GPS constellation will only work when it is in a loaded chunk. If you want your constellation to always be
accessible, you may want to permanently load the chunk using a vanilla or modded chunk loader. Make sure that your 6x6x6
area fits in a single chunk to reduce the number of chunks that need to be kept loaded.
Let's get started building the constellation! Place your first computer in one of the corners of your 6x6x6. Remember
which computer this is as other computers need to be placed relative to it. Place the second computer 4 blocks above the
first. Go back to your first computer and place your third computer 5 blocks in front of your first computer, leaving 4
blocks of air between them. Finally for the fourth computer, go back to your first computer and place it 5 blocks right
of your first computer, leaving 4 blocks of air between them.
With all four computers placed within the 6x6x6, place one modem on top of each computer. You should have 4 modems and 4
computers all within your 6x6x6 where each modem is attached to a computer and each computer has a modem.
Currently your GPS constellation will not work, that's because each host is not aware that it's a GPS host. We will fix
this in the next section.
## Configuring the constellation
Now that the structure of your constellation is built, we need to configure each host in it.
Go back to the first computer that you placed and create a startup file, by running `edit startup`.
Type the following code into the file:
```lua
shell.run("gps", "host", x, y, z)
```
Escape from the computer GUI and then press <kbd>F3</kbd> to open Minecraft's debug screen and then look at the computer
(without opening the GUI). On the right of the screen about halfway down you should see an entry labeled `Targeted
Block`, the numbers correspond to the position of the block that you are looking at. Replace `x` with the first number,
`y` with the second number, and `z` with the third number.
For example, if I had a computer at x = 59, y = 5, z = -150, then my <kbd>F3</kbd> debug screen entry would be `Target
Block: 59, 5, -150` and I would change my startup file to this `shell.run("gps", "host", 59, 5, -150)`.
To hide Minecraft's debug screen, press <kbd>F3</kbd> again.
Create similar startup files for the other computers in your constellation, making sure to input the each computer's own
coordinates.
:::caution Modem messages come from the computer's position, not the modem's
Wireless modems transmit from the block that they are attached to *not* the block space that they occupy, the
coordinates that you input into your GPS host should be the position of the computer and not the position of the modem.
:::
Congratulations, your constellation is now fully set up! You can test it by placing another computer close by, placing a
wireless modem on it, and running the `gps locate` program (or calling the @{gps.locate} function).
:::info Why use Minecraft's coordinates?
CC doesn't care if you use Minecraft's coordinate system, so long as all of the GPS hosts with overlapping ranges use
the same reference point (requesting computers will get confused if hosts have different reference points). However,
using MC's coordinate system does provide a nice standard to adopt server-wide. It also is consistent with how command
computers get their location, they use MC's command system to get their block which returns that in MC's coordinate
system.
:::

View File

@@ -1,99 +0,0 @@
---
module: [kind=guide] local_ips
---
# Allowing access to local IPs
By default, ComputerCraft blocks access to local IP addresses for security. This means you can't normally access any
HTTP server running on your computer. However, this may be useful for testing programs without having a remote
server. You can unblock these IPs in the ComputerCraft config.
- [Minecraft 1.13 and later, CC:T 1.87.0 and later](#cc-1.87.0)
- [Minecraft 1.13 and later, CC:T 1.86.2 and earlier](#cc-1.86.2)
- [Minecraft 1.12.2 and earlier](#mc-1.12)
## Minecraft 1.13 and later, CC:T 1.87.0 and later {#cc-1.87.0}
The configuration file can be located at `serverconfig/computercraft-server.toml` inside the world folder on either
single-player or multiplayer. Look for lines that look like this:
```toml
#A list of rules which control behaviour of the "http" API for specific domains or IPs.
#Each rule is an item with a 'host' to match against, and a series of properties. The host may be a domain name ("pastebin.com"),
#wildcard ("*.pastebin.com") or CIDR notation ("127.0.0.0/8"). If no rules, the domain is blocked.
[[http.rules]]
host = "$private"
action = "deny"
```
On 1.95.0 and later, this will be a single entry with `host = "$private"`. On earlier versions, this will be a number of
`[[http.rules]]` with various IP addresses. You will want to remove all of the `[[http.rules]]` entries that have
`action = "deny"`. Then save the file and relaunch Minecraft (Server).
Here's what it should look like after removing:
```toml
#A list of rules which control behaviour of the "http" API for specific domains or IPs.
#Each rule is an item with a 'host' to match against, and a series of properties. The host may be a domain name ("pastebin.com"),
#wildcard ("*.pastebin.com") or CIDR notation ("127.0.0.0/8"). If no rules, the domain is blocked.
[[http.rules]]
#The maximum size (in bytes) that a computer can send or receive in one websocket packet.
max_websocket_message = 131072
host = "*"
#The maximum size (in bytes) that a computer can upload in a single request. This includes headers and POST text.
max_upload = 4194304
action = "allow"
#The maximum size (in bytes) that a computer can download in a single request. Note that responses may receive more data than allowed, but this data will not be returned to the client.
max_download = 16777216
#The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited.
timeout = 30000
```
## Minecraft 1.13 and later, CC:T 1.86.2 and earlier {#cc-1.86.2}
The configuration file for singleplayer is at `.minecraft/config/computercraft-common.toml`. Look for lines that look
like this:
```toml
#A list of wildcards for domains or IP ranges that cannot be accessed through the "http" API on Computers.
#If this is empty then all whitelisted domains will be accessible. Example: "*.github.com" will block access to all subdomains of github.com.
#You can use domain names ("pastebin.com"), wildcards ("*.pastebin.com") or CIDR notation ("127.0.0.0/8").
blacklist = ["127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8"]
```
Remove everything inside the array, leaving the last line as `blacklist = []`. Then save the file and relaunch Minecraft.
Here's what it should look like after removing:
```toml
#A list of wildcards for domains or IP ranges that cannot be accessed through the "http" API on Computers.
#If this is empty then all whitelisted domains will be accessible. Example: "*.github.com" will block access to all subdomains of github.com.
#You can use domain names ("pastebin.com"), wildcards ("*.pastebin.com") or CIDR notation ("127.0.0.0/8").
blacklist = []
```
## Minecraft 1.12.2 and earlier {#mc-1.12}
On singleplayer, the configuration file is located at `.minecraft\config\ComputerCraft.cfg`. On multiplayer, the
configuration file is located at `<server folder>\config\ComputerCraft.cfg`. Look for lines that look like this:
```ini
# A list of wildcards for domains or IP ranges that cannot be accessed through the "http" API on Computers.
# If this is empty then all explicitly allowed domains will be accessible. Example: "*.github.com" will block access to all subdomains of github.com.
# You can use domain names ("pastebin.com"), wildcards ("*.pastebin.com") or CIDR notation ("127.0.0.0/8").
S:blocked_domains <
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
fd00::/8
>
```
Delete everything between the `<>`, leaving the last line as `S:blocked_domains = <>`. Then save the file and relaunch
Minecraft (Server).
Here's what it should look like after removing:
```ini
# A list of wildcards for domains or IP ranges that cannot be accessed through the "http" API on Computers.
# If this is empty then all explicitly allowed domains will be accessible. Example: "*.github.com" will block access to all subdomains of github.com.
# You can use domain names ("pastebin.com"), wildcards ("*.pastebin.com") or CIDR notation ("127.0.0.0/8").
S:blocked_domains <>
```

View File

@@ -1,204 +0,0 @@
---
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 add 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 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 a second and a half 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 1.5 seconds ago to it.
For this, we'll need to keep track of the last 72k samples - exactly 1.5 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 1.5 seconds is 72k 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 * 1.5
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 1.5 seconds 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
-- The audio processing above can be quite slow and preparing the first batch of audio
-- may timeout the computer. We sleep to avoid this.
-- There's definitely better ways of handling this - this is just an example!
sleep(0.05)
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 [GitHub Discussions] 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"
[GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions
[IRC]: https://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet"

View File

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

View File

@@ -1 +0,0 @@
<meta name="theme-color" content="#c8d87c">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

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