mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-02-08 15:10:05 +00:00
Initial commit
This commit is contained in:
commit
073df30e53
33
.editorconfig
Normal file
33
.editorconfig
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
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
|
21
.gitattributes
vendored
Normal file
21
.gitattributes
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.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
|
||||||
|
*.kts eol=lf diff=java
|
||||||
|
*.lua eol=lf
|
||||||
|
*.md eol=lf diff=markdown
|
||||||
|
*.txt eol=lf
|
||||||
|
|
||||||
|
*.png binary
|
||||||
|
*.jar binary
|
||||||
|
*.dfpwm binary
|
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
# Build directories
|
||||||
|
/classes
|
||||||
|
/logs
|
||||||
|
/build
|
||||||
|
/buildSrc/build
|
||||||
|
/build-tools/build
|
||||||
|
/libs
|
||||||
|
/run
|
||||||
|
/projects/*/run
|
||||||
|
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
|
/.classpath
|
||||||
|
/.project
|
||||||
|
/.settings
|
||||||
|
/.vscode
|
||||||
|
*.launch
|
||||||
|
/.java
|
8
.gitmodules
vendored
Normal file
8
.gitmodules
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
[submodule "vendor/Cobalt"]
|
||||||
|
path = vendor/Cobalt
|
||||||
|
url = https://github.com/SquidDev/Cobalt.git
|
||||||
|
branch = java-6
|
35
.pre-commit-config.yaml
Normal file
35
.pre-commit-config.yaml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.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/fsfe/reuse-tool
|
||||||
|
rev: v1.1.0
|
||||||
|
hooks:
|
||||||
|
- id: reuse
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: license
|
||||||
|
name: Spotless
|
||||||
|
files: ".*\\.(java|kt|kts)$"
|
||||||
|
language: system
|
||||||
|
entry: ./gradlew spotlessApply
|
||||||
|
pass_filenames: false
|
||||||
|
require_serial: true
|
30
.reuse/dep5
Normal file
30
.reuse/dep5
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Source: https://github.com/cc-tweaked/cc-tweaked
|
||||||
|
Upstream-Name: CC: Tweaked
|
||||||
|
Upstream-Contact: Jonathan Coates <git@squiddev.cc>
|
||||||
|
|
||||||
|
Files:
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/modules/command/.ignoreme
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/modules/main/.ignoreme
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/modules/turtle/.ignoreme
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/motd.txt
|
||||||
|
src/main/resources/mcmod.info
|
||||||
|
Comment: Several assets where it's inconvenient to create a .license file.
|
||||||
|
Copyright: The CC: Tweaked Developers
|
||||||
|
License: MPL-2.0
|
||||||
|
|
||||||
|
Files:
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/autorun/.ignoreme
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/help/*
|
||||||
|
src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/*
|
||||||
|
src/main/resources/assets/cctweaked/textures/gui/term_font.png
|
||||||
|
Comment: Bulk-license original assets as CCPL.
|
||||||
|
Copyright: 2011 Daniel Ratcliffe
|
||||||
|
License: LicenseRef-CCPL
|
||||||
|
|
||||||
|
Files:
|
||||||
|
gradle/wrapper/*
|
||||||
|
gradlew
|
||||||
|
gradlew.bat
|
||||||
|
Copyright: Gradle Inc
|
||||||
|
License: Apache-2.0
|
73
LICENSES/Apache-2.0.txt
Normal file
73
LICENSES/Apache-2.0.txt
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
121
LICENSES/CC0-1.0.txt
Normal file
121
LICENSES/CC0-1.0.txt
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
98
LICENSES/LicenseRef-CCPL.txt
Normal file
98
LICENSES/LicenseRef-CCPL.txt
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
ComputerCraft Public License
|
||||||
|
============================
|
||||||
|
|
||||||
|
Version 1.0.0 (Based on Minecraft Mod Public License 1.0.1)
|
||||||
|
|
||||||
|
0. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Minecraft: Denotes a copy of the PC Java version of the game “Minecraft” licensed by Mojang AB
|
||||||
|
|
||||||
|
User: Anybody that interacts with the software in one of the following ways:
|
||||||
|
- play
|
||||||
|
- decompile
|
||||||
|
- recompile or compile
|
||||||
|
- modify
|
||||||
|
- distribute
|
||||||
|
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
a user must own a legally acquired copy of Minecraft
|
||||||
|
|
||||||
|
2. Liability
|
||||||
|
------------
|
||||||
|
|
||||||
|
This mod is provided 'as is' with no warranties, implied or otherwise. The owner
|
||||||
|
of this mod takes no responsibility for any damages incurred from the use of
|
||||||
|
this mod. This mod alters fundamental parts of the Minecraft game, parts of
|
||||||
|
Minecraft may not work with this mod installed. All damages caused from the use
|
||||||
|
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
|
||||||
|
without restriction.
|
||||||
|
|
||||||
|
4. Modification rights
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
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
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
Is subject to distribution rights this entire mod in its various forms. This
|
||||||
|
include:
|
||||||
|
- original binary or source forms of this mod files
|
||||||
|
- modified versions of these binaries or source files, as well as binaries
|
||||||
|
resulting from source modifications
|
||||||
|
- 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
|
||||||
|
included in a distribution.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
All dependencies that this mod have on other mods or classes must be licensed
|
||||||
|
under conditions comparable to this version of CCPL, with the exception of the
|
||||||
|
Minecraft code and the mod loading framework (e.g. Forge).
|
||||||
|
|
||||||
|
Modified version of binaries and sources, as well as files containing sections
|
||||||
|
copied from this mod, should be distributed under the terms of the present
|
||||||
|
license.
|
||||||
|
|
||||||
|
7. Use of mod code and assets in other projects
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
It is permitted to use the code and assets contained in this mod (and modified
|
||||||
|
versions thereof) in other Minecraft Mods, provided they are non-commercial.
|
||||||
|
However: the code and assets may not be used in commercial mods, mods for other
|
||||||
|
games, other games, other non-game projects, or any commercial projects.
|
||||||
|
|
||||||
|
When using code covered by this license in other projects, the source code used
|
||||||
|
must be made available at no cost and remain licensed under the CCPL.
|
||||||
|
|
||||||
|
8. Contributing
|
||||||
|
---------------
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
373
LICENSES/MPL-2.0.txt
Normal file
373
LICENSES/MPL-2.0.txt
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
|
||||||
|
SPDX-License-Identifier: MPL-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# ![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](https://img.shields.io/static/v1?label=Download&message=CC:%20Tweaked&color=E04E14&logoColor=E04E14&logo=CurseForge)][CurseForge]
|
||||||
|
[![Download CC: Tweaked on Modrinth](https://img.shields.io/static/v1?label=Download&color=00AF5C&logoColor=00AF5C&logo=Modrinth&message=CC:%20Tweaked)][Modrinth]
|
||||||
|
|
||||||
|
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 improved performance and stability, along with a wealth of
|
||||||
|
new features.
|
||||||
|
|
||||||
|
CC: Tweaked can be installed from [CurseForge] or [Modrinth]. It runs on both [Minecraft Forge] and [Fabric].
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
We also host fairly comprehensive documentation at [tweaked.cc](https://tweaked.cc/ "The CC: Tweaked website").
|
||||||
|
|
||||||
|
## 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Vanilla (i.e. for multi-loader systems)
|
||||||
|
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api")
|
||||||
|
|
||||||
|
// Forge Gradle
|
||||||
|
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-core-api:$cctVersion")
|
||||||
|
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"
|
||||||
|
[Minecraft Forge]: https://files.minecraftforge.net/ "Download Minecraft Forge."
|
||||||
|
[Fabric]: https://fabricmc.net/use/installer/ "Download Fabric."
|
||||||
|
[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"
|
27
build-tools/build.gradle.kts
Normal file
27
build-tools/build.gradle.kts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
application
|
||||||
|
alias(libs.plugins.kotlin)
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(17))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.bundles.asm)
|
||||||
|
implementation(libs.bundles.kotlin)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
manifest.attributes("Main-Class" to "cc.tweaked.build.MainKt")
|
||||||
|
}
|
61
build-tools/src/main/kotlin/cc/tweaked/build/ClassEmitter.kt
Normal file
61
build-tools/src/main/kotlin/cc/tweaked/build/ClassEmitter.kt
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.build
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader
|
||||||
|
import org.objectweb.asm.ClassVisitor
|
||||||
|
import org.objectweb.asm.ClassWriter
|
||||||
|
import org.objectweb.asm.util.CheckClassAdapter
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
/** Generate additional classes which don't exist in the original source set. */
|
||||||
|
interface ClassEmitter {
|
||||||
|
/** Emit a class if it does not already exist. */
|
||||||
|
fun generate(name: String, classReader: ClassReader? = null, flags: Int = 0, write: (ClassVisitor) -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An implementation of [ClassEmitter] which writes files to a directory. */
|
||||||
|
class FileClassEmitter(private val outputDir: Path) : ClassEmitter {
|
||||||
|
private val emitted = mutableSetOf<String>()
|
||||||
|
override fun generate(name: String, classReader: ClassReader?, flags: Int, write: (ClassVisitor) -> Unit) {
|
||||||
|
if (!emitted.add(name)) return
|
||||||
|
|
||||||
|
val cw = NonLoadingClassWriter(classReader, flags)
|
||||||
|
write(CheckClassAdapter(cw))
|
||||||
|
|
||||||
|
val outputFile = outputDir.resolve("$name.class")
|
||||||
|
Files.createDirectories(outputFile.parent)
|
||||||
|
Files.write(outputFile, cw.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A unordered pair, such that (x, y) = (y, x) */
|
||||||
|
private class UnorderedPair<T>(private val x: T, private val y: T) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is cc.tweaked.build.UnorderedPair<*>) return false
|
||||||
|
return (x == other.x && y == other.y) || (x == other.y && y == other.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int = x.hashCode() xor y.hashCode()
|
||||||
|
override fun toString(): String = "UnorderedPair($x, $y)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val subclassRelations = mapOf<UnorderedPair<String>, String>(
|
||||||
|
)
|
||||||
|
|
||||||
|
/** A [ClassWriter] extension which avoids loading classes when computing frames. */
|
||||||
|
private class NonLoadingClassWriter(reader: ClassReader?, flags: Int) : ClassWriter(reader, flags) {
|
||||||
|
override fun getCommonSuperClass(type1: String, type2: String): String {
|
||||||
|
if (type1 == "java/lang/Object" || type2 == "java/lang/Object") return "java/lang/Object"
|
||||||
|
|
||||||
|
val subclass = subclassRelations[UnorderedPair(type1, type2)]
|
||||||
|
if (subclass != null) return subclass
|
||||||
|
|
||||||
|
println("[WARN] Guessing the super-class of $type1 and $type2.")
|
||||||
|
return "java/lang/Object"
|
||||||
|
}
|
||||||
|
}
|
29
build-tools/src/main/kotlin/cc/tweaked/build/Main.kt
Normal file
29
build-tools/src/main/kotlin/cc/tweaked/build/Main.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.build
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import kotlin.io.path.extension
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
if (args.size != 2) {
|
||||||
|
System.err.println("Expected: INPUT OUTPUT")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputDir = Paths.get(args[0])
|
||||||
|
val outputDir = Paths.get(args[1])
|
||||||
|
|
||||||
|
val emitter = FileClassEmitter(outputDir)
|
||||||
|
Files.find(inputDir, Int.MAX_VALUE, { path, _ -> path.extension == "class" }).use { files ->
|
||||||
|
files.forEach { inputFile ->
|
||||||
|
val reader = Files.newInputStream(inputFile).use { ClassReader(it) }
|
||||||
|
emitter.generate(reader.className, flags = 0) { cw -> reader.accept(Unlambda(emitter, cw), 0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
210
build-tools/src/main/kotlin/cc/tweaked/build/Unlambda.kt
Normal file
210
build-tools/src/main/kotlin/cc/tweaked/build/Unlambda.kt
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.build
|
||||||
|
|
||||||
|
import org.objectweb.asm.*
|
||||||
|
import org.objectweb.asm.Opcodes.*
|
||||||
|
|
||||||
|
class Unlambda(private val emitter: ClassEmitter, visitor: ClassVisitor) :
|
||||||
|
ClassVisitor(ASM9, visitor) {
|
||||||
|
internal lateinit var className: String
|
||||||
|
private var isInterface: Boolean = false
|
||||||
|
|
||||||
|
private var lambda = 0
|
||||||
|
|
||||||
|
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String, interfaces: Array<out String>?) {
|
||||||
|
super.visit(V1_6, access, name, signature, superName, interfaces)
|
||||||
|
|
||||||
|
if (version != V1_8) throw IllegalStateException("Expected Java version 8")
|
||||||
|
className = name
|
||||||
|
isInterface = (access and ACC_INTERFACE) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
|
||||||
|
val access = if (access.and(ACC_STATIC) != 0) access.and(ACC_PRIVATE.inv()) else access
|
||||||
|
val mw = super.visitMethod(access, name, descriptor, signature, exceptions) ?: return null
|
||||||
|
|
||||||
|
if (isInterface && name != "<clinit>") {
|
||||||
|
if ((access and ACC_STATIC) != 0) println("[WARN] $className.$name is a static method")
|
||||||
|
else if ((access and ACC_ABSTRACT) == 0) println("[WARN] $className.$name is a default method")
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnlambdaMethodVisitor(this, emitter, mw)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun nextLambdaName(): String {
|
||||||
|
val name = "lambda$lambda"
|
||||||
|
lambda++
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UnlambdaMethodVisitor(
|
||||||
|
private val parent: Unlambda,
|
||||||
|
private val emitter: ClassEmitter,
|
||||||
|
methodVisitor: MethodVisitor,
|
||||||
|
) : MethodVisitor(ASM9, methodVisitor) {
|
||||||
|
private class Bridge(val lambda: Handle, val bridgeName: String)
|
||||||
|
|
||||||
|
private val bridgeMethods = mutableListOf<Bridge>()
|
||||||
|
|
||||||
|
override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) {
|
||||||
|
if (opcode == INVOKESTATIC && isInterface) println("[WARN] Invoke interface $owner.$name in ${parent.className}")
|
||||||
|
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitInvokeDynamicInsn(name: String, descriptor: String, handle: Handle, vararg arguments: Any) {
|
||||||
|
if (handle.owner == "java/lang/invoke/LambdaMetafactory" && handle.name == "metafactory" && handle.desc == "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;") {
|
||||||
|
visitLambda(name, descriptor, arguments[0] as Type, arguments[1] as Handle)
|
||||||
|
} else {
|
||||||
|
super.visitInvokeDynamicInsn(name, descriptor, handle, *arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visitLambda(name: String, descriptor: String, signature: Type, lambda: Handle) {
|
||||||
|
val interfaceTy = Type.getReturnType(descriptor)
|
||||||
|
val fields = Type.getArgumentTypes(descriptor)
|
||||||
|
|
||||||
|
val lambdaName = parent.nextLambdaName()
|
||||||
|
val className = "${parent.className}\$$lambdaName"
|
||||||
|
val bridgeName = "${lambdaName}Bridge"
|
||||||
|
|
||||||
|
emitter.generate(className, flags = ClassWriter.COMPUTE_MAXS) { cw ->
|
||||||
|
cw.visit(V1_6, ACC_FINAL, className, null, "java/lang/Object", arrayOf(interfaceTy.internalName))
|
||||||
|
for ((i, ty) in fields.withIndex()) {
|
||||||
|
cw.visitField(ACC_PRIVATE or ACC_FINAL, "field$i", ty.descriptor, null, null)
|
||||||
|
.visitEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
cw.visitMethod(ACC_STATIC, "create", Type.getMethodDescriptor(interfaceTy, *fields), null, null).let { mw ->
|
||||||
|
mw.visitCode()
|
||||||
|
mw.visitTypeInsn(NEW, className)
|
||||||
|
mw.visitInsn(DUP)
|
||||||
|
for ((i, ty) in fields.withIndex()) mw.visitVarInsn(ty.getOpcode(ILOAD), i)
|
||||||
|
mw.visitMethodInsn(INVOKESPECIAL, className, "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, *fields), false)
|
||||||
|
mw.visitInsn(ARETURN)
|
||||||
|
mw.visitMaxs(0, 0)
|
||||||
|
mw.visitEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
cw.visitMethod(0, "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, *fields), null, null).let { mw ->
|
||||||
|
mw.visitCode()
|
||||||
|
mw.visitVarInsn(ALOAD, 0)
|
||||||
|
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
|
||||||
|
for ((i, ty) in fields.withIndex()) {
|
||||||
|
mw.visitVarInsn(ALOAD, 0)
|
||||||
|
mw.visitVarInsn(ty.getOpcode(ILOAD), i + 1)
|
||||||
|
mw.visitFieldInsn(PUTFIELD, className, "field$i", ty.descriptor)
|
||||||
|
}
|
||||||
|
mw.visitInsn(RETURN)
|
||||||
|
mw.visitMaxs(0, 0)
|
||||||
|
mw.visitEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
cw.visitMethod(ACC_PUBLIC, name, signature.descriptor, null, null).let { mw ->
|
||||||
|
mw.visitCode()
|
||||||
|
|
||||||
|
val targetArgs = when (lambda.tag) {
|
||||||
|
H_INVOKEVIRTUAL, H_INVOKESPECIAL -> arrayOf(
|
||||||
|
Type.getObjectType(lambda.owner),
|
||||||
|
*Type.getArgumentTypes(lambda.desc),
|
||||||
|
)
|
||||||
|
|
||||||
|
H_INVOKESTATIC, H_NEWINVOKESPECIAL -> Type.getArgumentTypes(lambda.desc)
|
||||||
|
else -> throw IllegalStateException("Unhandled opcode")
|
||||||
|
}
|
||||||
|
var targetArgOffset = 0
|
||||||
|
|
||||||
|
// If we're a ::new method handle, create the object.
|
||||||
|
if (lambda.tag == H_NEWINVOKESPECIAL) {
|
||||||
|
mw.visitTypeInsn(NEW, lambda.owner)
|
||||||
|
mw.visitInsn(DUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load our fields
|
||||||
|
for ((i, ty) in fields.withIndex()) {
|
||||||
|
mw.visitVarInsn(ALOAD, 0)
|
||||||
|
mw.visitFieldInsn(GETFIELD, className, "field$i", ty.descriptor)
|
||||||
|
|
||||||
|
val expectedTy = targetArgs[targetArgOffset]
|
||||||
|
if (ty != expectedTy) println("$ty != $expectedTy")
|
||||||
|
targetArgOffset++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the additional arguments
|
||||||
|
val arguments = signature.argumentTypes
|
||||||
|
for ((i, ty) in arguments.withIndex()) {
|
||||||
|
mw.visitVarInsn(ty.getOpcode(ILOAD), i + 1)
|
||||||
|
val expectedTy = targetArgs[targetArgOffset]
|
||||||
|
if (ty != expectedTy) {
|
||||||
|
println("[WARN] $ty != $expectedTy, adding a cast")
|
||||||
|
mw.visitTypeInsn(CHECKCAST, expectedTy.internalName)
|
||||||
|
}
|
||||||
|
targetArgOffset++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke our init call
|
||||||
|
mw.visitMethodInsn(
|
||||||
|
when (lambda.tag) {
|
||||||
|
H_INVOKEVIRTUAL, H_INVOKESPECIAL -> INVOKEVIRTUAL
|
||||||
|
H_INVOKESTATIC -> INVOKESTATIC
|
||||||
|
H_NEWINVOKESPECIAL -> INVOKESPECIAL
|
||||||
|
else -> throw IllegalStateException("Unhandled opcode")
|
||||||
|
},
|
||||||
|
lambda.owner, if (lambda.tag == H_INVOKESPECIAL) bridgeName else lambda.name, lambda.desc, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (lambda.tag != H_NEWINVOKESPECIAL) {
|
||||||
|
val expectedRetTy = signature.returnType
|
||||||
|
val retTy = Type.getReturnType(lambda.desc)
|
||||||
|
if (expectedRetTy != retTy) {
|
||||||
|
// println("[WARN] $retTy != $expectedRetTy, adding a cast")
|
||||||
|
if (retTy == Type.INT_TYPE && expectedRetTy.descriptor == "Ljava/lang/Object;") {
|
||||||
|
mw.visitMethodInsn(INVOKESTATIC, "jav/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false)
|
||||||
|
} else {
|
||||||
|
// println("[ERROR] Unhandled")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A little ugly special handling for ::new
|
||||||
|
mw.visitInsn(
|
||||||
|
if (lambda.tag == H_NEWINVOKESPECIAL) ARETURN else signature.returnType.getOpcode(IRETURN),
|
||||||
|
)
|
||||||
|
mw.visitMaxs(0, 0)
|
||||||
|
mw.visitEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
cw.visitEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're a ::new method handle, create the object.
|
||||||
|
if (lambda.tag == H_INVOKESPECIAL) {
|
||||||
|
bridgeMethods.add(Bridge(lambda, bridgeName))
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMethodInsn(INVOKESTATIC, className, "create", Type.getMethodDescriptor(interfaceTy, *fields), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitEnd() {
|
||||||
|
super.visitEnd()
|
||||||
|
|
||||||
|
for (bridge in bridgeMethods) {
|
||||||
|
println("[INFO] Using bridge method ${bridge.bridgeName} for ${bridge.lambda}")
|
||||||
|
val mw = parent.visitMethod(ACC_PUBLIC, bridge.bridgeName, bridge.lambda.desc, null, null) ?: continue
|
||||||
|
mw.visitCode()
|
||||||
|
mw.visitVarInsn(ALOAD, 0)
|
||||||
|
for ((i, ty) in Type.getArgumentTypes(bridge.lambda.desc)
|
||||||
|
.withIndex()) mw.visitVarInsn(ty.getOpcode(ILOAD), i + 1)
|
||||||
|
mw.visitMethodInsn(INVOKESPECIAL, bridge.lambda.owner, bridge.lambda.name, bridge.lambda.desc, false)
|
||||||
|
mw.visitInsn(Type.getReturnType(bridge.lambda.desc).getOpcode(IRETURN))
|
||||||
|
val size = 1 + Type.getArgumentTypes(bridge.lambda.desc).size
|
||||||
|
mw.visitMaxs(size, size)
|
||||||
|
mw.visitEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
167
build.gradle.kts
Normal file
167
build.gradle.kts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
import com.diffplug.gradle.spotless.FormatExtension
|
||||||
|
import com.diffplug.spotless.LineEnding
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.voldeloom)
|
||||||
|
alias(libs.plugins.spotless)
|
||||||
|
}
|
||||||
|
|
||||||
|
val modVersion: String by extra
|
||||||
|
val mcVersion: String by extra
|
||||||
|
|
||||||
|
group = "cc.tweaked"
|
||||||
|
version = modVersion
|
||||||
|
|
||||||
|
base.archivesName.convention("cc-tweaked-$mcVersion")
|
||||||
|
|
||||||
|
java {
|
||||||
|
// Last version able to set a --release as low as 6
|
||||||
|
toolchain.languageVersion.set(JavaLanguageVersion.of(11))
|
||||||
|
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.compileJava { options.release.set(8) }
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
|
||||||
|
exclusiveContent {
|
||||||
|
forRepository { maven("https://api.modrinth.com/maven") { name = "Modrinth" } }
|
||||||
|
filter { includeGroup("maven.modrinth") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volde {
|
||||||
|
forgeCapabilities {
|
||||||
|
setSrgsAsFallback(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
runs {
|
||||||
|
named("client") {
|
||||||
|
programArg("SquidDev")
|
||||||
|
property("fml.coreMods.load", "cc.tweaked.patch.CorePlugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
val shade by registering
|
||||||
|
compileOnly { extendsFrom(shade.get()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val buildTools by configurations.creating {
|
||||||
|
isCanBeConsumed = false
|
||||||
|
isCanBeResolved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
minecraft("com.mojang:minecraft:$mcVersion")
|
||||||
|
forge("net.minecraftforge:forge:${libs.versions.forge.get()}:universal@zip")
|
||||||
|
|
||||||
|
mappings(
|
||||||
|
volde.layered {
|
||||||
|
importBaseZip("net.minecraftforge:forge:${libs.versions.forge.get()}:src@zip")
|
||||||
|
removeClasses(listOf("bar", "bas"))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
modImplementation("maven.modrinth:computercraft:1.50")
|
||||||
|
"shade"("org.squiddev:Cobalt")
|
||||||
|
|
||||||
|
"buildTools"(project(":build-tools"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point compileJava to emit to classes/uninstrumentedJava/main, and then add a task to instrument these classes,
|
||||||
|
// saving them back to the the original class directory. This is held together with so much string :(.
|
||||||
|
val mainSource = sourceSets.main.get()
|
||||||
|
val javaClassesDir = mainSource.java.classesDirectory.get()
|
||||||
|
val untransformedClasses = project.layout.buildDirectory.dir("classes/uninstrumentedJava/main")
|
||||||
|
|
||||||
|
val instrumentJava = tasks.register(mainSource.getTaskName("Instrument", "Java"), JavaExec::class) {
|
||||||
|
dependsOn(tasks.compileJava)
|
||||||
|
inputs.dir(untransformedClasses).withPropertyName("inputDir")
|
||||||
|
outputs.dir(javaClassesDir).withPropertyName("outputDir")
|
||||||
|
|
||||||
|
javaLauncher.set(
|
||||||
|
javaToolchains.launcherFor {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(17))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mainClass.set("cc.tweaked.build.MainKt")
|
||||||
|
classpath = buildTools
|
||||||
|
|
||||||
|
args = listOf(
|
||||||
|
untransformedClasses.get().asFile.absolutePath,
|
||||||
|
javaClassesDir.asFile.absolutePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
doFirst { project.delete(javaClassesDir) }
|
||||||
|
}
|
||||||
|
|
||||||
|
mainSource.compiledBy(instrumentJava)
|
||||||
|
tasks.compileJava {
|
||||||
|
destinationDirectory.set(untransformedClasses)
|
||||||
|
finalizedBy(instrumentJava)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(AbstractArchiveTask::class.java).configureEach {
|
||||||
|
isPreserveFileTimestamps = false
|
||||||
|
isReproducibleFileOrder = true
|
||||||
|
dirMode = Integer.valueOf("755", 8)
|
||||||
|
fileMode = Integer.valueOf("664", 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
"FMLCorePlugin" to "cc.tweaked.patch.CorePlugin",
|
||||||
|
"FMLCorePluginContainsFMLMod" to "true",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
from(configurations["shade"].map { if (it.isDirectory) it else zipTree(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
filesMatching("mcmod.info") {
|
||||||
|
expand("version" to project.version, "mcVersion" to mcVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
encoding = StandardCharsets.UTF_8
|
||||||
|
lineEndings = LineEnding.UNIX
|
||||||
|
|
||||||
|
fun FormatExtension.defaults() {
|
||||||
|
endWithNewline()
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
indentWithSpaces(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
defaults()
|
||||||
|
removeUnusedImports()
|
||||||
|
}
|
||||||
|
|
||||||
|
val ktlintConfig = mapOf(
|
||||||
|
"ktlint_standard_no-wildcard-imports" to "disabled",
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
9
gradle.properties
Normal file
9
gradle.properties
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
org.gradle.jvmargs=-Xmx3G
|
||||||
|
|
||||||
|
modVersion=1.105.0
|
||||||
|
|
||||||
|
mcVersion=1.4.7
|
28
gradle/libs.versions.toml
Normal file
28
gradle/libs.versions.toml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
[versions]
|
||||||
|
forge = "1.4.7-6.6.2.534"
|
||||||
|
|
||||||
|
asm = "9.3"
|
||||||
|
kotlin = "1.8.10"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
|
||||||
|
asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" }
|
||||||
|
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
|
||||||
|
asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
|
||||||
|
asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }
|
||||||
|
|
||||||
|
kotlin-platform = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
|
||||||
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
|
|
||||||
|
[bundles]
|
||||||
|
asm = ["asm", "asm-analysis", "asm-commons", "asm-tree", "asm-util"]
|
||||||
|
kotlin = ["kotlin-stdlib"]
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
spotless = { id = "com.diffplug.spotless", version = "6.19.0" }
|
||||||
|
voldeloom = { id = "agency.highlysuspect.voldeloom", version = "2.4-SNAPSHOT" }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
244
gradlew
vendored
Executable file
244
gradlew
vendored
Executable file
@ -0,0 +1,244 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
42
settings.gradle.kts
Normal file
42
settings.gradle.kts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
// Duplicated in buildSrc/build.gradle.kts
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
|
||||||
|
maven("https://maven.fabricmc.net/") {
|
||||||
|
name = "Fabric"
|
||||||
|
content {
|
||||||
|
includeGroup("fabric-loom")
|
||||||
|
includeGroup("net.fabricmc")
|
||||||
|
includeModule("org.jetbrains", "intellij-fernflower")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maven("https://repo.sleeping.town") {
|
||||||
|
name = "Voldeloom"
|
||||||
|
content {
|
||||||
|
includeGroup("agency.highlysuspect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionStrategy {
|
||||||
|
eachPlugin {
|
||||||
|
if (requested.id.id == "agency.highlysuspect.voldeloom") {
|
||||||
|
useModule("agency.highlysuspect:voldeloom:${requested.version}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mcVersion: String by settings
|
||||||
|
rootProject.name = "cc-tweaked-$mcVersion"
|
||||||
|
|
||||||
|
includeBuild("vendor/Cobalt")
|
||||||
|
|
||||||
|
include("build-tools")
|
48
src/main/java/cc/tweaked/CCTweaked.java
Normal file
48
src/main/java/cc/tweaked/CCTweaked.java
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked;
|
||||||
|
|
||||||
|
import cpw.mods.fml.common.Mod;
|
||||||
|
import cpw.mods.fml.relauncher.FMLRelaunchLog;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
@Mod(
|
||||||
|
modid = "cctweaked", name = "CC: Tweaked",
|
||||||
|
version = "1.105.0",
|
||||||
|
dependencies = "required-after:ComputerCraft"
|
||||||
|
)
|
||||||
|
public class CCTweaked {
|
||||||
|
public static final Logger LOG = Logger.getLogger("CC: Tweaked");
|
||||||
|
|
||||||
|
static {
|
||||||
|
LOG.setParent(FMLRelaunchLog.log.getLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getLoadingJar() {
|
||||||
|
String path = CCTweaked.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||||
|
int bangIndex = path.indexOf('!');
|
||||||
|
if (bangIndex >= 0) path = path.substring(0, bangIndex);
|
||||||
|
|
||||||
|
URL url;
|
||||||
|
try {
|
||||||
|
url = new URL(path);
|
||||||
|
} catch (MalformedURLException e1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file;
|
||||||
|
try {
|
||||||
|
file = new File(url.toURI());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
file = new File(url.getPath());
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
94
src/main/java/cc/tweaked/patch/ClassTransformer.java
Normal file
94
src/main/java/cc/tweaked/patch/ClassTransformer.java
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch;
|
||||||
|
|
||||||
|
import cc.tweaked.patch.framework.TransformationChain;
|
||||||
|
import cc.tweaked.patch.framework.transform.BasicRemapper;
|
||||||
|
import cc.tweaked.patch.framework.transform.ClassMerger;
|
||||||
|
import cc.tweaked.patch.framework.transform.ReplaceConstant;
|
||||||
|
import cc.tweaked.patch.framework.transform.Transform;
|
||||||
|
import cpw.mods.fml.relauncher.IClassTransformer;
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
|
||||||
|
import static org.objectweb.asm.Opcodes.ALOAD;
|
||||||
|
import static org.objectweb.asm.Opcodes.GETFIELD;
|
||||||
|
|
||||||
|
public class ClassTransformer implements IClassTransformer {
|
||||||
|
private final TransformationChain chain = new TransformationChain()
|
||||||
|
// Use Cobalt instead of LuaJ
|
||||||
|
.atMethod(
|
||||||
|
"dan200.computer.core.Computer", "initLua", "()V",
|
||||||
|
BasicRemapper.remapType("dan200/computer/core/LuaJLuaMachine", "dan200/computercraft/core/lua/CobaltLuaMachine").toMethodTransform()
|
||||||
|
)
|
||||||
|
// Replace a bunch of core APIS
|
||||||
|
.atMethod(
|
||||||
|
"dan200.computer.core.Computer", "createAPIs", "()V",
|
||||||
|
BasicRemapper.builder()
|
||||||
|
.remapType("dan200/computer/core/apis/FSAPI", "dan200/computercraft/core/apis/FSAPI")
|
||||||
|
.remapType("dan200/computer/core/apis/OSAPI", "dan200/computercraft/core/apis/OSAPI")
|
||||||
|
.remapType("dan200/computer/core/apis/TermAPI", "dan200/computercraft/core/apis/TermAPI")
|
||||||
|
.build().toMethodTransform()
|
||||||
|
)
|
||||||
|
// Replace the monitor peripheral
|
||||||
|
.atClass("dan200.computer.shared.TileEntityMonitor", new ClassMerger(
|
||||||
|
"dan200.computer.shared.TileEntityMonitor",
|
||||||
|
"cc.tweaked.patch.mixins.TileEntityMonitorMixin"
|
||||||
|
))
|
||||||
|
// Load from our ROM instead of the CC one. We do this by:
|
||||||
|
// 1. Changing the path of the assets folder.
|
||||||
|
.atMethod(
|
||||||
|
"dan200.computer.core.Computer", "initLua", "()V",
|
||||||
|
ReplaceConstant.replace("/lua/bios.lua", "/assets/cctweaked/lua/bios.lua")
|
||||||
|
)
|
||||||
|
.atMethod(
|
||||||
|
"dan200.computer.core.FileSystem", "romMount", "(Ljava/lang/String;Ljava/io/File;)V",
|
||||||
|
ReplaceConstant.replace("lua/rom/", "assets/cctweaked/lua/rom/")
|
||||||
|
)
|
||||||
|
// 2. Reading the assets from the CC:T jar instead of the CC one.
|
||||||
|
.atMethod("dan200.computer.shared.NetworkedComputerHelper", "getLoadingJar", "()Ljava/io/File;", mv -> new MethodVisitor(Opcodes.ASM4, mv) {
|
||||||
|
@Override
|
||||||
|
public void visitCode() {
|
||||||
|
super.visitCode();
|
||||||
|
|
||||||
|
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "cc/tweaked/CCTweaked", "getLoadingJar", "()Ljava/io/File;");
|
||||||
|
mv.visitInsn(Opcodes.ARETURN);
|
||||||
|
mv.visitMaxs(1, 1);
|
||||||
|
mv.visitEnd();
|
||||||
|
|
||||||
|
mv = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Ensure we render non-advanced terminals as greyscale
|
||||||
|
.atClass("dan200.computer.client.GuiTerminal", new RedirectDrawString(mv -> {
|
||||||
|
mv.visitVarInsn(ALOAD, 0);
|
||||||
|
mv.visitFieldInsn(GETFIELD, "dan200/computer/client/GuiTerminal", "m_terminal", "Ldan200/computer/shared/IComputerEntity;");
|
||||||
|
}))
|
||||||
|
.atClass("dan200.computer.client.TileEntityMonitorRenderer", new RedirectDrawString(mv -> {
|
||||||
|
mv.visitVarInsn(ALOAD, 1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
{
|
||||||
|
Transform<ClassVisitor> fwfr = BasicRemapper.remapType("dan200/computer/client/FixedWidthFontRenderer", "dan200/computercraft/client/FixedWidthFontRenderer").toClassTransform();
|
||||||
|
for (String klass : new String[]{
|
||||||
|
"dan200.computer.client.ComputerCraftProxyClient",
|
||||||
|
"dan200.computer.client.GuiPrintout",
|
||||||
|
"dan200.computer.client.GuiTerminal",
|
||||||
|
"dan200.computer.client.TileEntityMonitorRenderer",
|
||||||
|
"dan200.computer.server.ComputerCraftProxyServer",
|
||||||
|
"dan200.computer.shared.ComputerCraftProxyCommon",
|
||||||
|
"dan200.computer.shared.IComputerCraftProxy",
|
||||||
|
"dan200.ComputerCraft",
|
||||||
|
}) {
|
||||||
|
chain.atClass(klass, fwfr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] transform(String name, byte[] contents) {
|
||||||
|
return name.startsWith("dan200") ? chain.transform(name, contents) : contents;
|
||||||
|
}
|
||||||
|
}
|
48
src/main/java/cc/tweaked/patch/CorePlugin.java
Normal file
48
src/main/java/cc/tweaked/patch/CorePlugin.java
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch;
|
||||||
|
|
||||||
|
import cpw.mods.fml.relauncher.FMLRelaunchLog;
|
||||||
|
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
@IFMLLoadingPlugin.TransformerExclusions({
|
||||||
|
"cc.tweaked.patch."
|
||||||
|
})
|
||||||
|
public class CorePlugin implements IFMLLoadingPlugin {
|
||||||
|
public static final Logger LOG = Logger.getLogger("CC: Tweaked (Core)");
|
||||||
|
|
||||||
|
static {
|
||||||
|
LOG.setLevel(Level.ALL);
|
||||||
|
LOG.setParent(FMLRelaunchLog.log.getLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getLibraryRequestClass() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getASMTransformerClass() {
|
||||||
|
return new String[]{ "cc.tweaked.patch.ClassTransformer" };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getModContainerClass() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSetupClass() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectData(Map<String, Object> map) {
|
||||||
|
}
|
||||||
|
}
|
53
src/main/java/cc/tweaked/patch/RedirectDrawString.java
Normal file
53
src/main/java/cc/tweaked/patch/RedirectDrawString.java
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch;
|
||||||
|
|
||||||
|
import cc.tweaked.patch.framework.transform.Transform;
|
||||||
|
import dan200.computercraft.client.FixedWidthFontRenderer;
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.objectweb.asm.Opcodes.ASM4;
|
||||||
|
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects a call from {@link FixedWidthFontRenderer#drawString(String, int, int, String, int)} to
|
||||||
|
* {@link FixedWidthFontRenderer#drawStringIsColour(String, int, int, String, int, boolean)}, pulling the colour from
|
||||||
|
* the environment.
|
||||||
|
*
|
||||||
|
* @see dan200.computer.client.GuiComputer
|
||||||
|
* @see dan200.computer.client.TileEntityMonitorRenderer
|
||||||
|
*/
|
||||||
|
class RedirectDrawString implements Transform<ClassVisitor> {
|
||||||
|
private final Consumer<MethodVisitor> getTerminal;
|
||||||
|
|
||||||
|
RedirectDrawString(Consumer<MethodVisitor> getTerminal) {
|
||||||
|
this.getTerminal = getTerminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassVisitor chain(ClassVisitor visitor) {
|
||||||
|
return new ClassVisitor(ASM4, visitor) {
|
||||||
|
@Override
|
||||||
|
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
|
||||||
|
return new MethodVisitor(ASM4, super.visitMethod(access, name, desc, signature, exceptions)) {
|
||||||
|
@Override
|
||||||
|
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
|
||||||
|
if (!owner.endsWith("FixedWidthFontRenderer") || !name.endsWith("drawString") || !desc.equals("(Ljava/lang/String;IILjava/lang/String;I)V")) {
|
||||||
|
super.visitMethodInsn(opcode, owner, name, desc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTerminal.accept(mv);
|
||||||
|
mv.visitMethodInsn(INVOKEINTERFACE, "dan200/computer/shared/ITerminalEntity", "isColour", "()Z");
|
||||||
|
mv.visitMethodInsn(opcode, owner, "drawStringIsColour", "(Ljava/lang/String;IILjava/lang/String;IZ)V");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
109
src/main/java/cc/tweaked/patch/framework/AnnotationHelper.java
Normal file
109
src/main/java/cc/tweaked/patch/framework/AnnotationHelper.java
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework;
|
||||||
|
|
||||||
|
import org.objectweb.asm.tree.AnnotationNode;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.objectweb.asm.tree.FieldNode;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers for reading annotations from nodes
|
||||||
|
*/
|
||||||
|
public class AnnotationHelper {
|
||||||
|
/**
|
||||||
|
* We use a field called {@code ANNOTATION} to store data about the class itself
|
||||||
|
*/
|
||||||
|
public final static String ANNOTATION = "ANNOTATION";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the annotation for a list of nodes
|
||||||
|
*
|
||||||
|
* @param nodes List of annotation nodes to search
|
||||||
|
* @param name Name of the annotation to find
|
||||||
|
* @return Map of name to value annotations
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> getAnnotation(List<AnnotationNode> nodes, String name) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (AnnotationNode node : nodes) {
|
||||||
|
if (node.desc.equals(name)) {
|
||||||
|
Map<String, Object> result = new HashMap<String, Object>();
|
||||||
|
if (node.values != null && node.values.size() > 0) {
|
||||||
|
for (int i = 0; i < node.values.size(); i += 2) {
|
||||||
|
result.put((String) node.values.get(i), node.values.get(i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the annotation for a class node
|
||||||
|
* If will also search in the ANNOTATION field
|
||||||
|
*
|
||||||
|
* @param node The class node
|
||||||
|
* @param name The name of the annotation
|
||||||
|
* @return Map of name to value annotations
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> getAnnotation(ClassNode node, String name) {
|
||||||
|
Map<String, Object> annotation = getAnnotation(node.invisibleAnnotations, name);
|
||||||
|
if (annotation != null) return annotation;
|
||||||
|
|
||||||
|
for (FieldNode field : (List<FieldNode>) node.fields) {
|
||||||
|
if (field.name.equals(ANNOTATION)) {
|
||||||
|
return getAnnotation(field.invisibleAnnotations, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a annotation is in a list of annotations
|
||||||
|
*
|
||||||
|
* @param nodes The list of annotations
|
||||||
|
* @param name The name of the annotation
|
||||||
|
* @return If this item has the annotation
|
||||||
|
*/
|
||||||
|
public static boolean hasAnnotation(List<AnnotationNode> nodes, String name) {
|
||||||
|
return getAnnotation(nodes, name) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a class node has a specific annotation.
|
||||||
|
* If will also search in the ANNOTATION field
|
||||||
|
*
|
||||||
|
* @param node The class node
|
||||||
|
* @param name The name of the annotation
|
||||||
|
* @return If this class has the annotation
|
||||||
|
*/
|
||||||
|
public static boolean hasAnnotation(ClassNode node, String name) {
|
||||||
|
return getAnnotation(node, name) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of a annotation
|
||||||
|
*
|
||||||
|
* @param annotation The annotation data
|
||||||
|
* @param key Key of the value to get
|
||||||
|
* @param <T> Type of the return value
|
||||||
|
* @return The found value or null.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T getAnnotationValue(Map<String, Object> annotation, String key) {
|
||||||
|
if (annotation == null) return null;
|
||||||
|
|
||||||
|
Object value = annotation.get(key);
|
||||||
|
if (value == null) return null;
|
||||||
|
|
||||||
|
return (T) value;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework;
|
||||||
|
|
||||||
|
import cc.tweaked.patch.framework.transform.Transform;
|
||||||
|
import org.objectweb.asm.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main entrypoint to the transformer interface. This allows registering {@linkplain Transform transforms} over a
|
||||||
|
* specific class or method, and then handles applying those rewrites.
|
||||||
|
*/
|
||||||
|
public class TransformationChain {
|
||||||
|
private final Map<String, ClassTransformer> transforms = new HashMap<>();
|
||||||
|
|
||||||
|
public TransformationChain atClass(String name, Transform<ClassVisitor> transform) {
|
||||||
|
transforms.computeIfAbsent(name, x -> new ClassTransformer()).transforms.add(transform);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformationChain atMethod(String owner, String name, String desc, Transform<MethodVisitor> transform) {
|
||||||
|
transforms.computeIfAbsent(owner, x -> new ClassTransformer())
|
||||||
|
.methodTransforms.computeIfAbsent(new MethodDesc(name, desc), x -> new ArrayList<>())
|
||||||
|
.add(transform);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] transform(String className, byte[] input) {
|
||||||
|
ClassTransformer transform = transforms.get(className);
|
||||||
|
if (transform == null) return input;
|
||||||
|
|
||||||
|
ClassReader reader = new ClassReader(input);
|
||||||
|
ClassWriter writer = new ClassWriter(0);
|
||||||
|
reader.accept(transform.transform(writer), 0);
|
||||||
|
return writer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MethodDesc {
|
||||||
|
final String name;
|
||||||
|
final String desc;
|
||||||
|
|
||||||
|
private MethodDesc(String name, String desc) {
|
||||||
|
this.name = name;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
MethodDesc that = (MethodDesc) o;
|
||||||
|
|
||||||
|
if (!name.equals(that.name)) return false;
|
||||||
|
return desc.equals(that.desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = name.hashCode();
|
||||||
|
result = 31 * result + desc.hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClassTransformer {
|
||||||
|
final Map<MethodDesc, List<Transform<MethodVisitor>>> methodTransforms = new HashMap<>();
|
||||||
|
final List<Transform<ClassVisitor>> transforms = new ArrayList<>();
|
||||||
|
|
||||||
|
ClassVisitor transform(ClassVisitor visitor) {
|
||||||
|
for (Transform<ClassVisitor> transform : transforms) visitor = transform.chain(visitor);
|
||||||
|
if (!methodTransforms.isEmpty()) visitor = new MethodTransformClassVisitor(visitor, methodTransforms);
|
||||||
|
|
||||||
|
return visitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MethodTransformClassVisitor extends ClassVisitor {
|
||||||
|
private final Map<MethodDesc, List<Transform<MethodVisitor>>> methodTransforms;
|
||||||
|
|
||||||
|
MethodTransformClassVisitor(ClassVisitor visitor, Map<MethodDesc, List<Transform<MethodVisitor>>> methodTransforms) {
|
||||||
|
super(Opcodes.ASM4, visitor);
|
||||||
|
this.methodTransforms = methodTransforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodVisitor visitMethod(int opcode, String name, String desc, String signature, String[] exceptions) {
|
||||||
|
MethodVisitor visitor = super.visitMethod(opcode, name, desc, signature, exceptions);
|
||||||
|
|
||||||
|
List<Transform<MethodVisitor>> transforms = methodTransforms.get(new MethodDesc(name, desc));
|
||||||
|
if (transforms != null) {
|
||||||
|
for (Transform<MethodVisitor> transform : transforms) visitor = transform.chain(visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return visitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.commons.Remapper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic {@link Remapper} implementation which just applies a constant mapping.
|
||||||
|
*
|
||||||
|
* @see org.objectweb.asm.commons.SimpleRemapper
|
||||||
|
*/
|
||||||
|
public class BasicRemapper extends Remapper {
|
||||||
|
private final Map<String, String> types;
|
||||||
|
|
||||||
|
private BasicRemapper(Map<String, String> types) {
|
||||||
|
this.types = types;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BasicRemapper remapType(String from, String to) {
|
||||||
|
return new BasicRemapper(Collections.singletonMap(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String map(String typeName) {
|
||||||
|
return types.getOrDefault(typeName, typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transform<MethodVisitor> toMethodTransform() {
|
||||||
|
return mv -> new RemapMethod(mv, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transform<ClassVisitor> toClassTransform() {
|
||||||
|
return cw -> new RemapClass(cw, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Remap[" + types + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final Map<String, String> types = new HashMap<>();
|
||||||
|
|
||||||
|
public Builder remapType(String from, String to) {
|
||||||
|
types.put(from, to);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BasicRemapper build() {
|
||||||
|
return new BasicRemapper(types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces parts of the class with a mixin-like class
|
||||||
|
*/
|
||||||
|
public class ClassMerger implements Transform<ClassVisitor> {
|
||||||
|
private final String className;
|
||||||
|
private final String mixinName;
|
||||||
|
|
||||||
|
public ClassMerger(String className, String mixinName) {
|
||||||
|
this.className = className.replace('.', '/');
|
||||||
|
this.mixinName = mixinName.replace('.', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassVisitor chain(ClassVisitor delegate) {
|
||||||
|
ClassReader reader;
|
||||||
|
try (InputStream stream = ClassMerger.class.getResourceAsStream("/" + mixinName + ".class")) {
|
||||||
|
if (stream == null) throw new IllegalArgumentException("Failed to find " + mixinName);
|
||||||
|
reader = new ClassReader(stream);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Failed to read " + mixinName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MergeVisitor(delegate, reader, BasicRemapper.remapType(mixinName, className));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import cc.tweaked.patch.framework.AnnotationHelper;
|
||||||
|
import org.objectweb.asm.*;
|
||||||
|
import org.objectweb.asm.commons.Remapper;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.objectweb.asm.tree.FieldNode;
|
||||||
|
import org.objectweb.asm.tree.MethodNode;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge two classes together.
|
||||||
|
*/
|
||||||
|
public class MergeVisitor extends ClassVisitor {
|
||||||
|
private final static String SHADOW = Type.getDescriptor(Shadow.class);
|
||||||
|
private final static String REWRITE = Type.getDescriptor(Rewrite.class);
|
||||||
|
|
||||||
|
private final ClassNode node;
|
||||||
|
|
||||||
|
private final Set<String> visited = new HashSet<>();
|
||||||
|
private final Remapper remapper;
|
||||||
|
|
||||||
|
private boolean writingOverride = false;
|
||||||
|
private String superClass = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge two classes together.
|
||||||
|
*
|
||||||
|
* @param cv The visitor to write to
|
||||||
|
* @param node The node that holds override methods
|
||||||
|
* @param remapper Mapper for override classes to new ones
|
||||||
|
*/
|
||||||
|
public MergeVisitor(ClassVisitor cv, ClassNode node, Remapper remapper) {
|
||||||
|
super(Opcodes.ASM4);
|
||||||
|
this.cv = new RemapClass(cv, remapper);
|
||||||
|
this.node = node;
|
||||||
|
this.remapper = remapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge two classes together.
|
||||||
|
*
|
||||||
|
* @param cv The visitor to write to
|
||||||
|
* @param node The class reader that holds override properties
|
||||||
|
* @param remapper Mapper for override classes to new ones
|
||||||
|
*/
|
||||||
|
public MergeVisitor(ClassVisitor cv, ClassReader node, Remapper remapper) {
|
||||||
|
this(cv, makeNode(node), remapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to make a {@link ClassNode}
|
||||||
|
*
|
||||||
|
* @param reader The class reader to make a node
|
||||||
|
* @return The created node
|
||||||
|
*/
|
||||||
|
private static ClassNode makeNode(ClassReader reader) {
|
||||||
|
ClassNode node = new ClassNode();
|
||||||
|
reader.accept(node, ClassReader.EXPAND_FRAMES);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||||
|
if (AnnotationHelper.hasAnnotation(node, SHADOW)) {
|
||||||
|
// If we are a stub, visit normally
|
||||||
|
super.visit(version, access, name, signature, superName, interfaces);
|
||||||
|
} else if (AnnotationHelper.hasAnnotation(node, REWRITE)) {
|
||||||
|
// If we are a total rewrite, then visit the overriding class
|
||||||
|
node.accept(cv);
|
||||||
|
|
||||||
|
// And prevent writing the normal one
|
||||||
|
cv = null;
|
||||||
|
} else {
|
||||||
|
// Merge both interfaces
|
||||||
|
Set<String> overrideInterfaces = new HashSet<>();
|
||||||
|
for (String inter : (List<String>) node.interfaces) {
|
||||||
|
overrideInterfaces.add(remapper.mapType(inter));
|
||||||
|
}
|
||||||
|
Collections.addAll(overrideInterfaces, interfaces);
|
||||||
|
|
||||||
|
writingOverride = true;
|
||||||
|
superClass = superName;
|
||||||
|
|
||||||
|
super.visit(node.version, access, name, node.signature, superName, overrideInterfaces.toArray(new String[0]));
|
||||||
|
|
||||||
|
// Visit fields
|
||||||
|
for (FieldNode field : (List<FieldNode>) node.fields) {
|
||||||
|
if (!AnnotationHelper.hasAnnotation(field.invisibleAnnotations, SHADOW)) field.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit methods
|
||||||
|
for (MethodNode method : (List<MethodNode>) node.methods) {
|
||||||
|
if (!method.name.equals("<init>") && !method.name.equals("<clinit>")) {
|
||||||
|
if (!AnnotationHelper.hasAnnotation(method.invisibleAnnotations, SHADOW)) method.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writingOverride = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
|
||||||
|
if (!visited.add(name)) return null;
|
||||||
|
return super.visitField(access, name, desc, signature, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
|
||||||
|
String description = "(" + remapper.mapMethodDesc(desc) + ")";
|
||||||
|
|
||||||
|
if (!visited.add(name + description)) return null;
|
||||||
|
|
||||||
|
MethodVisitor visitor = super.visitMethod(access, name, desc, signature, exceptions);
|
||||||
|
|
||||||
|
// We remap super methods if the method is not static and we are writing the override methods
|
||||||
|
return visitor != null && !Modifier.isStatic(access) && writingOverride && superClass != null
|
||||||
|
? new SuperMethodVisitor(api, visitor)
|
||||||
|
: visitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor that remaps super calls
|
||||||
|
*/
|
||||||
|
public class SuperMethodVisitor extends MethodVisitor {
|
||||||
|
public SuperMethodVisitor(int api, MethodVisitor mv) {
|
||||||
|
super(api, mv);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
|
||||||
|
// If it is a constructor, or it is in the current class (private method)
|
||||||
|
// we shouldn't remap to the base class
|
||||||
|
// Reference: http://stackoverflow.com/questions/20382652/detect-super-word-in-java-code-using-bytecode
|
||||||
|
if (opcode == INVOKESPECIAL && !name.equals("<init>") && owner.equals(node.superName)) {
|
||||||
|
owner = superClass;
|
||||||
|
}
|
||||||
|
super.visitMethodInsn(opcode, owner, name, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this node as a stub, it will not be injected into the target class.
|
||||||
|
*/
|
||||||
|
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.CONSTRUCTOR })
|
||||||
|
@Retention(RetentionPolicy.CLASS)
|
||||||
|
public @interface Shadow {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrite the original class instead of merging
|
||||||
|
*/
|
||||||
|
@Target({ ElementType.TYPE, ElementType.FIELD })
|
||||||
|
@Retention(RetentionPolicy.CLASS)
|
||||||
|
public @interface Rewrite {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import org.objectweb.asm.*;
|
||||||
|
import org.objectweb.asm.commons.Remapper;
|
||||||
|
import org.objectweb.asm.commons.RemappingClassAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alternative to {@link RemappingClassAdapter}, which doesn't require {@link ClassReader#EXPAND_FRAMES}.
|
||||||
|
*/
|
||||||
|
public class RemapClass extends ClassVisitor {
|
||||||
|
private final Remapper remapper;
|
||||||
|
|
||||||
|
public RemapClass(ClassVisitor classVisitor, Remapper remapper) {
|
||||||
|
super(Opcodes.ASM4, classVisitor);
|
||||||
|
this.remapper = remapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
|
||||||
|
return super.visitField(access, name, remapper.mapDesc(desc), signature, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
|
||||||
|
return new RemapMethod(super.visitMethod(access, name, remapper.mapMethodDesc(desc), signature, exceptions), remapper);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.commons.Remapper;
|
||||||
|
import org.objectweb.asm.commons.RemappingMethodAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alternative to {@link RemappingMethodAdapter}, which doesn't require {@link ClassReader#EXPAND_FRAMES}.
|
||||||
|
*/
|
||||||
|
public class RemapMethod extends MethodVisitor {
|
||||||
|
private final Remapper remapper;
|
||||||
|
|
||||||
|
public RemapMethod(MethodVisitor methodVisitor, Remapper remapper) {
|
||||||
|
super(Opcodes.ASM4, methodVisitor);
|
||||||
|
this.remapper = remapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
|
||||||
|
super.visitMethodInsn(opcode, remapper.mapType(owner), name, remapper.mapMethodDesc(desc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
|
||||||
|
super.visitFieldInsn(opcode, remapper.mapType(owner), name, remapper.mapDesc(desc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitTypeInsn(int opcode, String type) {
|
||||||
|
super.visitTypeInsn(opcode, remapper.mapType(type));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import cc.tweaked.patch.CorePlugin;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link MethodVisitor} {@linkplain Transform transformer} which rewrites constants
|
||||||
|
*/
|
||||||
|
public final class ReplaceConstant implements Transform<MethodVisitor> {
|
||||||
|
private final Map<Object, Object> replace;
|
||||||
|
|
||||||
|
private ReplaceConstant(Map<Object, Object> replace) {
|
||||||
|
this.replace = replace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transform<MethodVisitor> replace(Object from, Object to) {
|
||||||
|
return new ReplaceConstant(Collections.singletonMap(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodVisitor chain(MethodVisitor visitor) {
|
||||||
|
return new MethodVisitor(Opcodes.ASM4, visitor) {
|
||||||
|
private final Set<Object> unmatched = new HashSet<>(replace.keySet());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitLdcInsn(Object cst) {
|
||||||
|
Object replacement = replace.get(cst);
|
||||||
|
if (replacement == null) {
|
||||||
|
super.visitLdcInsn(cst);
|
||||||
|
} else {
|
||||||
|
unmatched.remove(cst);
|
||||||
|
super.visitLdcInsn(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
super.visitEnd();
|
||||||
|
|
||||||
|
if (!unmatched.isEmpty()) {
|
||||||
|
CorePlugin.LOG.warning("Failed to replace the following constants " + unmatched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package cc.tweaked.patch.framework.transform;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transformation over a method or class.
|
||||||
|
*
|
||||||
|
* @param <V> The type of visitor, either {@link ClassVisitor} or {@link MethodVisitor}.
|
||||||
|
*/
|
||||||
|
public interface Transform<V> {
|
||||||
|
/**
|
||||||
|
* Apply this transformation, chaining together a target {@linkplain V visitor} with your visitor.
|
||||||
|
*
|
||||||
|
* @param visitor The visitor to wrap.
|
||||||
|
* @return The wrapped visitor.
|
||||||
|
*/
|
||||||
|
V chain(V visitor);
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package cc.tweaked.patch.mixins;
|
||||||
|
|
||||||
|
import cc.tweaked.patch.framework.transform.MergeVisitor;
|
||||||
|
import dan200.computer.api.IComputerAccess;
|
||||||
|
import dan200.computer.api.IPeripheral;
|
||||||
|
import dan200.computer.core.Terminal;
|
||||||
|
import dan200.computer.shared.NetworkedTerminalHelper;
|
||||||
|
import dan200.computer.shared.TileEntityMonitor;
|
||||||
|
import dan200.computercraft.api.lua.ObjectArguments;
|
||||||
|
import dan200.computercraft.core.asm.LuaMethod;
|
||||||
|
import dan200.computercraft.core.asm.Methods;
|
||||||
|
import dan200.computercraft.core.asm.NamedMethod;
|
||||||
|
import dan200.computercraft.shared.peripheral.monitor.MonitorPeripheral;
|
||||||
|
import dan200.computercraft.shared.peripheral.monitor.TileEntityMonitorAccessor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the {@link TileEntityMonitor} peripheral with {@link MonitorPeripheral}.
|
||||||
|
*/
|
||||||
|
public abstract class TileEntityMonitorMixin implements TileEntityMonitorAccessor, IPeripheral {
|
||||||
|
private @MergeVisitor.Shadow int m_textScale;
|
||||||
|
private @MergeVisitor.Shadow NetworkedTerminalHelper m_terminal;
|
||||||
|
private @MergeVisitor.Shadow boolean m_changed;
|
||||||
|
|
||||||
|
private List<NamedMethod<LuaMethod>> methods;
|
||||||
|
private MonitorPeripheral peripheral;
|
||||||
|
|
||||||
|
public String[] getMethodNames() {
|
||||||
|
// Overwrite the original getMethodNames, instead deferring to our peripheral.
|
||||||
|
if (methods == null) {
|
||||||
|
peripheral = new MonitorPeripheral(this);
|
||||||
|
methods = Methods.LUA_METHOD.getMethods(peripheral.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] names = new String[methods.size()];
|
||||||
|
for (int i = 0; i < methods.size(); i++) names[i] = methods.get(i).getName();
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object[] callMethod(IComputerAccess computer, int method, Object[] arguments) throws Exception {
|
||||||
|
return methods.get(method).getMethod().apply(peripheral, new ObjectArguments(arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Terminal cct$getOriginTerminal() {
|
||||||
|
return origin().getTerminal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cct$setTextScale(int scale) {
|
||||||
|
TileEntityMonitorMixin origin = origin();
|
||||||
|
synchronized (origin.m_terminal) {
|
||||||
|
if (origin.m_textScale != scale) {
|
||||||
|
origin.m_textScale = scale;
|
||||||
|
origin.rebuildTerminal(null);
|
||||||
|
origin.m_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int cct$getTextScale() {
|
||||||
|
return origin().m_textScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@MergeVisitor.Shadow
|
||||||
|
private TileEntityMonitorMixin origin() {
|
||||||
|
throw new AssertionError("Stub method");
|
||||||
|
}
|
||||||
|
|
||||||
|
@MergeVisitor.Shadow
|
||||||
|
public abstract Terminal getTerminal();
|
||||||
|
|
||||||
|
@MergeVisitor.Shadow
|
||||||
|
private void rebuildTerminal(Terminal copyFrom) {
|
||||||
|
throw new AssertionError("Stub method");
|
||||||
|
}
|
||||||
|
}
|
331
src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java
Normal file
331
src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
import dan200.computer.api.IComputerAccess;
|
||||||
|
import dan200.computer.api.IPeripheral;
|
||||||
|
import dan200.computer.core.ILuaObject;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods for extracting values and validating Lua arguments, such as those provided to
|
||||||
|
* {@link ILuaObject#callMethod(int, Object[])} or
|
||||||
|
* {@link IPeripheral#callMethod(IComputerAccess, int, Object[])}.
|
||||||
|
* <p>
|
||||||
|
* This provides two sets of functions: the {@code get*} methods, which require an argument to be valid, and
|
||||||
|
* {@code opt*}, which accept a default value and return that if the argument was not present or was {@code null}.
|
||||||
|
* If the argument is of the wrong type, a suitable error message will be thrown, with a similar format to Lua's own
|
||||||
|
* error messages.
|
||||||
|
*
|
||||||
|
* <h2>Example usage:</h2>
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* int slot = getInt( args, 0 );
|
||||||
|
* int amount = optInt( args, 1, 64 );
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public final class ArgumentHelper {
|
||||||
|
private ArgumentHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string representation of the given value's type.
|
||||||
|
*
|
||||||
|
* @param value The value whose type we are trying to compute.
|
||||||
|
* @return A string representation of the given value's type, in a similar format to that provided by Lua's
|
||||||
|
* {@code type} function.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String getType(Object value) {
|
||||||
|
if (value == null) return "nil";
|
||||||
|
if (value instanceof String) return "string";
|
||||||
|
if (value instanceof Boolean) return "boolean";
|
||||||
|
if (value instanceof Number) return "number";
|
||||||
|
if (value instanceof Map) return "table";
|
||||||
|
return "userdata";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a "bad argument" exception, from an expected type and the actual value provided.
|
||||||
|
*
|
||||||
|
* @param index The argument number, starting from 0.
|
||||||
|
* @param expected The expected type for this argument.
|
||||||
|
* @param actual The actual value provided for this argument.
|
||||||
|
* @return The constructed exception, which should be thrown immediately.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static LuaException badArgumentOf(int index, String expected, Object actual) {
|
||||||
|
return badArgument(index, expected, getType(actual));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a "bad argument" exception, from an expected and actual type.
|
||||||
|
*
|
||||||
|
* @param index The argument number, starting from 0.
|
||||||
|
* @param expected The expected type for this argument.
|
||||||
|
* @param actual The provided type for this argument.
|
||||||
|
* @return The constructed exception, which should be thrown immediately.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static LuaException badArgument(int index, String expected, String actual) {
|
||||||
|
return new LuaException("bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a double.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
* @see #getFiniteDouble(Object[], int) if you require this to be finite (i.e. not infinite or NaN).
|
||||||
|
*/
|
||||||
|
public static double getDouble(Object[] args, int index) throws LuaException {
|
||||||
|
if (index >= args.length) throw badArgument(index, "number", "nil");
|
||||||
|
Object value = args[index];
|
||||||
|
if (!(value instanceof Number)) throw badArgumentOf(index, "number", value);
|
||||||
|
return ((Number) value).doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as an integer.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not an integer.
|
||||||
|
*/
|
||||||
|
public static int getInt(Object[] args, int index) throws LuaException {
|
||||||
|
return (int) getLong(args, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a long.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a long.
|
||||||
|
*/
|
||||||
|
public static long getLong(Object[] args, int index) throws LuaException {
|
||||||
|
if (index >= args.length) throw badArgument(index, "number", "nil");
|
||||||
|
Object value = args[index];
|
||||||
|
if (!(value instanceof Number)) throw badArgumentOf(index, "number", value);
|
||||||
|
return checkFinite(index, (Number) value).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a finite number (not infinite or NaN).
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not finite.
|
||||||
|
*/
|
||||||
|
public static double getFiniteDouble(Object[] args, int index) throws LuaException {
|
||||||
|
return checkFinite(index, getDouble(args, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a boolean.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a boolean.
|
||||||
|
*/
|
||||||
|
public static boolean getBoolean(Object[] args, int index) throws LuaException {
|
||||||
|
if (index >= args.length) throw badArgument(index, "boolean", "nil");
|
||||||
|
Object value = args[index];
|
||||||
|
if (!(value instanceof Boolean)) throw badArgumentOf(index, "boolean", value);
|
||||||
|
return (Boolean) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a string.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String getString(Object[] args, int index) throws LuaException {
|
||||||
|
if (index >= args.length) throw badArgument(index, "string", "nil");
|
||||||
|
Object value = args[index];
|
||||||
|
if (!(value instanceof String)) throw badArgumentOf(index, "string", value);
|
||||||
|
return (String) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a string.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String getStringCoerced(Object[] args, int index) {
|
||||||
|
Object value = index >= args.length ? null : args[index];
|
||||||
|
if (value == null) return "nil";
|
||||||
|
if (value instanceof Boolean || value instanceof String) return value.toString();
|
||||||
|
if (value instanceof Number) {
|
||||||
|
double asDouble = ((Number) value).doubleValue();
|
||||||
|
int asInt = (int) asDouble;
|
||||||
|
return asInt == asDouble ? Integer.toString(asInt) : Double.toString(asDouble);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is somewhat bogus - the hash codes don't match up - but it's a good approximation.
|
||||||
|
return String.format("%s: %08x", getType(index), value.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a table.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a table.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static Map<?, ?> getTable(Object[] args, int index) throws LuaException {
|
||||||
|
if (index >= args.length) throw badArgument(index, "table", "nil");
|
||||||
|
Object value = args[index];
|
||||||
|
if (!(value instanceof Map)) throw badArgumentOf(index, "table", value);
|
||||||
|
return (Map<?, ?>) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a double.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public static double optDouble(Object[] args, int index, double def) throws LuaException {
|
||||||
|
Object value = index < args.length ? args[index] : null;
|
||||||
|
if (value == null) return def;
|
||||||
|
if (!(value instanceof Number)) throw badArgumentOf(index, "number", value);
|
||||||
|
return ((Number) value).doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as an int.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public static int optInt(Object[] args, int index, int def) throws LuaException {
|
||||||
|
return (int) optLong(args, index, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a long.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public static long optLong(Object[] args, int index, long def) throws LuaException {
|
||||||
|
Object value = index < args.length ? args[index] : null;
|
||||||
|
if (value == null) return def;
|
||||||
|
if (!(value instanceof Number)) throw badArgumentOf(index, "number", value);
|
||||||
|
return checkFinite(index, (Number) value).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a finite number (not infinite or NaN).
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not finite.
|
||||||
|
*/
|
||||||
|
public static double optFiniteDouble(Object[] args, int index, double def) throws LuaException {
|
||||||
|
return checkFinite(index, optDouble(args, index, def));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a boolean.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a boolean.
|
||||||
|
*/
|
||||||
|
public static boolean optBoolean(Object[] args, int index, boolean def) throws LuaException {
|
||||||
|
Object value = index < args.length ? args[index] : null;
|
||||||
|
if (value == null) return def;
|
||||||
|
if (!(value instanceof Boolean)) throw badArgumentOf(index, "boolean", value);
|
||||||
|
return (Boolean) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a string.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
public static String optString(Object[] args, int index, String def) throws LuaException {
|
||||||
|
Object value = index < args.length ? args[index] : null;
|
||||||
|
if (value == null) return def;
|
||||||
|
if (!(value instanceof String)) throw badArgumentOf(index, "string", value);
|
||||||
|
return (String) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a table.
|
||||||
|
*
|
||||||
|
* @param args The arguments to extract from.
|
||||||
|
* @param index The index into the argument array to read from.
|
||||||
|
* @param def The default value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a table.
|
||||||
|
*/
|
||||||
|
public static Map<?, ?> optTable(Object[] args, int index, Map<Object, Object> def) throws LuaException {
|
||||||
|
Object value = index < args.length ? args[index] : null;
|
||||||
|
if (value == null) return def;
|
||||||
|
if (!(value instanceof Map)) throw badArgumentOf(index, "table", value);
|
||||||
|
return (Map<?, ?>) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Number checkFinite(int index, Number value) throws LuaException {
|
||||||
|
checkFinite(index, value.doubleValue());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double checkFinite(int index, double value) throws LuaException {
|
||||||
|
if (!Double.isFinite(value)) throw badArgument(index, "number", getNumericType(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a more detailed representation of this number's type. If this is finite, it will just return "number",
|
||||||
|
* otherwise it returns whether it is infinite or NaN.
|
||||||
|
*
|
||||||
|
* @param value The value to extract the type for.
|
||||||
|
* @return This value's numeric type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static String getNumericType(double value) {
|
||||||
|
if (Double.isNaN(value)) return "nan";
|
||||||
|
if (value == Double.POSITIVE_INFINITY) return "inf";
|
||||||
|
if (value == Double.NEGATIVE_INFINITY) return "-inf";
|
||||||
|
return "number";
|
||||||
|
}
|
||||||
|
}
|
46
src/main/java/dan200/computercraft/api/lua/Coerced.java
Normal file
46
src/main/java/dan200/computercraft/api/lua/Coerced.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper type for "coerced" values.
|
||||||
|
* <p>
|
||||||
|
* This is designed to be used with {@link LuaFunction} annotated functions, to mark an argument as being coerced to
|
||||||
|
* the given type, rather than requiring an exact type.
|
||||||
|
*
|
||||||
|
* <h2>Example:</h2>
|
||||||
|
* <pre>{@code
|
||||||
|
* @LuaFunction
|
||||||
|
* public final void doSomething(Coerced<String> myString) {
|
||||||
|
* var value = myString.value();
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param <T> The type of the underlying value.
|
||||||
|
* @see IArguments#getStringCoerced(int)
|
||||||
|
*/
|
||||||
|
public final class Coerced<T> {
|
||||||
|
private final T value;
|
||||||
|
|
||||||
|
public Coerced(T value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return obj == this || obj instanceof Coerced && Objects.equals(value, ((Coerced<?>) obj).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(value);
|
||||||
|
}
|
||||||
|
}
|
444
src/main/java/dan200/computercraft/api/lua/IArguments.java
Normal file
444
src/main/java/dan200/computercraft/api/lua/IArguments.java
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The arguments passed to a function.
|
||||||
|
*/
|
||||||
|
public abstract class IArguments {
|
||||||
|
/**
|
||||||
|
* Get the number of arguments passed to this function.
|
||||||
|
*
|
||||||
|
* @return The number of passed arguments.
|
||||||
|
*/
|
||||||
|
public abstract int count();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the argument at the specific index. The returned value must obey the following conversion rules:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Lua values of type "string" will be represented by a {@link String}.</li>
|
||||||
|
* <li>Lua values of type "number" will be represented by a {@link Number}.</li>
|
||||||
|
* <li>Lua values of type "boolean" will be represented by a {@link Boolean}.</li>
|
||||||
|
* <li>Lua values of type "table" will be represented by a {@link Map}.</li>
|
||||||
|
* <li>Lua values of any other type will be represented by a {@code null} value.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@code null} if not present.
|
||||||
|
* @throws LuaException If the argument cannot be converted to Java. This should be thrown in extraneous
|
||||||
|
* circumstances (if the conversion would allocate too much memory) and should
|
||||||
|
* <em>not</em> be thrown if the original argument is not present or is an unsupported
|
||||||
|
* data type (such as a function or userdata).
|
||||||
|
* @throws IllegalStateException If accessing these arguments outside the scope of the original function. See
|
||||||
|
* {@link #escapes()}.
|
||||||
|
*/
|
||||||
|
public abstract Object get(int index) throws LuaException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type name of the argument at the specific index.
|
||||||
|
* <p>
|
||||||
|
* This method is meant to be used in error reporting (namely with {@link LuaValues#badArgumentOf(IArguments, int, String)}),
|
||||||
|
* and should not be used to determine the actual type of an argument.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The name of this type.
|
||||||
|
* @see LuaValues#getType(Object)
|
||||||
|
*/
|
||||||
|
public abstract String getType(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a number of arguments. The returned arguments instance will access arguments at position {@code i + count},
|
||||||
|
* rather than {@code i}. However, errors will still use the given argument index.
|
||||||
|
*
|
||||||
|
* @param count The number of arguments to drop.
|
||||||
|
* @return The new {@link IArguments} instance.
|
||||||
|
*/
|
||||||
|
public abstract IArguments drop(int count);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array containing all as {@link Object}s.
|
||||||
|
*
|
||||||
|
* @return All arguments.
|
||||||
|
* @throws LuaException If an error occurred while fetching an argument.
|
||||||
|
* @see #get(int) To get a single argument.
|
||||||
|
*/
|
||||||
|
public Object[] getAll() throws LuaException {
|
||||||
|
Object[] result = new Object[count()];
|
||||||
|
for (int i = 0; i < result.length; i++) result[i] = get(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a double.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
* @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN).
|
||||||
|
*/
|
||||||
|
public double getDouble(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
|
||||||
|
return ((Number) value).doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as an integer.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not an integer.
|
||||||
|
*/
|
||||||
|
public int getInt(int index) throws LuaException {
|
||||||
|
return (int) getLong(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a long.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a long.
|
||||||
|
*/
|
||||||
|
public long getLong(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
|
||||||
|
return LuaValues.checkFiniteNum(index, (Number) value).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a finite number (not infinite or NaN).
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not finite.
|
||||||
|
*/
|
||||||
|
public double getFiniteDouble(int index) throws LuaException {
|
||||||
|
return LuaValues.checkFinite(index, getDouble(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a boolean.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a boolean.
|
||||||
|
*/
|
||||||
|
public boolean getBoolean(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (!(value instanceof Boolean)) throw LuaValues.badArgumentOf(this, index, "boolean");
|
||||||
|
return (Boolean) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a string.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
public String getString(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (!(value instanceof String)) throw LuaValues.badArgumentOf(this, index, "string");
|
||||||
|
return (String) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the argument, converting it to a string by following Lua conventions.
|
||||||
|
* <p>
|
||||||
|
* Unlike {@code Objects.toString(arguments.get(i))}, this may follow Lua's string formatting, so {@code nil} will be
|
||||||
|
* converted to {@code "nil"} and tables/functions will use their original hash.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's representation as a string.
|
||||||
|
* @throws LuaException If the argument cannot be converted to Java. This should be thrown in extraneous
|
||||||
|
* circumstances (if the conversion would allocate too much memory) and should
|
||||||
|
* <em>not</em> be thrown if the original argument is not present or is an unsupported
|
||||||
|
* data type (such as a function or userdata).
|
||||||
|
* @throws IllegalStateException If accessing these arguments outside the scope of the original function. See
|
||||||
|
* {@link #escapes()}.
|
||||||
|
* @see Coerced
|
||||||
|
*/
|
||||||
|
public String getStringCoerced(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (value == null) return "nil";
|
||||||
|
if (value instanceof Boolean || value instanceof String) return value.toString();
|
||||||
|
if (value instanceof Number) {
|
||||||
|
double asDouble = ((Number) value).doubleValue();
|
||||||
|
int asInt = (int) asDouble;
|
||||||
|
return asInt == asDouble ? Integer.toString(asInt) : Double.toString(asDouble);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is somewhat bogus - the hash codes don't match up - but it's a good approximation.
|
||||||
|
return String.format("%s: %08x", getType(index), value.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string argument as a byte array.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value. This is a <em>read only</em> buffer.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getBytes(int index) throws LuaException {
|
||||||
|
return LuaValues.encode(getString(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string argument as an enum value.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param klass The type of enum to parse.
|
||||||
|
* @param <T> The type of enum to parse.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a string or not a valid option for this enum.
|
||||||
|
*/
|
||||||
|
public <T extends Enum<T>> T getEnum(int index, Class<T> klass) throws LuaException {
|
||||||
|
return LuaValues.checkEnum(index, klass, getString(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a table.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a table.
|
||||||
|
*/
|
||||||
|
public Map<?, ?> getTable(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (!(value instanceof Map)) throw LuaValues.badArgumentOf(this, index, "table");
|
||||||
|
return (Map<?, ?>) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a double.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public Optional<Double> optDouble(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (value == null) return Optional.empty();
|
||||||
|
if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
|
||||||
|
return Optional.of(((Number) value).doubleValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as an int.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public Optional<Integer> optInt(int index) throws LuaException {
|
||||||
|
return optLong(index).map(Long::intValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a long.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public Optional<Long> optLong(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (value == null) return Optional.empty();
|
||||||
|
if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
|
||||||
|
return Optional.of(LuaValues.checkFiniteNum(index, (Number) value).longValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a finite number (not infinite or NaN).
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not finite.
|
||||||
|
*/
|
||||||
|
public Optional<Double> optFiniteDouble(int index) throws LuaException {
|
||||||
|
Optional<Double> value = optDouble(index);
|
||||||
|
if (value.isPresent()) LuaValues.checkFiniteNum(index, value.get());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a boolean.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not a boolean.
|
||||||
|
*/
|
||||||
|
public Optional<Boolean> optBoolean(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (value == null) return Optional.empty();
|
||||||
|
if (!(value instanceof Boolean)) throw LuaValues.badArgumentOf(this, index, "boolean");
|
||||||
|
return Optional.of((Boolean) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a string.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
public Optional<String> optString(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (value == null) return Optional.empty();
|
||||||
|
if (!(value instanceof String)) throw LuaValues.badArgumentOf(this, index, "string");
|
||||||
|
return Optional.of((String) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string argument as a byte array.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present. This is a <em>read only</em> buffer.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
public Optional<ByteBuffer> optBytes(int index) throws LuaException {
|
||||||
|
return optString(index).map(LuaValues::encode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string argument as an enum value.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param klass The type of enum to parse.
|
||||||
|
* @param <T> The type of enum to parse.
|
||||||
|
* @return The argument's value.
|
||||||
|
* @throws LuaException If the value is not a string or not a valid option for this enum.
|
||||||
|
*/
|
||||||
|
public <T extends Enum<T>> Optional<T> optEnum(int index, Class<T> klass) throws LuaException {
|
||||||
|
Optional<String> str = optString(index);
|
||||||
|
return str.isPresent() ? Optional.of(LuaValues.checkEnum(index, klass, str.get())) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a table.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @return The argument's value, or {@link Optional#empty()} if not present.
|
||||||
|
* @throws LuaException If the value is not a table.
|
||||||
|
*/
|
||||||
|
public Optional<Map<?, ?>> optTable(int index) throws LuaException {
|
||||||
|
Object value = get(index);
|
||||||
|
if (value == null) return Optional.empty();
|
||||||
|
if (!(value instanceof Map)) throw LuaValues.badArgumentOf(this, index, "map");
|
||||||
|
return Optional.of((Map<?, ?>) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a double.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public double optDouble(int index, double def) throws LuaException {
|
||||||
|
return optDouble(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as an int.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public int optInt(int index, int def) throws LuaException {
|
||||||
|
return optInt(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a long.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a number.
|
||||||
|
*/
|
||||||
|
public long optLong(int index, long def) throws LuaException {
|
||||||
|
return optLong(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a finite number (not infinite or NaN).
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not finite.
|
||||||
|
*/
|
||||||
|
public double optFiniteDouble(int index, double def) throws LuaException {
|
||||||
|
return optFiniteDouble(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a boolean.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a boolean.
|
||||||
|
*/
|
||||||
|
public boolean optBoolean(int index, boolean def) throws LuaException {
|
||||||
|
return optBoolean(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a string.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a string.
|
||||||
|
*/
|
||||||
|
public String optString(int index, String def) throws LuaException {
|
||||||
|
return optString(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an argument as a table.
|
||||||
|
*
|
||||||
|
* @param index The argument number.
|
||||||
|
* @param def The public value, if this argument is not given.
|
||||||
|
* @return The argument's value, or {@code def} if none was provided.
|
||||||
|
* @throws LuaException If the value is not a table.
|
||||||
|
*/
|
||||||
|
public Map<?, ?> optTable(int index, Map<Object, Object> def) throws LuaException {
|
||||||
|
return optTable(index).orElse(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a version of these arguments which escapes the scope of the current function call.
|
||||||
|
* <p>
|
||||||
|
* Some {@link IArguments} implementations provide a view over the underlying Lua data structures, allowing for
|
||||||
|
* zero-copy implementations of some methods (such as {@link #getTableUnsafe(int)} or {@link #getBytes(int)}).
|
||||||
|
* However, this means the arguments can only be accessed inside the current function call.
|
||||||
|
* <p>
|
||||||
|
* If the arguments escape the scope of the current call (for instance, are later accessed on the main server
|
||||||
|
* thread), then these arguments must be marked as "escaping", which may choose to perform a copy of the underlying
|
||||||
|
* arguments.
|
||||||
|
* <p>
|
||||||
|
* If you are using {@link LuaFunction#mainThread()}, this will be done automatically. However, if you call
|
||||||
|
* {@link ILuaContext#issueMainThreadTask(LuaTask)} (or similar), then you will need to mark arguments as escaping
|
||||||
|
* yourself.
|
||||||
|
*
|
||||||
|
* @return An {@link IArguments} instance which can escape the current scope. May be {@code this}.
|
||||||
|
* @throws LuaException For the same reasons as {@link #get(int)}.
|
||||||
|
*/
|
||||||
|
public IArguments escapes() throws LuaException {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
46
src/main/java/dan200/computercraft/api/lua/LuaException.java
Normal file
46
src/main/java/dan200/computercraft/api/lua/LuaException.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. This API may be redistributed unmodified and in full only.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception representing an error in Lua, like that raised by the {@code error()} function.
|
||||||
|
*/
|
||||||
|
public class LuaException extends Exception {
|
||||||
|
private static final long serialVersionUID = -6136063076818512651L;
|
||||||
|
private final boolean hasLevel;
|
||||||
|
private final int level;
|
||||||
|
|
||||||
|
public LuaException(String message) {
|
||||||
|
super(message);
|
||||||
|
hasLevel = false;
|
||||||
|
level = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LuaException(String message, int level) {
|
||||||
|
super(message);
|
||||||
|
hasLevel = true;
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a level was explicitly specified when constructing. If a level is not provided, the Lua runtime may
|
||||||
|
* attempt to pick the most suitable one.
|
||||||
|
*
|
||||||
|
* @return Whether this has an explicit level.
|
||||||
|
*/
|
||||||
|
public boolean hasLevel() {
|
||||||
|
return hasLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The level this error is raised at. Level 1 is the function's caller, level 2 is that function's caller, and so
|
||||||
|
* on.
|
||||||
|
*
|
||||||
|
* @return The level to raise the error at.
|
||||||
|
*/
|
||||||
|
public int getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
61
src/main/java/dan200/computercraft/api/lua/LuaFunction.java
Normal file
61
src/main/java/dan200/computercraft/api/lua/LuaFunction.java
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to mark a Java function which is callable from Lua.
|
||||||
|
* <p>
|
||||||
|
* Methods annotated with {@link LuaFunction} must be public final instance methods. They can have any number of
|
||||||
|
* parameters, but they must be of the following types:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link IArguments}: The arguments supplied to this function.</li>
|
||||||
|
* <li>
|
||||||
|
* Alternatively, one may specify the desired arguments as normal parameters and the argument parsing code will
|
||||||
|
* be generated automatically.
|
||||||
|
* <p>
|
||||||
|
* Each parameter must be one of the given types supported by {@link IArguments} (for instance, {@link int} or
|
||||||
|
* {@link Map}). Optional values are supported by accepting a parameter of type {@link Optional}.
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* This function may return {@link MethodResult}. However, if you simply return a value (rather than having to yield),
|
||||||
|
* you may return {@code void}, a single value (either an object or a primitive like {@code int}) or array of objects.
|
||||||
|
* These will be treated the same as {@link MethodResult#of()}, {@link MethodResult#of(Object)} and
|
||||||
|
* {@link MethodResult#of(Object...)}.
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
public @interface LuaFunction {
|
||||||
|
/**
|
||||||
|
* Explicitly specify the method names of this function. If not given, it uses the name of the annotated method.
|
||||||
|
*
|
||||||
|
* @return This function's name(s).
|
||||||
|
*/
|
||||||
|
String[] value() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run this function on the main server thread. This should be specified for any method which interacts with
|
||||||
|
* Minecraft in a thread-unsafe manner.
|
||||||
|
*
|
||||||
|
* @return Whether this function should be run on the main thread.
|
||||||
|
* @see ILuaContext#issueMainThreadTask(LuaTask)
|
||||||
|
*/
|
||||||
|
boolean mainThread() default false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow using "unsafe" arguments, such {@link IArguments#getTableUnsafe(int)}.
|
||||||
|
* <p>
|
||||||
|
* This is incompatible with {@link #mainThread()}.
|
||||||
|
*
|
||||||
|
* @return Whether this function supports unsafe arguments.
|
||||||
|
*/
|
||||||
|
boolean unsafe() default false;
|
||||||
|
}
|
156
src/main/java/dan200/computercraft/api/lua/LuaValues.java
Normal file
156
src/main/java/dan200/computercraft/api/lua/LuaValues.java
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Various utility functions for operating with Lua values.
|
||||||
|
*
|
||||||
|
* @see IArguments
|
||||||
|
*/
|
||||||
|
public final class LuaValues {
|
||||||
|
private LuaValues() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a Lua string into a read-only {@link ByteBuffer}.
|
||||||
|
*
|
||||||
|
* @param string The string to encode.
|
||||||
|
* @return The encoded string.
|
||||||
|
*/
|
||||||
|
public static ByteBuffer encode(String string) {
|
||||||
|
byte[] chars = new byte[string.length()];
|
||||||
|
for (int i = 0; i < chars.length; i++) {
|
||||||
|
char c = string.charAt(i);
|
||||||
|
chars[i] = c < 256 ? (byte) c : 63;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ByteBuffer.wrap(chars).asReadOnlyBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a more detailed representation of this number's type. If this is finite, it will just return "number",
|
||||||
|
* otherwise it returns whether it is infinite or NaN.
|
||||||
|
*
|
||||||
|
* @param value The value to extract the type for.
|
||||||
|
* @return This value's numeric type.
|
||||||
|
*/
|
||||||
|
public static String getNumericType(double value) {
|
||||||
|
if (Double.isNaN(value)) return "nan";
|
||||||
|
if (value == Double.POSITIVE_INFINITY) return "inf";
|
||||||
|
if (value == Double.NEGATIVE_INFINITY) return "-inf";
|
||||||
|
return "number";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string representation of the given value's type.
|
||||||
|
*
|
||||||
|
* @param value The value whose type we are trying to compute.
|
||||||
|
* @return A string representation of the given value's type, in a similar format to that provided by Lua's
|
||||||
|
* {@code type} function.
|
||||||
|
*/
|
||||||
|
public static String getType(Object value) {
|
||||||
|
if (value == null) return "nil";
|
||||||
|
if (value instanceof String) return "string";
|
||||||
|
if (value instanceof Boolean) return "boolean";
|
||||||
|
if (value instanceof Number) return "number";
|
||||||
|
if (value instanceof Map) return "table";
|
||||||
|
return "userdata";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a "bad argument" exception, from an {@link IArguments} argument and an expected type.
|
||||||
|
*
|
||||||
|
* @param arguments The current arguments.
|
||||||
|
* @param index The argument number, starting from 0.
|
||||||
|
* @param expected The expected type for this argument.
|
||||||
|
* @return The constructed exception, which should be thrown immediately.
|
||||||
|
*/
|
||||||
|
public static LuaException badArgumentOf(IArguments arguments, int index, String expected) {
|
||||||
|
return badArgument(index, expected, arguments.getType(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a "bad argument" exception, from an expected and actual type.
|
||||||
|
*
|
||||||
|
* @param index The argument number, starting from 0.
|
||||||
|
* @param expected The expected type for this argument.
|
||||||
|
* @param actual The provided type for this argument.
|
||||||
|
* @return The constructed exception, which should be thrown immediately.
|
||||||
|
*/
|
||||||
|
public static LuaException badArgument(int index, String expected, String actual) {
|
||||||
|
return new LuaException("bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a table item exception, from an expected and actual type.
|
||||||
|
*
|
||||||
|
* @param index The index into the table, starting from 1.
|
||||||
|
* @param expected The expected type for this table item.
|
||||||
|
* @param actual The provided type for this table item.
|
||||||
|
* @return The constructed exception, which should be thrown immediately.
|
||||||
|
*/
|
||||||
|
public static LuaException badTableItem(int index, String expected, String actual) {
|
||||||
|
return new LuaException("table item #" + index + " is not " + expected + " (got " + actual + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a field exception, from an expected and actual type.
|
||||||
|
*
|
||||||
|
* @param key The name of the field.
|
||||||
|
* @param expected The expected type for this table item.
|
||||||
|
* @param actual The provided type for this table item.
|
||||||
|
* @return The constructed exception, which should be thrown immediately.
|
||||||
|
*/
|
||||||
|
public static LuaException badField(String key, String expected, String actual) {
|
||||||
|
return new LuaException("field " + key + " is not " + expected + " (got " + actual + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}.
|
||||||
|
*
|
||||||
|
* @param index The argument index to check.
|
||||||
|
* @param value The value to check.
|
||||||
|
* @return The input {@code value}.
|
||||||
|
* @throws LuaException If this is not a finite number.
|
||||||
|
*/
|
||||||
|
public static Number checkFiniteNum(int index, Number value) throws LuaException {
|
||||||
|
checkFinite(index, value.doubleValue());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}.
|
||||||
|
*
|
||||||
|
* @param index The argument index to check.
|
||||||
|
* @param value The value to check.
|
||||||
|
* @return The input {@code value}.
|
||||||
|
* @throws LuaException If this is not a finite number.
|
||||||
|
*/
|
||||||
|
public static double checkFinite(int index, double value) throws LuaException {
|
||||||
|
if (!Double.isFinite(value)) throw badArgument(index, "number", getNumericType(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a string is a valid enum value.
|
||||||
|
*
|
||||||
|
* @param index The argument index to check.
|
||||||
|
* @param klass The class of the enum instance.
|
||||||
|
* @param value The value to extract.
|
||||||
|
* @param <T> The type of enum we are extracting.
|
||||||
|
* @return The parsed enum value.
|
||||||
|
* @throws LuaException If this is not a known enum value.
|
||||||
|
*/
|
||||||
|
public static <T extends Enum<T>> T checkEnum(int index, Class<T> klass, String value) throws LuaException {
|
||||||
|
for (T possibility : klass.getEnumConstants()) {
|
||||||
|
if (possibility.name().equalsIgnoreCase(value)) return possibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LuaException("bad argument #" + (index + 1) + " (unknown option " + value + ")");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.api.lua;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link IArguments} which wraps an array of {@link Object}.
|
||||||
|
*/
|
||||||
|
public final class ObjectArguments extends IArguments {
|
||||||
|
private static final IArguments EMPTY = new ObjectArguments();
|
||||||
|
|
||||||
|
private final List<Object> args;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public ObjectArguments(IArguments arguments) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectArguments(Object... args) {
|
||||||
|
this.args = Arrays.asList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectArguments(List<Object> args) {
|
||||||
|
this.args = Objects.requireNonNull(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int count() {
|
||||||
|
return args.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IArguments drop(int count) {
|
||||||
|
if (count < 0) throw new IllegalStateException("count cannot be negative");
|
||||||
|
if (count == 0) return this;
|
||||||
|
if (count >= args.size()) return EMPTY;
|
||||||
|
|
||||||
|
return new ObjectArguments(args.subList(count, args.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object get(int index) {
|
||||||
|
return index >= args.size() ? null : args.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(int index) {
|
||||||
|
return LuaValues.getType(get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] getAll() {
|
||||||
|
return args.toArray();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.client;
|
||||||
|
|
||||||
|
import dan200.computercraft.util.Colour;
|
||||||
|
import net.minecraft.client.renderer.RenderEngine;
|
||||||
|
import net.minecraft.client.renderer.Tessellator;
|
||||||
|
import net.minecraft.client.settings.GameSettings;
|
||||||
|
import org.lwjgl.opengl.GL11;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A replacement for {@link dan200.computer.client.FixedWidthFontRenderer} which uses CC's 1.76+ font.
|
||||||
|
*/
|
||||||
|
public class FixedWidthFontRenderer {
|
||||||
|
public static final int FONT_HEIGHT = 9;
|
||||||
|
public static final int FONT_WIDTH = 6;
|
||||||
|
public static final float WIDTH = 256.0f;
|
||||||
|
|
||||||
|
public static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH;
|
||||||
|
public static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
|
||||||
|
|
||||||
|
private final int fontTextureName;
|
||||||
|
|
||||||
|
public FixedWidthFontRenderer(GameSettings gameSettings, String s, RenderEngine renderEngine) {
|
||||||
|
this.fontTextureName = renderEngine.getTexture("/assets/cctweaked/textures/gui/term_font.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStringWidth(String s) {
|
||||||
|
return s == null ? 0 : s.length() * FONT_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public void drawString(String s, int x, int y, String colours, int marginSize) {
|
||||||
|
this.drawString(s, x, y, colours, (float) marginSize, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public void drawString(String text, int x, int y, String colours, float marginSize, boolean forceBackground) {
|
||||||
|
drawString(text, x, y, colours, marginSize, forceBackground, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drawStringIsColour(String text, int x, int y, String colours, int marginSize, boolean isColour) {
|
||||||
|
drawString(text, x, y, colours, marginSize, false, !isColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drawString(String text, int x, int y, String colours, float marginSize, boolean forceBackground, boolean greyscale) {
|
||||||
|
if (text == null) return;
|
||||||
|
boolean hasBackgrounds = colours.length() > text.length();
|
||||||
|
|
||||||
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, this.fontTextureName);
|
||||||
|
|
||||||
|
Tessellator tessellator = Tessellator.instance;
|
||||||
|
tessellator.startDrawing(GL11.GL_QUADS);
|
||||||
|
if (hasBackgrounds) {
|
||||||
|
drawBackground(tessellator, x, y, colours, text.length(), marginSize, marginSize, FONT_HEIGHT, forceBackground, greyscale);
|
||||||
|
}
|
||||||
|
drawString(tessellator, x, y, text, colours, greyscale);
|
||||||
|
tessellator.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawBackground(
|
||||||
|
Tessellator emitter, float x, float y, String backgroundColour, int start,
|
||||||
|
float leftMarginSize, float rightMarginSize, float height, boolean forceBackground, boolean greyscale
|
||||||
|
) {
|
||||||
|
int length = backgroundColour.length() - start;
|
||||||
|
if (leftMarginSize > 0) {
|
||||||
|
drawQuad(emitter, x - leftMarginSize, y, leftMarginSize, height, backgroundColour.charAt(start), forceBackground, greyscale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightMarginSize > 0) {
|
||||||
|
drawQuad(emitter, x + length * FONT_WIDTH, y, rightMarginSize, height, backgroundColour.charAt(backgroundColour.length() - 1), forceBackground, greyscale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch together runs of identical background cells.
|
||||||
|
int blockStart = 0;
|
||||||
|
char blockColour = '\0';
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
char colourIndex = backgroundColour.charAt(i + start);
|
||||||
|
if (colourIndex == blockColour) continue;
|
||||||
|
|
||||||
|
if (blockColour != '\0') {
|
||||||
|
drawQuad(emitter, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (i - blockStart), height, blockColour, forceBackground, greyscale);
|
||||||
|
}
|
||||||
|
|
||||||
|
blockColour = colourIndex;
|
||||||
|
blockStart = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockColour != '\0') {
|
||||||
|
drawQuad(emitter, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (length - blockStart), height, blockColour, forceBackground, greyscale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawQuad(Tessellator emitter, float x, float y, float width, float height, char colourIndex, boolean forceBackground, boolean greyscale) {
|
||||||
|
Colour colour = Colour.fromInt(getColour(colourIndex, Colour.BLACK));
|
||||||
|
// Skip drawing black quads. Mostly important for printouts.
|
||||||
|
if (colour == Colour.BLACK && !forceBackground) return;
|
||||||
|
|
||||||
|
quad(
|
||||||
|
emitter, x, y, x + width, y + height, 0, getHex(colour, greyscale),
|
||||||
|
BACKGROUND_START, BACKGROUND_START, BACKGROUND_END, BACKGROUND_END
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void drawString(Tessellator emitter, float x, float y, String text, String textColour, boolean greyscale) {
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
int colour = getHex(Colour.fromInt(getColour(textColour.charAt(i), Colour.WHITE)), greyscale);
|
||||||
|
|
||||||
|
int index = text.charAt(i);
|
||||||
|
if (index > 255) index = '?';
|
||||||
|
drawChar(emitter, x + i * FONT_WIDTH, y, index, colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawChar(Tessellator emitter, float x, float y, int index, int colour) {
|
||||||
|
// Short circuit to avoid the common case - the texture should be blank here after all.
|
||||||
|
if (index == '\0' || index == ' ') return;
|
||||||
|
|
||||||
|
int column = index % 16;
|
||||||
|
int row = index / 16;
|
||||||
|
|
||||||
|
int xStart = 1 + column * (FONT_WIDTH + 2);
|
||||||
|
int yStart = 1 + row * (FONT_HEIGHT + 2);
|
||||||
|
|
||||||
|
quad(
|
||||||
|
emitter, x, y, x + FONT_WIDTH, y + FONT_HEIGHT, 0, colour,
|
||||||
|
xStart / WIDTH, yStart / WIDTH, (xStart + FONT_WIDTH) / WIDTH, (yStart + FONT_HEIGHT) / WIDTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void quad(Tessellator emitter, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||||
|
emitter.setColorOpaque_I(colour);
|
||||||
|
emitter.addVertexWithUV(x1, y1, z, u1, v1);
|
||||||
|
emitter.addVertexWithUV(x1, y2, z, u1, v2);
|
||||||
|
emitter.addVertexWithUV(x2, y2, z, u2, v2);
|
||||||
|
emitter.addVertexWithUV(x2, y1, z, u2, v1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getHex(Colour colour, boolean greyscale) {
|
||||||
|
if (greyscale) {
|
||||||
|
int single = (int) ((colour.getR() + colour.getG() + colour.getB()) / 3 * 255) & 0xFF;
|
||||||
|
return (single << 16) | (single << 8) | single;
|
||||||
|
} else {
|
||||||
|
return colour.getHex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getColour(char c, Colour def) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return 15 - def.ordinal();
|
||||||
|
}
|
||||||
|
}
|
519
src/main/java/dan200/computercraft/core/apis/FSAPI.java
Normal file
519
src/main/java/dan200/computercraft/core/apis/FSAPI.java
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import dan200.computer.core.*;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||||
|
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
|
||||||
|
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||||
|
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This
|
||||||
|
* includes:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>**Reading and writing files:** Call {@link #open} to obtain a file "handle", which can be used to read from or
|
||||||
|
* write to a file.</li>
|
||||||
|
* <li>**Path manipulation:** {@link #combine}, {@link #getName} and {@link #getDir} allow you to manipulate file
|
||||||
|
* paths, joining them together or extracting components.</li>
|
||||||
|
* <li>**Querying paths:** For instance, checking if a file exists, or whether it's a directory. See {@link #getSize},
|
||||||
|
* {@link #exists}, {@link #isDir}, {@link #isReadOnly} and {@link #attributes}.</li>
|
||||||
|
* <li>**File and directory manipulation:** For instance, moving or copying files. See {@link #makeDir}, {@link #move},
|
||||||
|
* {@link #copy} and {@link #delete}.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* :::note
|
||||||
|
* All functions in the API work on absolute paths, and do not take the @{shell.dir|current directory} into account.
|
||||||
|
* You can use @{shell.resolve} to convert a relative path into an absolute one.
|
||||||
|
* :::
|
||||||
|
* <p>
|
||||||
|
* ## Mounts
|
||||||
|
* While a computer can only have one hard drive and filesystem, other filesystems may be "mounted" inside it. For
|
||||||
|
* instance, the {@link dan200.computer.shared.TileEntityDiskDrive drive peripheral} mounts
|
||||||
|
* its disk's contents at {@code "disk/"}, {@code "disk1/"}, etc...
|
||||||
|
* <p>
|
||||||
|
* You can see which mount a path belongs to with the {@link #getDrive} function. This returns {@code "hdd"} for the
|
||||||
|
* computer's main filesystem ({@code "/"}), {@code "rom"} for the rom ({@code "rom/"}).
|
||||||
|
* <p>
|
||||||
|
* Most filesystems have a limited capacity, operations which would cause that capacity to be reached (such as writing
|
||||||
|
* an incredibly large file) will fail. You can see a mount's capacity with {@link #getCapacity} and the remaining
|
||||||
|
* space with {@link #getFreeSpace}.
|
||||||
|
*
|
||||||
|
* @cc.module fs
|
||||||
|
*/
|
||||||
|
public class FSAPI implements ILuaAPI {
|
||||||
|
private final IAPIEnvironment environment;
|
||||||
|
private FileSystem fileSystem = null;
|
||||||
|
|
||||||
|
public FSAPI(IAPIEnvironment env) {
|
||||||
|
environment = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getNames() {
|
||||||
|
return new String[]{ "fs" };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void advance(double v) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startup() {
|
||||||
|
fileSystem = environment.getFileSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
fileSystem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getMethodNames() {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] callMethod(int method, Object[] arguments) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileSystem getFileSystem() {
|
||||||
|
FileSystem filesystem = fileSystem;
|
||||||
|
if (filesystem == null) throw new IllegalStateException("File system is not mounted");
|
||||||
|
return filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of files in a directory.
|
||||||
|
*
|
||||||
|
* @param path The path to list.
|
||||||
|
* @return A table with a list of files in the directory.
|
||||||
|
* @throws LuaException If the path doesn't exist.
|
||||||
|
* @cc.usage List all files under {@code /rom/}
|
||||||
|
* <pre>{@code
|
||||||
|
* local files = fs.list("/rom/")
|
||||||
|
* for i = 1, #files do
|
||||||
|
* print(files[i])
|
||||||
|
* end
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final String[] list(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
String[] files = getFileSystem().list(path);
|
||||||
|
Arrays.sort(files);
|
||||||
|
return files;
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines several parts of a path into one full path, adding separators as
|
||||||
|
* needed.
|
||||||
|
*
|
||||||
|
* @param arguments The paths to combine.
|
||||||
|
* @return The new path, with separators added between parts as needed.
|
||||||
|
* @throws LuaException On argument errors.
|
||||||
|
* @cc.tparam string path The first part of the path. For example, a parent directory path.
|
||||||
|
* @cc.tparam string ... Additional parts of the path to combine.
|
||||||
|
* @cc.changed 1.95.0 Now supports multiple arguments.
|
||||||
|
* @cc.usage Combine several file paths together
|
||||||
|
* <pre>{@code
|
||||||
|
* fs.combine("/rom/programs", "../apis", "parallel.lua")
|
||||||
|
* -- => rom/apis/parallel.lua
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final String combine(IArguments arguments) throws LuaException {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append(FileSystemExtensions.sanitizePath(arguments.getString(0), true));
|
||||||
|
|
||||||
|
for (int i = 1, n = arguments.count(); i < n; i++) {
|
||||||
|
String part = FileSystemExtensions.sanitizePath(arguments.getString(i), true);
|
||||||
|
if (result.length() != 0 && !part.isEmpty()) result.append('/');
|
||||||
|
result.append(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileSystemExtensions.sanitizePath(result.toString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file name portion of a path.
|
||||||
|
*
|
||||||
|
* @param path The path to get the name from.
|
||||||
|
* @return The final part of the path (the file name).
|
||||||
|
* @cc.since 1.2
|
||||||
|
* @cc.usage Get the file name of {@code rom/startup.lua}
|
||||||
|
* <pre>{@code
|
||||||
|
* fs.getName("rom/startup.lua")
|
||||||
|
* -- => startup.lua
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final String getName(String path) {
|
||||||
|
return getFileSystem().getName(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parent directory portion of a path.
|
||||||
|
*
|
||||||
|
* @param path The path to get the directory from.
|
||||||
|
* @return The path with the final part removed (the parent directory).
|
||||||
|
* @cc.since 1.63
|
||||||
|
* @cc.usage Get the directory name of {@code rom/startup.lua}
|
||||||
|
* <pre>{@code
|
||||||
|
* fs.getDir("rom/startup.lua")
|
||||||
|
* -- => rom
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final String getDir(String path) {
|
||||||
|
return FileSystemExtensions.getDirectory(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the specified file.
|
||||||
|
*
|
||||||
|
* @param path The file to get the file size of.
|
||||||
|
* @return The size of the file, in bytes.
|
||||||
|
* @throws LuaException If the path doesn't exist.
|
||||||
|
* @cc.since 1.3
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final long getSize(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
return getFileSystem().getSize(path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the specified path exists.
|
||||||
|
*
|
||||||
|
* @param path The path to check the existence of.
|
||||||
|
* @return Whether the path exists.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final boolean exists(String path) {
|
||||||
|
try {
|
||||||
|
return getFileSystem().exists(path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the specified path is a directory.
|
||||||
|
*
|
||||||
|
* @param path The path to check.
|
||||||
|
* @return Whether the path is a directory.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final boolean isDir(String path) {
|
||||||
|
try {
|
||||||
|
return getFileSystem().isDir(path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a path is read-only.
|
||||||
|
*
|
||||||
|
* @param path The path to check.
|
||||||
|
* @return Whether the path cannot be written to.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final boolean isReadOnly(String path) {
|
||||||
|
try {
|
||||||
|
return getFileSystem().isReadOnly(path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a directory, and any missing parents, at the specified path.
|
||||||
|
*
|
||||||
|
* @param path The path to the directory to create.
|
||||||
|
* @throws LuaException If the directory couldn't be created.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void makeDir(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
getFileSystem().makeDir(path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a file or directory from one path to another.
|
||||||
|
* <p>
|
||||||
|
* Any parent directories are created as needed.
|
||||||
|
*
|
||||||
|
* @param path The current file or directory to move from.
|
||||||
|
* @param dest The destination path for the file or directory.
|
||||||
|
* @throws LuaException If the file or directory couldn't be moved.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void move(String path, String dest) throws LuaException {
|
||||||
|
try {
|
||||||
|
getFileSystem().move(path, dest);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file or directory to a new path.
|
||||||
|
* <p>
|
||||||
|
* Any parent directories are created as needed.
|
||||||
|
*
|
||||||
|
* @param path The file or directory to copy.
|
||||||
|
* @param dest The path to the destination file or directory.
|
||||||
|
* @throws LuaException If the file or directory couldn't be copied.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void copy(String path, String dest) throws LuaException {
|
||||||
|
try {
|
||||||
|
getFileSystem().copy(path, dest);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a file or directory.
|
||||||
|
* <p>
|
||||||
|
* If the path points to a directory, all of the enclosed files and
|
||||||
|
* subdirectories are also deleted.
|
||||||
|
*
|
||||||
|
* @param path The path to the file or directory to delete.
|
||||||
|
* @throws LuaException If the file or directory couldn't be deleted.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void delete(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
getFileSystem().delete(path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Add individual handle type documentation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file for reading or writing at a path.
|
||||||
|
* <p>
|
||||||
|
* The {@code mode} string can be any of the following:
|
||||||
|
* <ul>
|
||||||
|
* <li><strong>"r"</strong>: Read mode</li>
|
||||||
|
* <li><strong>"w"</strong>: Write mode</li>
|
||||||
|
* <li><strong>"a"</strong>: Append mode</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The mode may also have a "b" at the end, which opens the file in "binary
|
||||||
|
* mode". This allows you to read binary files, as well as seek within a file.
|
||||||
|
*
|
||||||
|
* @param path The path to the file to open.
|
||||||
|
* @param mode The mode to open the file with.
|
||||||
|
* @return A file handle object for the file, or {@code nil} + an error message on error.
|
||||||
|
* @throws LuaException If an invalid mode was specified.
|
||||||
|
* @cc.treturn [1] table A file handle object for the file.
|
||||||
|
* @cc.treturn [2] nil If the file does not exist, or cannot be opened.
|
||||||
|
* @cc.treturn string|nil A message explaining why the file cannot be opened.
|
||||||
|
* @cc.usage Read the contents of a file.
|
||||||
|
* <pre>{@code
|
||||||
|
* local file = fs.open("/rom/help/intro.txt", "r")
|
||||||
|
* local contents = file.readAll()
|
||||||
|
* file.close()
|
||||||
|
*
|
||||||
|
* print(contents)
|
||||||
|
* }</pre>
|
||||||
|
* @cc.usage Open a file and read all lines into a table. @{io.lines} offers an alternative way to do this.
|
||||||
|
* <pre>{@code
|
||||||
|
* local file = fs.open("/rom/motd.txt", "r")
|
||||||
|
* local lines = {}
|
||||||
|
* while true do
|
||||||
|
* local line = file.readLine()
|
||||||
|
*
|
||||||
|
* -- If line is nil then we've reached the end of the file and should stop
|
||||||
|
* if not line then break end
|
||||||
|
*
|
||||||
|
* lines[#lines + 1] = line
|
||||||
|
* end
|
||||||
|
*
|
||||||
|
* file.close()
|
||||||
|
*
|
||||||
|
* print(lines[math.random(#lines)]) -- Pick a random line and print it.
|
||||||
|
* }</pre>
|
||||||
|
* @cc.usage Open a file and write some text to it. You can run {@code edit out.txt} to see the written text.
|
||||||
|
* <pre>{@code
|
||||||
|
* local file = fs.open("out.txt", "w")
|
||||||
|
* file.write("Just testing some code")
|
||||||
|
* file.close() -- Remember to call close, otherwise changes may not be written!
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] open(String path, String mode) throws LuaException {
|
||||||
|
try {
|
||||||
|
switch (mode) {
|
||||||
|
// Open the file for reading, then create a wrapper around the reader
|
||||||
|
case "r":
|
||||||
|
return new Object[]{ new EncodedReadableHandle(getFileSystem().openForRead(path)) };
|
||||||
|
// Open the file for writing, then create a wrapper around the writer
|
||||||
|
case "w":
|
||||||
|
return new Object[]{ new EncodedWritableHandle(getFileSystem().openForWrite(path, false)) };
|
||||||
|
// Open the file for appending, then create a wrapper around the writer
|
||||||
|
case "a":
|
||||||
|
return new Object[]{ new EncodedWritableHandle(getFileSystem().openForWrite(path, true)) };
|
||||||
|
// Open the file for binary reading, then create a wrapper around the reader
|
||||||
|
case "rb":
|
||||||
|
IMountedFileBinary reader = getFileSystem().openForBinaryRead(path);
|
||||||
|
return new Object[]{ new BinaryReadableHandle(reader) };
|
||||||
|
// Open the file for binary writing, then create a wrapper around the writer
|
||||||
|
case "wb":
|
||||||
|
return new Object[]{ new BinaryWritableHandle(getFileSystem().openForBinaryWrite(path, false)) };
|
||||||
|
// Open the file for binary appending, then create a wrapper around the reader
|
||||||
|
case "ab":
|
||||||
|
return new Object[]{ new BinaryWritableHandle(getFileSystem().openForBinaryWrite(path, true)) };
|
||||||
|
default:
|
||||||
|
throw new LuaException("Unsupported mode");
|
||||||
|
}
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
return new Object[]{ null, e.getMessage() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the mount that the specified path is located on.
|
||||||
|
*
|
||||||
|
* @param path The path to get the drive of.
|
||||||
|
* @return The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files.
|
||||||
|
* @throws LuaException If the path doesn't exist.
|
||||||
|
* @cc.treturn string|nil The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files.
|
||||||
|
* @cc.usage Print the drives of a couple of mounts:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* print("/: " .. fs.getDrive("/"))
|
||||||
|
* print("/rom/: " .. fs.getDrive("rom"))
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] getDrive(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
return getFileSystem().exists(path) ? new Object[]{ getFileSystem().getMountLabel(path) } : null;
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of free space available on the drive the path is
|
||||||
|
* located on.
|
||||||
|
*
|
||||||
|
* @param path The path to check the free space for.
|
||||||
|
* @return The amount of free space available, in bytes.
|
||||||
|
* @throws LuaException If the path doesn't exist.
|
||||||
|
* @cc.treturn number|"unlimited" The amount of free space available, in bytes, or "unlimited".
|
||||||
|
* @cc.since 1.4
|
||||||
|
* @see #getCapacity To get the capacity of this drive.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object getFreeSpace(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
long freeSpace = getFileSystem().getFreeSpace(path);
|
||||||
|
return freeSpace >= 0 ? freeSpace : "unlimited";
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for files matching a string with wildcards.
|
||||||
|
* <p>
|
||||||
|
* This string is formatted like a normal path string, but can include any
|
||||||
|
* number of wildcards ({@code *}) to look for files matching anything.
|
||||||
|
* For example, <code>rom/*/command*</code> will look for any path starting with
|
||||||
|
* {@code command} inside any subdirectory of {@code /rom}.
|
||||||
|
*
|
||||||
|
* @param path The wildcard-qualified path to search for.
|
||||||
|
* @return A list of paths that match the search string.
|
||||||
|
* @throws LuaException If the path doesn't exist.
|
||||||
|
* @cc.since 1.6
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final String[] find(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
return FileSystemExtensions.find(getFileSystem(), path);
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the capacity of the drive the path is located on.
|
||||||
|
*
|
||||||
|
* @param path The path of the drive to get.
|
||||||
|
* @return The drive's capacity.
|
||||||
|
* @throws LuaException If the capacity cannot be determined.
|
||||||
|
* @cc.treturn number|nil This drive's capacity. This will be nil for "read-only" drives, such as the ROM or
|
||||||
|
* treasure disks.
|
||||||
|
* @cc.since 1.87.0
|
||||||
|
* @see #getFreeSpace To get the free space available on this drive.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object getCapacity(String path) throws LuaException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attributes about a specific file or folder.
|
||||||
|
* <p>
|
||||||
|
* The returned attributes table contains information about the size of the file, whether it is a directory,
|
||||||
|
* when it was created and last modified, and whether it is read only.
|
||||||
|
* <p>
|
||||||
|
* The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be
|
||||||
|
* given to {@link OSAPI#date} in order to convert it to more usable form.
|
||||||
|
*
|
||||||
|
* @param path The path to get attributes for.
|
||||||
|
* @return The resulting attributes.
|
||||||
|
* @throws LuaException If the path does not exist.
|
||||||
|
* @cc.treturn { size = number, isDir = boolean, isReadOnly = boolean, created = number, modified = number } The resulting attributes.
|
||||||
|
* @cc.since 1.87.0
|
||||||
|
* @cc.changed 1.91.0 Renamed `modification` field to `modified`.
|
||||||
|
* @cc.changed 1.95.2 Added `isReadOnly` to attributes.
|
||||||
|
* @see #getSize If you only care about the file's size.
|
||||||
|
* @see #isDir If you only care whether a path is a directory or not.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Map<String, Object> attributes(String path) throws LuaException {
|
||||||
|
try {
|
||||||
|
boolean isDir = getFileSystem().isDir(path);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("modification", 0);
|
||||||
|
result.put("modified", 0);
|
||||||
|
result.put("created", 0);
|
||||||
|
result.put("size", isDir ? 0 : getFileSystem().getSize(path));
|
||||||
|
result.put("isDir", isDir);
|
||||||
|
result.put("isReadOnly", getFileSystem().isReadOnly(path));
|
||||||
|
return result;
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import dan200.computer.core.FileSystem;
|
||||||
|
import dan200.computer.core.FileSystemException;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backports additional methods from {@link FileSystem}.
|
||||||
|
*/
|
||||||
|
final class FileSystemExtensions {
|
||||||
|
private static void findIn(FileSystem fs, String dir, List<String> matches, Pattern wildPattern) throws FileSystemException {
|
||||||
|
String[] list = fs.list(dir);
|
||||||
|
for (String entry : list) {
|
||||||
|
String entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
|
||||||
|
if (wildPattern.matcher(entryPath).matches()) {
|
||||||
|
matches.add(entryPath);
|
||||||
|
}
|
||||||
|
if (fs.isDir(entryPath)) {
|
||||||
|
findIn(fs, entryPath, matches, wildPattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized String[] find(FileSystem fs, String wildPath) throws FileSystemException {
|
||||||
|
// Match all the files on the system
|
||||||
|
wildPath = sanitizePath(wildPath, true);
|
||||||
|
|
||||||
|
// If we don't have a wildcard at all just check the file exists
|
||||||
|
int starIndex = wildPath.indexOf('*');
|
||||||
|
if (starIndex == -1) {
|
||||||
|
return fs.exists(wildPath) ? new String[]{ wildPath } : new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
|
||||||
|
int prevDir = wildPath.substring(0, starIndex).lastIndexOf('/');
|
||||||
|
String startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir);
|
||||||
|
|
||||||
|
// If this isn't a directory then just abort
|
||||||
|
if (!fs.isDir(startDir)) return new String[0];
|
||||||
|
|
||||||
|
// Scan as normal, starting from this directory
|
||||||
|
Pattern wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
|
||||||
|
List<String> matches = new ArrayList<>();
|
||||||
|
findIn(fs, startDir, matches, wildPattern);
|
||||||
|
|
||||||
|
// Return matches
|
||||||
|
String[] array = matches.toArray(new String[0]);
|
||||||
|
Arrays.sort(array);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDirectory(String path) {
|
||||||
|
path = sanitizePath(path, true);
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return "..";
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastSlash = path.lastIndexOf('/');
|
||||||
|
if (lastSlash >= 0) {
|
||||||
|
return path.substring(0, lastSlash);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
|
||||||
|
|
||||||
|
public static String sanitizePath(String path, boolean allowWildcards) {
|
||||||
|
// Allow windowsy slashes
|
||||||
|
path = path.replace('\\', '/');
|
||||||
|
|
||||||
|
// Clean the path or illegal characters.
|
||||||
|
final char[] specialChars = new char[]{
|
||||||
|
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
|
||||||
|
};
|
||||||
|
|
||||||
|
StringBuilder cleanName = new StringBuilder();
|
||||||
|
for (int i = 0; i < path.length(); i++) {
|
||||||
|
char c = path.charAt(i);
|
||||||
|
if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) {
|
||||||
|
cleanName.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = cleanName.toString();
|
||||||
|
|
||||||
|
// Collapse the string into its component parts, removing ..'s
|
||||||
|
Deque<String> outputParts = new ArrayDeque<String>();
|
||||||
|
for (String fullPart : Splitter.on('/').split(path)) {
|
||||||
|
String part = fullPart.trim();
|
||||||
|
|
||||||
|
if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part).matches()) {
|
||||||
|
// . is redundant
|
||||||
|
// ... and more are treated as .
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.equals("..")) {
|
||||||
|
// .. can cancel out the last folder entered
|
||||||
|
if (!outputParts.isEmpty()) {
|
||||||
|
String top = outputParts.peekLast();
|
||||||
|
if (!top.equals("..")) {
|
||||||
|
outputParts.removeLast();
|
||||||
|
} else {
|
||||||
|
outputParts.addLast("..");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputParts.addLast("..");
|
||||||
|
}
|
||||||
|
} else if (part.length() >= 255) {
|
||||||
|
// If part length > 255 and it is the last part
|
||||||
|
outputParts.addLast(part.substring(0, 255).trim());
|
||||||
|
} else {
|
||||||
|
// Anything else we add to the stack
|
||||||
|
outputParts.addLast(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.join("/", outputParts);
|
||||||
|
}
|
||||||
|
}
|
253
src/main/java/dan200/computercraft/core/apis/LuaDateTime.java
Normal file
253
src/main/java/dan200/computercraft/core/apis/LuaDateTime.java
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
|
import java.time.format.TextStyle;
|
||||||
|
import java.time.temporal.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.LongUnaryOperator;
|
||||||
|
|
||||||
|
final class LuaDateTime {
|
||||||
|
private LuaDateTime() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void format(DateTimeFormatterBuilder formatter, String format) throws LuaException {
|
||||||
|
for (int i = 0; i < format.length(); ) {
|
||||||
|
char c;
|
||||||
|
switch (c = format.charAt(i++)) {
|
||||||
|
case '\n':
|
||||||
|
formatter.appendLiteral('\n');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
formatter.appendLiteral(c);
|
||||||
|
break;
|
||||||
|
case '%':
|
||||||
|
if (i >= format.length()) break;
|
||||||
|
switch (c = format.charAt(i++)) {
|
||||||
|
default:
|
||||||
|
throw new LuaException("bad argument #1: invalid conversion specifier '%" + c + "'");
|
||||||
|
case '%':
|
||||||
|
formatter.appendLiteral('%');
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
formatter.appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT);
|
||||||
|
break;
|
||||||
|
case 'A':
|
||||||
|
formatter.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL);
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
case 'h':
|
||||||
|
formatter.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT);
|
||||||
|
break;
|
||||||
|
case 'B':
|
||||||
|
formatter.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL);
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
format(formatter, "%a %b %e %H:%M:%S %Y");
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
formatter.appendValueReduced(CENTURY, 2, 2, 0);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
formatter.appendValue(ChronoField.DAY_OF_MONTH, 2);
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
case 'x':
|
||||||
|
format(formatter, "%m/%d/%y");
|
||||||
|
break;
|
||||||
|
case 'e':
|
||||||
|
formatter.padNext(2).appendValue(ChronoField.DAY_OF_MONTH);
|
||||||
|
break;
|
||||||
|
case 'F':
|
||||||
|
format(formatter, "%Y-%m-%d");
|
||||||
|
break;
|
||||||
|
case 'g':
|
||||||
|
formatter.appendValueReduced(IsoFields.WEEK_BASED_YEAR, 2, 2, 0);
|
||||||
|
break;
|
||||||
|
case 'G':
|
||||||
|
formatter.appendValue(IsoFields.WEEK_BASED_YEAR);
|
||||||
|
break;
|
||||||
|
case 'H':
|
||||||
|
formatter.appendValue(ChronoField.HOUR_OF_DAY, 2);
|
||||||
|
break;
|
||||||
|
case 'I':
|
||||||
|
formatter.appendValue(ChronoField.CLOCK_HOUR_OF_AMPM, 2);
|
||||||
|
break;
|
||||||
|
case 'j':
|
||||||
|
formatter.appendValue(ChronoField.DAY_OF_YEAR, 3);
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
formatter.appendValue(ChronoField.MONTH_OF_YEAR, 2);
|
||||||
|
break;
|
||||||
|
case 'M':
|
||||||
|
formatter.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
formatter.appendLiteral('\n');
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
formatter.appendText(ChronoField.AMPM_OF_DAY);
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
format(formatter, "%I:%M:%S %p");
|
||||||
|
break;
|
||||||
|
case 'R':
|
||||||
|
format(formatter, "%H:%M");
|
||||||
|
break;
|
||||||
|
case 'S':
|
||||||
|
formatter.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
formatter.appendLiteral('\t');
|
||||||
|
break;
|
||||||
|
case 'T':
|
||||||
|
case 'X':
|
||||||
|
format(formatter, "%H:%M:%S");
|
||||||
|
break;
|
||||||
|
case 'u':
|
||||||
|
formatter.appendValue(ChronoField.DAY_OF_WEEK);
|
||||||
|
break;
|
||||||
|
case 'U':
|
||||||
|
formatter.appendValue(ChronoField.ALIGNED_WEEK_OF_YEAR, 2);
|
||||||
|
break;
|
||||||
|
case 'V':
|
||||||
|
formatter.appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2);
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
formatter.appendValue(ZERO_WEEK);
|
||||||
|
break;
|
||||||
|
case 'W':
|
||||||
|
formatter.appendValue(WeekFields.ISO.weekOfYear(), 2);
|
||||||
|
break;
|
||||||
|
case 'y':
|
||||||
|
formatter.appendValueReduced(ChronoField.YEAR, 2, 2, 0);
|
||||||
|
break;
|
||||||
|
case 'Y':
|
||||||
|
formatter.appendValue(ChronoField.YEAR);
|
||||||
|
break;
|
||||||
|
case 'z':
|
||||||
|
formatter.appendOffset("+HHMM", "+0000");
|
||||||
|
break;
|
||||||
|
case 'Z':
|
||||||
|
formatter.appendChronologyId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static long fromTable(Map<?, ?> table) throws LuaException {
|
||||||
|
int year = getField(table, "year", -1);
|
||||||
|
int month = getField(table, "month", -1);
|
||||||
|
int day = getField(table, "day", -1);
|
||||||
|
int hour = getField(table, "hour", 12);
|
||||||
|
int minute = getField(table, "min", 12);
|
||||||
|
int second = getField(table, "sec", 12);
|
||||||
|
LocalDateTime time = LocalDateTime.of(year, month, day, hour, minute, second);
|
||||||
|
|
||||||
|
Boolean isDst = getBoolField(table, "isdst");
|
||||||
|
if (isDst != null) {
|
||||||
|
boolean requireDst = isDst;
|
||||||
|
for (ZoneOffset possibleOffset : ZoneOffset.systemDefault().getRules().getValidOffsets(time)) {
|
||||||
|
Instant instant = time.toInstant(possibleOffset);
|
||||||
|
if (possibleOffset.getRules().getDaylightSavings(instant).isZero() == requireDst) {
|
||||||
|
return instant.getEpochSecond();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoneOffset offset = ZoneOffset.systemDefault().getRules().getOffset(time);
|
||||||
|
return time.toInstant(offset).getEpochSecond();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, ?> toTable(TemporalAccessor date, ZoneId offset, Instant instant) {
|
||||||
|
Map<String, Object> table = new HashMap<>(9);
|
||||||
|
table.put("year", date.getLong(ChronoField.YEAR));
|
||||||
|
table.put("month", date.getLong(ChronoField.MONTH_OF_YEAR));
|
||||||
|
table.put("day", date.getLong(ChronoField.DAY_OF_MONTH));
|
||||||
|
table.put("hour", date.getLong(ChronoField.HOUR_OF_DAY));
|
||||||
|
table.put("min", date.getLong(ChronoField.MINUTE_OF_HOUR));
|
||||||
|
table.put("sec", date.getLong(ChronoField.SECOND_OF_MINUTE));
|
||||||
|
table.put("wday", date.getLong(WeekFields.SUNDAY_START.dayOfWeek()));
|
||||||
|
table.put("yday", date.getLong(ChronoField.DAY_OF_YEAR));
|
||||||
|
table.put("isdst", offset.getRules().isDaylightSavings(instant));
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getField(Map<?, ?> table, String field, int def) throws LuaException {
|
||||||
|
Object value = table.get(field);
|
||||||
|
if (value instanceof Number) return ((Number) value).intValue();
|
||||||
|
if (def < 0) throw new LuaException("field \"" + field + "\" missing in date table");
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean getBoolField(Map<?, ?> table, String field) throws LuaException {
|
||||||
|
Object value = table.get(field);
|
||||||
|
if (value instanceof Boolean || value == null) return (Boolean) value;
|
||||||
|
throw new LuaException("field \"" + field + "\" missing in date table");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final TemporalField CENTURY = map(ChronoField.YEAR, ValueRange.of(0, 99), x -> (x / 100) % 100);
|
||||||
|
private static final TemporalField ZERO_WEEK = map(WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of(0, 6), x -> x - 1);
|
||||||
|
|
||||||
|
private static TemporalField map(TemporalField field, ValueRange range, LongUnaryOperator convert) {
|
||||||
|
return new TemporalField() {
|
||||||
|
@Override
|
||||||
|
public TemporalUnit getBaseUnit() {
|
||||||
|
return field.getBaseUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TemporalUnit getRangeUnit() {
|
||||||
|
return field.getRangeUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValueRange range() {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDateBased() {
|
||||||
|
return field.isDateBased();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTimeBased() {
|
||||||
|
return field.isTimeBased();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupportedBy(TemporalAccessor temporal) {
|
||||||
|
return field.isSupportedBy(temporal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getFrom(TemporalAccessor temporal) {
|
||||||
|
return convert.applyAsLong(temporal.getLong(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <R extends Temporal> R adjustInto(R temporal, long newValue) {
|
||||||
|
return (R) temporal.with(field, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
503
src/main/java/dan200/computercraft/core/apis/OSAPI.java
Normal file
503
src/main/java/dan200/computercraft/core/apis/OSAPI.java
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import dan200.computer.core.IAPIEnvironment;
|
||||||
|
import dan200.computer.core.ILuaAPI;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.core.util.StringUtil;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link OSAPI} API allows interacting with the current computer.
|
||||||
|
*
|
||||||
|
* @cc.module os
|
||||||
|
*/
|
||||||
|
public class OSAPI implements ILuaAPI {
|
||||||
|
private final IAPIEnvironment apiEnvironment;
|
||||||
|
|
||||||
|
private final Map<Integer, Alarm> alarms = new HashMap<>();
|
||||||
|
private final Map<Integer, Timer> timers = new HashMap<>();
|
||||||
|
private int clock;
|
||||||
|
private double time;
|
||||||
|
private int day;
|
||||||
|
|
||||||
|
private int nextTimerToken = 0;
|
||||||
|
private int nextAlarmToken = 0;
|
||||||
|
|
||||||
|
private static class Timer {
|
||||||
|
int ticksLeft;
|
||||||
|
|
||||||
|
Timer(int ticksLeft) {
|
||||||
|
this.ticksLeft = ticksLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class Alarm implements Comparable<Alarm> {
|
||||||
|
final double time;
|
||||||
|
final int day;
|
||||||
|
|
||||||
|
private Alarm(double time, int day) {
|
||||||
|
this.time = time;
|
||||||
|
this.day = day;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Alarm o) {
|
||||||
|
double t = day * 24.0 + time;
|
||||||
|
double ot = day * 24.0 + time;
|
||||||
|
return Double.compare(t, ot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OSAPI(IAPIEnvironment environment) {
|
||||||
|
apiEnvironment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getNames() {
|
||||||
|
return new String[]{ "os" };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startup() {
|
||||||
|
time = apiEnvironment.getComputerEnvironment().getTimeOfDay();
|
||||||
|
day = apiEnvironment.getComputerEnvironment().getDay();
|
||||||
|
clock = 0;
|
||||||
|
|
||||||
|
synchronized (timers) {
|
||||||
|
timers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (alarms) {
|
||||||
|
alarms.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void advance(double dt) {
|
||||||
|
clock++;
|
||||||
|
|
||||||
|
synchronized (timers) {
|
||||||
|
// Countdown all of our active timers
|
||||||
|
Iterator<Map.Entry<Integer, Timer>> it = timers.entrySet().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Map.Entry<Integer, Timer> entry = it.next();
|
||||||
|
Timer timer = entry.getValue();
|
||||||
|
timer.ticksLeft--;
|
||||||
|
if (timer.ticksLeft <= 0) {
|
||||||
|
// Queue the "timer" event
|
||||||
|
apiEnvironment.queueEvent("timer", new Object[]{ entry.getKey() });
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all of our alarms
|
||||||
|
synchronized (alarms) {
|
||||||
|
double previousTime = time;
|
||||||
|
int previousDay = day;
|
||||||
|
double time = apiEnvironment.getComputerEnvironment().getTimeOfDay();
|
||||||
|
int day = apiEnvironment.getComputerEnvironment().getDay();
|
||||||
|
|
||||||
|
if (time > previousTime || day > previousDay) {
|
||||||
|
double now = this.day * 24.0 + this.time;
|
||||||
|
Iterator<Map.Entry<Integer, Alarm>> it = alarms.entrySet().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Map.Entry<Integer, Alarm> entry = it.next();
|
||||||
|
Alarm alarm = entry.getValue();
|
||||||
|
double t = alarm.day * 24.0 + alarm.time;
|
||||||
|
if (now >= t) {
|
||||||
|
apiEnvironment.queueEvent("alarm", new Object[]{ entry.getKey() });
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.time = time;
|
||||||
|
this.day = day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
synchronized (alarms) {
|
||||||
|
alarms.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getMethodNames() {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] callMethod(int method, Object[] arguments) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getTimeForCalendar(Calendar c) {
|
||||||
|
float time = c.get(Calendar.HOUR_OF_DAY);
|
||||||
|
time += c.get(Calendar.MINUTE) / 60.0f;
|
||||||
|
time += c.get(Calendar.SECOND) / (60.0f * 60.0f);
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getDayForCalendar(Calendar c) {
|
||||||
|
GregorianCalendar g = c instanceof GregorianCalendar ? (GregorianCalendar) c : new GregorianCalendar();
|
||||||
|
int year = c.get(Calendar.YEAR);
|
||||||
|
int day = 0;
|
||||||
|
for (int y = 1970; y < year; y++) {
|
||||||
|
day += g.isLeapYear(y) ? 366 : 365;
|
||||||
|
}
|
||||||
|
day += c.get(Calendar.DAY_OF_YEAR);
|
||||||
|
return day;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getEpochForCalendar(Calendar c) {
|
||||||
|
return c.getTime().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event to the event queue. This event can later be pulled with
|
||||||
|
* os.pullEvent.
|
||||||
|
*
|
||||||
|
* @param name The name of the event to queue.
|
||||||
|
* @param args The parameters of the event.
|
||||||
|
* @cc.tparam string name The name of the event to queue.
|
||||||
|
* @cc.param ... The parameters of the event.
|
||||||
|
* @cc.see os.pullEvent To pull the event queued
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void queueEvent(String name, IArguments args) throws LuaException {
|
||||||
|
apiEnvironment.queueEvent(name, args.drop(1).getAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a timer that will run for the specified number of seconds. Once
|
||||||
|
* the timer fires, a {@code timer} event will be added to the queue with
|
||||||
|
* the ID returned from this function as the first parameter.
|
||||||
|
* <p>
|
||||||
|
* As with @{os.sleep|sleep}, {@code timer} will automatically be rounded up
|
||||||
|
* to the nearest multiple of 0.05 seconds, as it waits for a fixed amount
|
||||||
|
* of world ticks.
|
||||||
|
*
|
||||||
|
* @param timer The number of seconds until the timer fires.
|
||||||
|
* @return The ID of the new timer. This can be used to filter the
|
||||||
|
* {@code timer} event, or {@link #cancelTimer cancel the timer}.
|
||||||
|
* @throws LuaException If the time is below zero.
|
||||||
|
* @see #cancelTimer To cancel a timer.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final int startTimer(double timer) throws LuaException {
|
||||||
|
timers.put(nextTimerToken, new Timer((int) Math.round(checkFinite(0, timer) / 0.05)));
|
||||||
|
return nextTimerToken++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a timer previously started with startTimer. This will stop the
|
||||||
|
* timer from firing.
|
||||||
|
*
|
||||||
|
* @param token The ID of the timer to cancel.
|
||||||
|
* @cc.since 1.6
|
||||||
|
* @see #startTimer To start a timer.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void cancelTimer(int token) {
|
||||||
|
timers.remove(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an alarm that will fire at the specified in-game time. When it
|
||||||
|
* fires, * an {@code alarm} event will be added to the event queue with the
|
||||||
|
* ID * returned from this function as the first parameter.
|
||||||
|
*
|
||||||
|
* @param time The time at which to fire the alarm, in the range [0.0, 24.0).
|
||||||
|
* @return The ID of the new alarm. This can be used to filter the
|
||||||
|
* {@code alarm} event, or {@link #cancelAlarm cancel the alarm}.
|
||||||
|
* @throws LuaException If the time is out of range.
|
||||||
|
* @cc.since 1.2
|
||||||
|
* @see #cancelAlarm To cancel an alarm.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final int setAlarm(double time) throws LuaException {
|
||||||
|
checkFinite(0, time);
|
||||||
|
if (time < 0.0 || time >= 24.0) throw new LuaException("Number out of range");
|
||||||
|
synchronized (alarms) {
|
||||||
|
int day = time > this.time ? this.day : this.day + 1;
|
||||||
|
alarms.put(nextAlarmToken, new Alarm(time, day));
|
||||||
|
return nextAlarmToken++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels an alarm previously started with setAlarm. This will stop the
|
||||||
|
* alarm from firing.
|
||||||
|
*
|
||||||
|
* @param token The ID of the alarm to cancel.
|
||||||
|
* @cc.since 1.6
|
||||||
|
* @see #setAlarm To set an alarm.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void cancelAlarm(int token) {
|
||||||
|
synchronized (alarms) {
|
||||||
|
alarms.remove(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the computer immediately.
|
||||||
|
*/
|
||||||
|
@LuaFunction("shutdown")
|
||||||
|
public final void doShutdown() {
|
||||||
|
apiEnvironment.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reboots the computer immediately.
|
||||||
|
*/
|
||||||
|
@LuaFunction("reboot")
|
||||||
|
public final void doReboot() {
|
||||||
|
apiEnvironment.reboot(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the computer.
|
||||||
|
*
|
||||||
|
* @return The ID of the computer.
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "getComputerID", "computerID" })
|
||||||
|
public final int getComputerID() {
|
||||||
|
return apiEnvironment.getComputerID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the label of the computer, or {@code nil} if none is set.
|
||||||
|
*
|
||||||
|
* @return The label of the computer.
|
||||||
|
* @cc.treturn string|nil The label of the computer.
|
||||||
|
* @cc.since 1.3
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "getComputerLabel", "computerLabel" })
|
||||||
|
public final Object[] getComputerLabel() {
|
||||||
|
String label = apiEnvironment.getComputerEnvironment().getLabel(getComputerID());
|
||||||
|
return label == null ? null : new Object[]{ label };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the label of this computer.
|
||||||
|
*
|
||||||
|
* @param label The new label. May be {@code nil} in order to clear it.
|
||||||
|
* @cc.since 1.3
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void setComputerLabel(Optional<String> label) {
|
||||||
|
apiEnvironment.getComputerEnvironment().setLabel(getComputerID(), label.map(StringUtil::normaliseLabel).orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of seconds that the computer has been running.
|
||||||
|
*
|
||||||
|
* @return The computer's uptime.
|
||||||
|
* @cc.since 1.2
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final double clock() {
|
||||||
|
return clock * 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current time depending on the string passed in. This will
|
||||||
|
* always be in the range [0.0, 24.0).
|
||||||
|
* <p>
|
||||||
|
* * If called with {@code ingame}, the current world time will be returned.
|
||||||
|
* This is the default if nothing is passed.
|
||||||
|
* * If called with {@code utc}, returns the hour of the day in UTC time.
|
||||||
|
* * If called with {@code local}, returns the hour of the day in the
|
||||||
|
* timezone the server is located in.
|
||||||
|
* <p>
|
||||||
|
* This function can also be called with a table returned from {@link #date},
|
||||||
|
* which will convert the date fields into a UNIX timestamp (number of
|
||||||
|
* seconds since 1 January 1970).
|
||||||
|
*
|
||||||
|
* @param args The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
|
||||||
|
* @return The hour of the selected locale, or a UNIX timestamp from the table, depending on the argument passed in.
|
||||||
|
* @throws LuaException If an invalid locale is passed.
|
||||||
|
* @cc.tparam [opt] string|table locale The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
|
||||||
|
* @cc.see textutils.formatTime To convert times into a user-readable string.
|
||||||
|
* @cc.usage Print the current in-game time.
|
||||||
|
* <pre>{@code
|
||||||
|
* textutils.formatTime(os.time())
|
||||||
|
* }</pre>
|
||||||
|
* @cc.since 1.2
|
||||||
|
* @cc.changed 1.80pr1 Add support for getting the local local and UTC time.
|
||||||
|
* @cc.changed 1.82.0 Arguments are now case insensitive.
|
||||||
|
* @cc.changed 1.83.0 {@link #time(IArguments)} now accepts table arguments and converts them to UNIX timestamps.
|
||||||
|
* @see #date To get a date table that can be converted with this function.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object time(IArguments args) throws LuaException {
|
||||||
|
Object value = args.get(0);
|
||||||
|
if (value instanceof Map) return LuaDateTime.fromTable((Map<?, ?>) value);
|
||||||
|
|
||||||
|
String param = args.optString(0, "ingame");
|
||||||
|
switch (param.toLowerCase(Locale.ROOT)) {
|
||||||
|
case "utc":
|
||||||
|
return getTimeForCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC")));
|
||||||
|
case "local":
|
||||||
|
return getTimeForCalendar(Calendar.getInstance());
|
||||||
|
case "ingame":
|
||||||
|
return time;
|
||||||
|
default:
|
||||||
|
throw new LuaException("Unsupported operation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the day depending on the locale specified.
|
||||||
|
* <p>
|
||||||
|
* * If called with {@code ingame}, returns the number of days since the
|
||||||
|
* world was created. This is the default.
|
||||||
|
* * If called with {@code utc}, returns the number of days since 1 January
|
||||||
|
* 1970 in the UTC timezone.
|
||||||
|
* * If called with {@code local}, returns the number of days since 1
|
||||||
|
* January 1970 in the server's local timezone.
|
||||||
|
*
|
||||||
|
* @param args The locale to get the day for. Defaults to {@code ingame} if not set.
|
||||||
|
* @return The day depending on the selected locale.
|
||||||
|
* @throws LuaException If an invalid locale is passed.
|
||||||
|
* @cc.since 1.48
|
||||||
|
* @cc.changed 1.82.0 Arguments are now case insensitive.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final int day(Optional<String> args) throws LuaException {
|
||||||
|
switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) {
|
||||||
|
case "utc":
|
||||||
|
return getDayForCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC")));
|
||||||
|
case "local":
|
||||||
|
return getDayForCalendar(Calendar.getInstance());
|
||||||
|
case "ingame":
|
||||||
|
return day;
|
||||||
|
default:
|
||||||
|
throw new LuaException("Unsupported operation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of milliseconds since an epoch depending on the locale.
|
||||||
|
* <p>
|
||||||
|
* * If called with {@code ingame}, returns the number of *in-game* milliseconds since the
|
||||||
|
* world was created. This is the default.
|
||||||
|
* * If called with {@code utc}, returns the number of milliseconds since 1
|
||||||
|
* January 1970 in the UTC timezone.
|
||||||
|
* * If called with {@code local}, returns the number of milliseconds since 1
|
||||||
|
* January 1970 in the server's local timezone.
|
||||||
|
* <p>
|
||||||
|
* :::info
|
||||||
|
* The {@code ingame} time zone assumes that one Minecraft day consists of 86,400,000
|
||||||
|
* milliseconds. Since one in-game day is much faster than a real day (20 minutes), this
|
||||||
|
* will change quicker than real time - one real second is equal to 72000 in-game
|
||||||
|
* milliseconds. If you wish to convert this value to real time, divide by 72000; to
|
||||||
|
* convert to ticks (where a day is 24000 ticks), divide by 3600.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* @param args The locale to get the milliseconds for. Defaults to {@code ingame} if not set.
|
||||||
|
* @return The milliseconds since the epoch depending on the selected locale.
|
||||||
|
* @throws LuaException If an invalid locale is passed.
|
||||||
|
* @cc.since 1.80pr1
|
||||||
|
* @cc.usage Get the current time and use {@link #date} to convert it to a table.
|
||||||
|
* <pre>{@code
|
||||||
|
* -- Dividing by 1000 converts it from milliseconds to seconds.
|
||||||
|
* local time = os.epoch("local") / 1000
|
||||||
|
* local time_table = os.date("*t", time)
|
||||||
|
* print(textutils.serialize(time_table))
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final long epoch(Optional<String> args) throws LuaException {
|
||||||
|
switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) {
|
||||||
|
case "utc": {
|
||||||
|
// Get utc epoch
|
||||||
|
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||||
|
return getEpochForCalendar(c);
|
||||||
|
}
|
||||||
|
case "local": {
|
||||||
|
// Get local epoch
|
||||||
|
Calendar c = Calendar.getInstance();
|
||||||
|
return getEpochForCalendar(c);
|
||||||
|
}
|
||||||
|
case "ingame":
|
||||||
|
// Get in-game epoch
|
||||||
|
synchronized (alarms) {
|
||||||
|
return day * 86400000L + (long) (time * 3600000.0);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new LuaException("Unsupported operation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a date string (or table) using a specified format string and
|
||||||
|
* optional time to format.
|
||||||
|
* <p>
|
||||||
|
* The format string takes the same formats as C's {@code strftime} function
|
||||||
|
* (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it
|
||||||
|
* can be prefixed with an exclamation mark ({@code !}) to use UTC time
|
||||||
|
* instead of the server's local timezone.
|
||||||
|
* <p>
|
||||||
|
* If the format is exactly {@code *t} (optionally prefixed with {@code !}), a
|
||||||
|
* table will be returned instead. This table has fields for the year, month,
|
||||||
|
* day, hour, minute, second, day of the week, day of the year, and whether
|
||||||
|
* Daylight Savings Time is in effect. This table can be converted to a UNIX
|
||||||
|
* timestamp (days since 1 January 1970) with {@link #date}.
|
||||||
|
*
|
||||||
|
* @param formatA The format of the string to return. This defaults to {@code %c}, which expands to a string similar to "Sat Dec 24 16:58:00 2011".
|
||||||
|
* @param timeA The time to convert to a string. This defaults to the current time.
|
||||||
|
* @return The resulting format string.
|
||||||
|
* @throws LuaException If an invalid format is passed.
|
||||||
|
* @cc.since 1.83.0
|
||||||
|
* @cc.usage Print the current date in a user-friendly string.
|
||||||
|
* <pre>{@code
|
||||||
|
* os.date("%A %d %B %Y") -- See the reference above!
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object date(Optional<String> formatA, Optional<Long> timeA) throws LuaException {
|
||||||
|
String format = formatA.orElse("%c");
|
||||||
|
long time = timeA.orElseGet(() -> Instant.now().getEpochSecond());
|
||||||
|
|
||||||
|
Instant instant = Instant.ofEpochSecond(time);
|
||||||
|
ZonedDateTime date;
|
||||||
|
ZoneOffset offset;
|
||||||
|
if (format.startsWith("!")) {
|
||||||
|
offset = ZoneOffset.UTC;
|
||||||
|
date = ZonedDateTime.ofInstant(instant, offset);
|
||||||
|
format = format.substring(1);
|
||||||
|
} else {
|
||||||
|
ZoneId id = ZoneId.systemDefault();
|
||||||
|
offset = id.getRules().getOffset(instant);
|
||||||
|
date = ZonedDateTime.ofInstant(instant, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format.equals("*t")) return LuaDateTime.toTable(date, offset, instant);
|
||||||
|
|
||||||
|
DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder();
|
||||||
|
LuaDateTime.format(formatter, format);
|
||||||
|
// ROOT would be more sensible, but US appears more consistent with the default C locale
|
||||||
|
// on Linux.
|
||||||
|
return formatter.toFormatter(Locale.US).format(date);
|
||||||
|
}
|
||||||
|
}
|
86
src/main/java/dan200/computercraft/core/apis/TermAPI.java
Normal file
86
src/main/java/dan200/computercraft/core/apis/TermAPI.java
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import dan200.computer.core.IAPIEnvironment;
|
||||||
|
import dan200.computer.core.IComputerEnvironment;
|
||||||
|
import dan200.computer.core.ILuaAPI;
|
||||||
|
import dan200.computer.core.Terminal;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.util.Colour;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interact with a computer's terminal or monitors, writing text and drawing
|
||||||
|
* ASCII graphics.
|
||||||
|
*
|
||||||
|
* @cc.module term
|
||||||
|
*/
|
||||||
|
public class TermAPI extends TermMethods implements ILuaAPI {
|
||||||
|
private final Terminal terminal;
|
||||||
|
private final IComputerEnvironment environment;
|
||||||
|
|
||||||
|
public TermAPI(IAPIEnvironment environment) {
|
||||||
|
this.environment = environment.getComputerEnvironment();
|
||||||
|
terminal = environment.getTerminal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getNames() {
|
||||||
|
return new String[]{ "term" };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startup() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void advance(double v) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getMethodNames() {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] callMethod(int i, Object[] objects) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default palette value for a colour.
|
||||||
|
*
|
||||||
|
* @param colour The colour whose palette should be fetched.
|
||||||
|
* @return The RGB values.
|
||||||
|
* @throws LuaException When given an invalid colour.
|
||||||
|
* @cc.treturn number The red channel, will be between 0 and 1.
|
||||||
|
* @cc.treturn number The green channel, will be between 0 and 1.
|
||||||
|
* @cc.treturn number The blue channel, will be between 0 and 1.
|
||||||
|
* @cc.since 1.81.0
|
||||||
|
* @see TermMethods#setPaletteColour(IArguments) To change the palette colour.
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "nativePaletteColour", "nativePaletteColor" })
|
||||||
|
public final Object[] nativePaletteColour(int colour) throws LuaException {
|
||||||
|
Colour c = Colour.fromInt(parseColour(colour));
|
||||||
|
return new Object[]{ c.getR(), c.getG(), c.getB() };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isColour() {
|
||||||
|
return environment.isColour();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Terminal getTerminal() {
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
}
|
398
src/main/java/dan200/computercraft/core/apis/TermMethods.java
Normal file
398
src/main/java/dan200/computercraft/core/apis/TermMethods.java
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis;
|
||||||
|
|
||||||
|
import dan200.computer.core.Terminal;
|
||||||
|
import dan200.computercraft.api.lua.Coerced;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.util.Colour;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for all objects which interact with a terminal. Namely the {@link TermAPI} and monitors.
|
||||||
|
*
|
||||||
|
* @cc.module term.Redirect
|
||||||
|
*/
|
||||||
|
public abstract class TermMethods {
|
||||||
|
private static int getHighestBit(int group) {
|
||||||
|
// Equivalent to log2(group) - 1.
|
||||||
|
return 32 - Integer.numberOfLeadingZeros(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean isColour();
|
||||||
|
|
||||||
|
protected abstract Terminal getTerminal() throws LuaException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write {@code text} at the current cursor position, moving the cursor to the end of the text.
|
||||||
|
* <p>
|
||||||
|
* Unlike functions like {@code write} and {@code print}, this does not wrap the text - it simply copies the
|
||||||
|
* text to the current terminal line.
|
||||||
|
*
|
||||||
|
* @param textA The text to write.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void write(Coerced<String> textA) throws LuaException {
|
||||||
|
String text = textA.value();
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.write(text);
|
||||||
|
terminal.setCursorPos(terminal.getCursorX() + text.length(), terminal.getCursorY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move all positions up (or down) by {@code y} pixels.
|
||||||
|
* <p>
|
||||||
|
* Every pixel in the terminal will be replaced by the line {@code y} pixels below it. If {@code y} is negative, it
|
||||||
|
* will copy pixels from above instead.
|
||||||
|
*
|
||||||
|
* @param y The number of lines to move up by. This may be a negative number.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void scroll(int y) throws LuaException {
|
||||||
|
getTerminal().scroll(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the position of the cursor.
|
||||||
|
*
|
||||||
|
* @return The cursor's position.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.treturn number The x position of the cursor.
|
||||||
|
* @cc.treturn number The y position of the cursor.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] getCursorPos() throws LuaException {
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
return new Object[]{ terminal.getCursorX() + 1, terminal.getCursorY() + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the position of the cursor. {@link #write(Coerced) terminal writes} will begin from this position.
|
||||||
|
*
|
||||||
|
* @param x The new x position of the cursor.
|
||||||
|
* @param y The new y position of the cursor.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void setCursorPos(int x, int y) throws LuaException {
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.setCursorPos(x - 1, y - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the cursor is currently blinking.
|
||||||
|
*
|
||||||
|
* @return If the cursor is blinking.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.since 1.80pr1.9
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final boolean getCursorBlink() throws LuaException {
|
||||||
|
return getTerminal().getCursorBlink();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the cursor should be visible (and blinking) at the current {@link #getCursorPos() cursor position}.
|
||||||
|
*
|
||||||
|
* @param blink Whether the cursor should blink.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void setCursorBlink(boolean blink) throws LuaException {
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.setCursorBlink(blink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the terminal.
|
||||||
|
*
|
||||||
|
* @return The terminal's size.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.treturn number The terminal's width.
|
||||||
|
* @cc.treturn number The terminal's height.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] getSize() throws LuaException {
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
return new Object[]{ terminal.getWidth(), terminal.getHeight() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the terminal, filling it with the {@link #getBackgroundColour() current background colour}.
|
||||||
|
*
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void clear() throws LuaException {
|
||||||
|
getTerminal().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the line the cursor is currently on, filling it with the {@link #getBackgroundColour() current background
|
||||||
|
* colour}.
|
||||||
|
*
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void clearLine() throws LuaException {
|
||||||
|
getTerminal().clearLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the colour that new text will be written as.
|
||||||
|
*
|
||||||
|
* @return The current text colour.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.see colors For a list of colour constants, returned by this function.
|
||||||
|
* @cc.since 1.74
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "getTextColour", "getTextColor" })
|
||||||
|
public final int getTextColour() throws LuaException {
|
||||||
|
return encodeColour(getTerminal().getTextColour());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the colour that new text will be written as.
|
||||||
|
*
|
||||||
|
* @param colourArg The new text colour.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.see colors For a list of colour constants.
|
||||||
|
* @cc.since 1.45
|
||||||
|
* @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen.
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "setTextColour", "setTextColor" })
|
||||||
|
public final void setTextColour(int colourArg) throws LuaException {
|
||||||
|
int colour = parseColour(colourArg);
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.setTextColour(colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current background colour. This is used when {@link #write writing text} and {@link #clear clearing}
|
||||||
|
* the terminal.
|
||||||
|
*
|
||||||
|
* @return The current background colour.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.see colors For a list of colour constants, returned by this function.
|
||||||
|
* @cc.since 1.74
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "getBackgroundColour", "getBackgroundColor" })
|
||||||
|
public final int getBackgroundColour() throws LuaException {
|
||||||
|
return encodeColour(getTerminal().getBackgroundColour());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} the
|
||||||
|
* terminal.
|
||||||
|
*
|
||||||
|
* @param colourArg The new background colour.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.see colors For a list of colour constants.
|
||||||
|
* @cc.since 1.45
|
||||||
|
* @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen.
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "setBackgroundColour", "setBackgroundColor" })
|
||||||
|
public final void setBackgroundColour(int colourArg) throws LuaException {
|
||||||
|
int colour = parseColour(colourArg);
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.setBackgroundColour(colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this terminal supports colour.
|
||||||
|
* <p>
|
||||||
|
* Terminals which do not support colour will still allow writing coloured text/backgrounds, but it will be
|
||||||
|
* displayed in greyscale.
|
||||||
|
*
|
||||||
|
* @return Whether this terminal supports colour.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.since 1.45
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "isColour", "isColor" })
|
||||||
|
public final boolean getIsColour() throws LuaException {
|
||||||
|
return isColour();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes {@code text} to the terminal with the specific foreground and background colours.
|
||||||
|
* <p>
|
||||||
|
* As with {@link #write(Coerced)}, the text will be written at the current cursor location, with the cursor
|
||||||
|
* moving to the end of the text.
|
||||||
|
* <p>
|
||||||
|
* {@code textColour} and {@code backgroundColour} must both be strings the same length as {@code text}. All
|
||||||
|
* characters represent a single hexadecimal digit, which is converted to one of CC's colours. For instance,
|
||||||
|
* {@code "a"} corresponds to purple.
|
||||||
|
*
|
||||||
|
* @param text The text to write.
|
||||||
|
* @param textColour The corresponding text colours.
|
||||||
|
* @param backgroundColour The corresponding background colours.
|
||||||
|
* @throws LuaException If the three inputs are not the same length.
|
||||||
|
* @cc.see colors For a list of colour constants, and their hexadecimal values.
|
||||||
|
* @cc.since 1.74
|
||||||
|
* @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen.
|
||||||
|
* @cc.usage Prints "Hello, world!" in rainbow text.
|
||||||
|
* <pre>{@code
|
||||||
|
* term.blit("Hello, world!","01234456789ab","0000000000000")
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void blit(Coerced<String> text, Coerced<String> textColour, Coerced<String> backgroundColour) throws LuaException {
|
||||||
|
if (textColour.value().length() != text.value().length() || backgroundColour.value().length() != text.value().length()) {
|
||||||
|
throw new LuaException("Arguments must be the same length");
|
||||||
|
}
|
||||||
|
|
||||||
|
Terminal terminal = getTerminal();
|
||||||
|
synchronized (terminal) {
|
||||||
|
blit(terminal, text.value(), textColour.value(), backgroundColour.value());
|
||||||
|
terminal.setCursorPos(terminal.getCursorX() + text.value().length(), terminal.getCursorY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the palette for a specific colour.
|
||||||
|
* <p>
|
||||||
|
* ComputerCraft's palette system allows you to change how a specific colour should be displayed. For instance, you
|
||||||
|
* can make @{colors.red} <em>more red</em> by setting its palette to #FF0000. This does now allow you to draw more
|
||||||
|
* colours - you are still limited to 16 on the screen at one time - but you can change <em>which</em> colours are
|
||||||
|
* used.
|
||||||
|
*
|
||||||
|
* @param args The new palette values.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.tparam [1] number index The colour whose palette should be changed.
|
||||||
|
* @cc.tparam number colour A 24-bit integer representing the RGB value of the colour. For instance the integer
|
||||||
|
* `0xFF0000` corresponds to the colour #FF0000.
|
||||||
|
* @cc.tparam [2] number index The colour whose palette should be changed.
|
||||||
|
* @cc.tparam number r The intensity of the red channel, between 0 and 1.
|
||||||
|
* @cc.tparam number g The intensity of the green channel, between 0 and 1.
|
||||||
|
* @cc.tparam number b The intensity of the blue channel, between 0 and 1.
|
||||||
|
* @cc.usage Change the @{colors.red|red colour} from the default #CC4C4C to #FF0000.
|
||||||
|
* <pre>{@code
|
||||||
|
* term.setPaletteColour(colors.red, 0xFF0000)
|
||||||
|
* term.setTextColour(colors.red)
|
||||||
|
* print("Hello, world!")
|
||||||
|
* }</pre>
|
||||||
|
* @cc.usage As above, but specifying each colour channel separately.
|
||||||
|
* <pre>{@code
|
||||||
|
* term.setPaletteColour(colors.red, 1, 0, 0)
|
||||||
|
* term.setTextColour(colors.red)
|
||||||
|
* print("Hello, world!")
|
||||||
|
* }</pre>
|
||||||
|
* @cc.see colors.unpackRGB To convert from the 24-bit format to three separate channels.
|
||||||
|
* @cc.see colors.packRGB To convert from three separate channels to the 24-bit format.
|
||||||
|
* @cc.since 1.80pr1
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "setPaletteColour", "setPaletteColor" })
|
||||||
|
public final void setPaletteColour(IArguments args) throws LuaException {
|
||||||
|
// No-op
|
||||||
|
parseColour(args.getInt(0));
|
||||||
|
if (args.count() == 2) {
|
||||||
|
args.getInt(1);
|
||||||
|
} else {
|
||||||
|
args.getFiniteDouble(1);
|
||||||
|
args.getFiniteDouble(2);
|
||||||
|
args.getFiniteDouble(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current palette for a specific colour.
|
||||||
|
*
|
||||||
|
* @param colour The colour whose palette should be fetched.
|
||||||
|
* @return The resulting colour.
|
||||||
|
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||||
|
* @cc.treturn number The red channel, will be between 0 and 1.
|
||||||
|
* @cc.treturn number The green channel, will be between 0 and 1.
|
||||||
|
* @cc.treturn number The blue channel, will be between 0 and 1.
|
||||||
|
* @cc.since 1.80pr1
|
||||||
|
*/
|
||||||
|
@LuaFunction({ "getPaletteColour", "getPaletteColor" })
|
||||||
|
public final Object[] getPaletteColour(int colour) throws LuaException {
|
||||||
|
Colour c = Colour.fromInt(parseColour(colour));
|
||||||
|
return new Object[]{ c.getR(), c.getG(), c.getB() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int parseColour(int colour) throws LuaException {
|
||||||
|
if (colour <= 0) throw new LuaException("Colour out of range");
|
||||||
|
colour = 16 - getHighestBit(colour);
|
||||||
|
if (colour < 0 || colour > 15) throw new LuaException("Colour out of range");
|
||||||
|
return colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int encodeColour(int colour) {
|
||||||
|
return 1 << (15 - colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void blit(Terminal term, String text, String foreground, String background) {
|
||||||
|
if (term.getCursorY() < 0 || term.getCursorY() >= term.getHeight()) return;
|
||||||
|
int writeX = term.getCursorX();
|
||||||
|
int spaceLeft = term.getWidth() - term.getCursorX();
|
||||||
|
if (spaceLeft > term.getWidth() + text.length()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spaceLeft > term.getWidth()) {
|
||||||
|
writeX = 0;
|
||||||
|
text = text.substring(spaceLeft - term.getWidth());
|
||||||
|
spaceLeft = term.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replace('\t', ' ');
|
||||||
|
if (spaceLeft > 0) {
|
||||||
|
String oldLine = term.getLine(term.getCursorY());
|
||||||
|
String oldColourLine = term.getColourLine(term.getCursorY());
|
||||||
|
String oldTextLine = oldColourLine.substring(0, oldLine.length());
|
||||||
|
String oldBackgroundLine = oldColourLine.substring(oldLine.length(), 2 * oldLine.length());
|
||||||
|
StringBuilder newLine = new StringBuilder();
|
||||||
|
StringBuilder newTextLine = new StringBuilder();
|
||||||
|
StringBuilder newBackgroundLine = new StringBuilder();
|
||||||
|
newLine.append(oldLine, 0, writeX);
|
||||||
|
newTextLine.append(oldTextLine, 0, writeX);
|
||||||
|
newBackgroundLine.append(oldBackgroundLine, 0, writeX);
|
||||||
|
if (text.length() < spaceLeft) {
|
||||||
|
newLine.append(text);
|
||||||
|
for (int i = 0; i < text.length(); i++) newTextLine.append(foreground.charAt(i));
|
||||||
|
for (int i = 0; i < text.length(); i++) newBackgroundLine.append(remapColour(background.charAt(i)));
|
||||||
|
|
||||||
|
newLine.append(oldLine.substring(writeX + text.length()));
|
||||||
|
newTextLine.append(oldTextLine.substring(writeX + text.length()));
|
||||||
|
newBackgroundLine.append(oldBackgroundLine.substring(writeX + text.length()));
|
||||||
|
} else {
|
||||||
|
newLine.append(text, 0, spaceLeft);
|
||||||
|
for (int i = 0; i < spaceLeft; i++) newTextLine.append(remapColour(foreground.charAt(i)));
|
||||||
|
for (int i = 0; i < spaceLeft; i++) newBackgroundLine.append(remapColour(background.charAt(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
term.setLine(term.getCursorY(), newLine.toString(), newTextLine.append(newBackgroundLine).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String COLOURS = "0123456789abcdef";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remap a blit character to use the older format.
|
||||||
|
*
|
||||||
|
* @param c The colour to remap
|
||||||
|
* @return The new character.
|
||||||
|
*/
|
||||||
|
private static char remapColour(char c) {
|
||||||
|
if (c >= '0' && c <= '9') return COLOURS.charAt(15 - (c - '0'));
|
||||||
|
if (c >= 'a' && c <= 'f') return COLOURS.charAt(15 - (c - 'a' + 10));
|
||||||
|
if (c >= 'A' && c <= 'F') return COLOURS.charAt(15 - (c - 'A' + 10));
|
||||||
|
return ' ';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis.handles;
|
||||||
|
|
||||||
|
import dan200.computer.core.IMountedFileBinary;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.core.apis.FSAPI;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file handle opened with {@link FSAPI#open(String, String)} with the {@code "rb"}
|
||||||
|
* mode.
|
||||||
|
*
|
||||||
|
* @cc.module fs.BinaryReadHandle
|
||||||
|
*/
|
||||||
|
public class BinaryReadableHandle extends HandleGeneric {
|
||||||
|
private final IMountedFileBinary channel;
|
||||||
|
|
||||||
|
public BinaryReadableHandle(IMountedFileBinary channel) {
|
||||||
|
super(channel);
|
||||||
|
this.channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a number of bytes from this file.
|
||||||
|
*
|
||||||
|
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
|
||||||
|
* may be 0 to determine we are at the end of the file.
|
||||||
|
* @return The read bytes.
|
||||||
|
* @throws LuaException When trying to read a negative number of bytes.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
* @cc.treturn [1] nil If we are at the end of the file.
|
||||||
|
* @cc.treturn [2] number The value of the byte read. This is returned when the {@code count} is absent.
|
||||||
|
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
|
||||||
|
* @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] read(Optional<Integer> countArg) throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
try {
|
||||||
|
if (countArg.isPresent()) {
|
||||||
|
int count = countArg.get();
|
||||||
|
if (count < 0) throw new LuaException("Cannot read a negative number of bytes");
|
||||||
|
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
boolean readAnything = false;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int r = channel.read();
|
||||||
|
if (r == -1) break;
|
||||||
|
|
||||||
|
readAnything = true;
|
||||||
|
stream.write(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return readAnything ? new Object[]{ stream.toByteArray() } : null;
|
||||||
|
} else {
|
||||||
|
int b = channel.read();
|
||||||
|
return b == -1 ? null : new Object[]{ b };
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the remainder of the file.
|
||||||
|
*
|
||||||
|
* @return The file, or {@code null} if at the end of it.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
|
||||||
|
* @cc.since 1.80pr1
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] readAll() throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
try {
|
||||||
|
int expected = 32;
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream(expected);
|
||||||
|
|
||||||
|
boolean readAnything = false;
|
||||||
|
while (true) {
|
||||||
|
int r = channel.read();
|
||||||
|
if (r == -1) break;
|
||||||
|
|
||||||
|
readAnything = true;
|
||||||
|
stream.write(r);
|
||||||
|
}
|
||||||
|
return readAnything ? new Object[]{ stream.toByteArray() } : null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a line from the file.
|
||||||
|
*
|
||||||
|
* @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
|
||||||
|
* @return The read string.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||||
|
* @cc.since 1.80pr1.9
|
||||||
|
* @cc.changed 1.81.0 `\r` is now stripped.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
boolean withTrailing = withTrailingArg.orElse(false);
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
boolean readAnything = false, readRc = false;
|
||||||
|
while (true) {
|
||||||
|
int read = channel.read();
|
||||||
|
if (read < 0) {
|
||||||
|
// Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
|
||||||
|
// back.
|
||||||
|
if (readRc) stream.write('\r');
|
||||||
|
return readAnything ? new Object[]{ stream.toByteArray() } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readAnything = true;
|
||||||
|
|
||||||
|
if (read == '\n') {
|
||||||
|
if (withTrailing) {
|
||||||
|
if (readRc) stream.write('\r');
|
||||||
|
stream.write(read);
|
||||||
|
}
|
||||||
|
return new Object[]{ stream.toByteArray() };
|
||||||
|
} else {
|
||||||
|
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n.
|
||||||
|
// Note, this behaviour is non-standard compliant (strictly speaking we should have no
|
||||||
|
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and
|
||||||
|
// previous behaviour of the io library.
|
||||||
|
if (readRc) stream.write('\r');
|
||||||
|
readRc = read == '\r';
|
||||||
|
if (!readRc) stream.write(read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis.handles;
|
||||||
|
|
||||||
|
import dan200.computer.core.IMountedFileBinary;
|
||||||
|
import dan200.computercraft.api.lua.ArgumentHelper;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.core.apis.FSAPI;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file handle opened by {@link FSAPI#open} using the {@code "wb"} or {@code "ab"}
|
||||||
|
* modes.
|
||||||
|
*
|
||||||
|
* @cc.module fs.BinaryWriteHandle
|
||||||
|
*/
|
||||||
|
public class BinaryWritableHandle extends HandleGeneric {
|
||||||
|
private final IMountedFileBinary channel;
|
||||||
|
|
||||||
|
public BinaryWritableHandle(IMountedFileBinary channel) {
|
||||||
|
super(channel);
|
||||||
|
this.channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a string or byte to the file.
|
||||||
|
*
|
||||||
|
* @param arguments The value to write.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
* @cc.tparam [1] number charcode The byte to write.
|
||||||
|
* @cc.tparam [2] string contents The string to write.
|
||||||
|
* @cc.changed 1.80pr1 Now accepts a string to write multiple bytes.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void write(IArguments arguments) throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
try {
|
||||||
|
Object arg = arguments.get(0);
|
||||||
|
if (arg instanceof Number) {
|
||||||
|
int number = ((Number) arg).intValue();
|
||||||
|
channel.write(number);
|
||||||
|
} else if (arg instanceof String) {
|
||||||
|
ByteBuffer contents = arguments.getBytes(0);
|
||||||
|
for (int i = contents.position(), length = contents.capacity(); i < length; i++) {
|
||||||
|
channel.write(contents.get(i));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ArgumentHelper.badArgumentOf(0, "string or number", arg);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current file without closing it.
|
||||||
|
*
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void flush() throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
try {
|
||||||
|
channel.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis.handles;
|
||||||
|
|
||||||
|
import dan200.computer.core.IMountedFileNormal;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.core.apis.FSAPI;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file handle opened with {@link FSAPI#open(String, String)} with the {@code "r"}
|
||||||
|
* mode.
|
||||||
|
*
|
||||||
|
* @cc.module fs.ReadHandle
|
||||||
|
*/
|
||||||
|
public class EncodedReadableHandle extends HandleGeneric {
|
||||||
|
private final IMountedFileNormal reader;
|
||||||
|
|
||||||
|
public EncodedReadableHandle(IMountedFileNormal reader) {
|
||||||
|
super(reader);
|
||||||
|
this.reader = reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a line from the file.
|
||||||
|
*
|
||||||
|
* @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
|
||||||
|
* @return The read string.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||||
|
* @cc.changed 1.81.0 Added option to return trailing newline.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
boolean withTrailing = withTrailingArg.orElse(false);
|
||||||
|
try {
|
||||||
|
String line = reader.readLine();
|
||||||
|
if (line != null) {
|
||||||
|
// While this is technically inaccurate, it's better than nothing
|
||||||
|
if (withTrailing) line += "\n";
|
||||||
|
return new Object[]{ line };
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the remainder of the file.
|
||||||
|
*
|
||||||
|
* @return The file, or {@code null} if at the end of it.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final Object[] readAll() throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
try {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
String line = reader.readLine();
|
||||||
|
while (line != null) {
|
||||||
|
result.append(line);
|
||||||
|
line = reader.readLine();
|
||||||
|
if (line != null) {
|
||||||
|
result.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Object[]{ result.toString() };
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis.handles;
|
||||||
|
|
||||||
|
import dan200.computer.core.IMountedFileNormal;
|
||||||
|
import dan200.computercraft.api.lua.Coerced;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.core.apis.FSAPI;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file handle opened by {@link FSAPI#open} using the {@code "w"} or {@code "a"} modes.
|
||||||
|
*
|
||||||
|
* @cc.module fs.WriteHandle
|
||||||
|
*/
|
||||||
|
public class EncodedWritableHandle extends HandleGeneric {
|
||||||
|
private final IMountedFileNormal writer;
|
||||||
|
|
||||||
|
public EncodedWritableHandle(IMountedFileNormal writer) {
|
||||||
|
super(writer);
|
||||||
|
this.writer = writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a string of characters to the file.
|
||||||
|
*
|
||||||
|
* @param textA The text to write to the file.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void write(Coerced<String> textA) throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
String text = textA.value();
|
||||||
|
try {
|
||||||
|
writer.write(text, 0, text.length(), false);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a string of characters to the file, following them with a new line character.
|
||||||
|
*
|
||||||
|
* @param textA The text to write to the file.
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void writeLine(Coerced<String> textA) throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
String text = textA.value();
|
||||||
|
try {
|
||||||
|
writer.write(text, 0, text.length(), true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current file without closing it.
|
||||||
|
*
|
||||||
|
* @throws LuaException If the file has been closed.
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void flush() throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
try {
|
||||||
|
writer.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new LuaException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.apis.handles;
|
||||||
|
|
||||||
|
import dan200.computer.core.IMountedFile;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public abstract class HandleGeneric {
|
||||||
|
private IMountedFile closeable;
|
||||||
|
|
||||||
|
protected HandleGeneric(IMountedFile closeable) {
|
||||||
|
this.closeable = closeable;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void checkOpen() throws LuaException {
|
||||||
|
if (closeable == null) throw new LuaException("attempt to use a closed file");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void close() {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
closeable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close this file, freeing any resources it uses.
|
||||||
|
* <p>
|
||||||
|
* Once a file is closed it may no longer be read or written to.
|
||||||
|
*
|
||||||
|
* @throws LuaException If the file has already been closed.
|
||||||
|
*/
|
||||||
|
@LuaFunction("close")
|
||||||
|
public final void doClose() throws LuaException {
|
||||||
|
checkOpen();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
|
||||||
|
final class DeclaringClassLoader extends ClassLoader {
|
||||||
|
static final DeclaringClassLoader INSTANCE = new DeclaringClassLoader();
|
||||||
|
|
||||||
|
private DeclaringClassLoader() {
|
||||||
|
super(DeclaringClassLoader.class.getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> define(String name, byte[] bytes, ProtectionDomain protectionDomain) throws ClassFormatError {
|
||||||
|
return defineClass(name, bytes, 0, bytes.length, protectionDomain);
|
||||||
|
}
|
||||||
|
}
|
335
src/main/java/dan200/computercraft/core/asm/Generator.java
Normal file
335
src/main/java/dan200/computercraft/core/asm/Generator.java
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
import cc.tweaked.CCTweaked;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
import com.google.common.cache.CacheLoader;
|
||||||
|
import com.google.common.cache.LoadingCache;
|
||||||
|
import com.google.common.primitives.Primitives;
|
||||||
|
import com.google.common.reflect.TypeToken;
|
||||||
|
import dan200.computercraft.api.lua.Coerced;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static org.objectweb.asm.Opcodes.*;
|
||||||
|
|
||||||
|
public final class Generator<T> {
|
||||||
|
private static final AtomicInteger METHOD_ID = new AtomicInteger();
|
||||||
|
|
||||||
|
private static final String METHOD_NAME = "apply";
|
||||||
|
private static final String[] EXCEPTIONS = new String[]{ Type.getInternalName(LuaException.class) };
|
||||||
|
|
||||||
|
private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(Object[].class);
|
||||||
|
private static final String DESC_METHOD_RESULT = Type.getDescriptor(Object[].class);
|
||||||
|
|
||||||
|
private static final String INTERNAL_ARGUMENTS = Type.getInternalName(IArguments.class);
|
||||||
|
private static final String DESC_ARGUMENTS = Type.getDescriptor(IArguments.class);
|
||||||
|
|
||||||
|
private static final String INTERNAL_COERCED = Type.getInternalName(Coerced.class);
|
||||||
|
|
||||||
|
private final Class<T> base;
|
||||||
|
private final List<Class<?>> context;
|
||||||
|
|
||||||
|
private final String[] interfaces;
|
||||||
|
private final String methodDesc;
|
||||||
|
|
||||||
|
private final Function<T, T> wrap;
|
||||||
|
|
||||||
|
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
|
||||||
|
.newBuilder()
|
||||||
|
.build(CacheLoader.from(catching(this::build, Collections.emptyList())));
|
||||||
|
|
||||||
|
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
|
||||||
|
.newBuilder()
|
||||||
|
.build(CacheLoader.from(catching(this::build, Optional.empty())));
|
||||||
|
|
||||||
|
Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) {
|
||||||
|
this.base = base;
|
||||||
|
this.context = context;
|
||||||
|
interfaces = new String[]{ Type.getInternalName(base) };
|
||||||
|
this.wrap = wrap;
|
||||||
|
|
||||||
|
StringBuilder methodDesc = new StringBuilder().append("(Ljava/lang/Object;");
|
||||||
|
for (Class<?> klass : context) methodDesc.append(Type.getDescriptor(klass));
|
||||||
|
methodDesc.append(DESC_ARGUMENTS).append(")").append(DESC_METHOD_RESULT);
|
||||||
|
this.methodDesc = methodDesc.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NamedMethod<T>> getMethods(Class<?> klass) {
|
||||||
|
try {
|
||||||
|
return classCache.get(klass);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
CCTweaked.LOG.log(Level.SEVERE, "Error getting methods for " + klass.getName() + ".", e.getCause());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NamedMethod<T>> build(Class<?> klass) {
|
||||||
|
ArrayList<NamedMethod<T>> methods = null;
|
||||||
|
for (Method method : klass.getMethods()) {
|
||||||
|
LuaFunction annotation = method.getAnnotation(LuaFunction.class);
|
||||||
|
if (annotation == null) continue;
|
||||||
|
|
||||||
|
if (Modifier.isStatic(method.getModifiers())) {
|
||||||
|
CCTweaked.LOG.warning(String.format("LuaFunction method %s.%s should be an instance method.", method.getDeclaringClass(), method.getName()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
T instance = methodCache.getUnchecked(method).orElse(null);
|
||||||
|
if (instance == null) continue;
|
||||||
|
|
||||||
|
if (methods == null) methods = new ArrayList<>();
|
||||||
|
addMethod(methods, method, annotation, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (methods == null) return Collections.emptyList();
|
||||||
|
methods.trimToSize();
|
||||||
|
return Collections.unmodifiableList(methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMethod(List<NamedMethod<T>> methods, Method method, LuaFunction annotation, T instance) {
|
||||||
|
String[] names = annotation.value();
|
||||||
|
boolean isSimple = true;
|
||||||
|
if (names.length == 0) {
|
||||||
|
methods.add(new NamedMethod<>(method.getName(), instance, isSimple));
|
||||||
|
} else {
|
||||||
|
for (String name : names) {
|
||||||
|
methods.add(new NamedMethod<>(name, instance, isSimple));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<T> build(Method method) {
|
||||||
|
String name = method.getDeclaringClass().getName() + "." + method.getName();
|
||||||
|
int modifiers = method.getModifiers();
|
||||||
|
|
||||||
|
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
|
||||||
|
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) {
|
||||||
|
CCTweaked.LOG.warning(String.format("Lua Method %s should be final.", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Modifier.isPublic(modifiers)) {
|
||||||
|
CCTweaked.LOG.severe(String.format("Lua Method %s should be a public method.", name));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
|
||||||
|
CCTweaked.LOG.warning(String.format("Lua Method %s should be on a public class.", name));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
CCTweaked.LOG.fine(String.format("Generating method wrapper for %s.", name));
|
||||||
|
|
||||||
|
Class<?>[] exceptions = method.getExceptionTypes();
|
||||||
|
for (Class<?> exception : exceptions) {
|
||||||
|
if (exception != LuaException.class) {
|
||||||
|
CCTweaked.LOG.warning(String.format("Lua Method %s cannot throw %s.", name, exception.getName()));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaFunction annotation = method.getAnnotation(LuaFunction.class);
|
||||||
|
if (annotation.unsafe() && annotation.mainThread()) {
|
||||||
|
CCTweaked.LOG.severe(String.format("Lua Method %s cannot use unsafe and mainThread", name));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have some rather ugly handling of static methods in both here and the main generate function. Static methods
|
||||||
|
// only come from generic sources, so this should be safe.
|
||||||
|
Class<?> target = Modifier.isStatic(modifiers) ? method.getParameterTypes()[0] : method.getDeclaringClass();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement();
|
||||||
|
byte[] bytes = generate(className, target, method, annotation.unsafe());
|
||||||
|
if (bytes == null) return Optional.empty();
|
||||||
|
|
||||||
|
Class<?> klass = DeclaringClassLoader.INSTANCE.define(className, bytes, method.getDeclaringClass().getProtectionDomain());
|
||||||
|
|
||||||
|
T instance = klass.asSubclass(base).getDeclaredConstructor().newInstance();
|
||||||
|
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
|
||||||
|
} catch (ReflectiveOperationException | ClassFormatError | RuntimeException e) {
|
||||||
|
CCTweaked.LOG.log(Level.SEVERE, String.format("Error generating wrapper for %s.", name), e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generate(String className, Class<?> target, Method method, boolean unsafe) {
|
||||||
|
String internalName = className.replace(".", "/");
|
||||||
|
|
||||||
|
// Construct a public final class which extends Object and implements MethodInstance.Delegate
|
||||||
|
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||||
|
cw.visit(V1_6, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces);
|
||||||
|
cw.visitSource("CC generated method", null);
|
||||||
|
|
||||||
|
{ // Constructor just invokes super.
|
||||||
|
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||||
|
mw.visitCode();
|
||||||
|
mw.visitVarInsn(ALOAD, 0);
|
||||||
|
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
|
||||||
|
mw.visitInsn(RETURN);
|
||||||
|
mw.visitMaxs(0, 0);
|
||||||
|
mw.visitEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS);
|
||||||
|
mw.visitCode();
|
||||||
|
|
||||||
|
// If we're an instance method, load the this parameter.
|
||||||
|
if (!Modifier.isStatic(method.getModifiers())) {
|
||||||
|
mw.visitVarInsn(ALOAD, 1);
|
||||||
|
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
int argIndex = 0;
|
||||||
|
for (java.lang.reflect.Type genericArg : method.getGenericParameterTypes()) {
|
||||||
|
Boolean loadedArg = loadArg(mw, target, method, unsafe, genericArg, argIndex);
|
||||||
|
if (loadedArg == null) return null;
|
||||||
|
if (loadedArg) argIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.visitMethodInsn(
|
||||||
|
Modifier.isStatic(method.getModifiers()) ? INVOKESTATIC : INVOKEVIRTUAL,
|
||||||
|
Type.getInternalName(method.getDeclaringClass()), method.getName(),
|
||||||
|
Type.getMethodDescriptor(method)
|
||||||
|
);
|
||||||
|
|
||||||
|
// We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult,
|
||||||
|
// we convert basic types into an immediate result.
|
||||||
|
Class<?> ret = method.getReturnType();
|
||||||
|
if (ret != Object[].class) {
|
||||||
|
if (ret == void.class) {
|
||||||
|
mw.visitMethodInsn(INVOKESTATIC, "dan200/computercraft/core/asm/Support", "of", "()" + DESC_METHOD_RESULT);
|
||||||
|
} else if (ret.isPrimitive()) {
|
||||||
|
Class<?> boxed = Primitives.wrap(ret);
|
||||||
|
mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(boxed), "valueOf", "(" + Type.getDescriptor(ret) + ")" + Type.getDescriptor(boxed));
|
||||||
|
mw.visitMethodInsn(INVOKESTATIC, "dan200/computercraft/core/asm/Support", "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT);
|
||||||
|
} else {
|
||||||
|
mw.visitMethodInsn(INVOKESTATIC, "dan200/computercraft/core/asm/Support", "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.visitInsn(ARETURN);
|
||||||
|
|
||||||
|
mw.visitMaxs(0, 0);
|
||||||
|
mw.visitEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
cw.visitEnd();
|
||||||
|
|
||||||
|
return cw.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean loadArg(MethodVisitor mw, Class<?> target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex) {
|
||||||
|
if (genericArg == target) {
|
||||||
|
mw.visitVarInsn(ALOAD, 1);
|
||||||
|
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> arg = Reflect.getRawType(method, genericArg, true);
|
||||||
|
if (arg == null) return null;
|
||||||
|
|
||||||
|
if (arg == IArguments.class) {
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = context.indexOf(arg);
|
||||||
|
if (idx >= 0) {
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg == Coerced.class) {
|
||||||
|
Class<?> klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.COERCED_IN).getType(), false);
|
||||||
|
if (klass == null) return null;
|
||||||
|
|
||||||
|
if (klass == String.class) {
|
||||||
|
mw.visitTypeInsn(NEW, INTERNAL_COERCED);
|
||||||
|
mw.visitInsn(DUP);
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||||
|
Reflect.loadInt(mw, argIndex);
|
||||||
|
mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;");
|
||||||
|
mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "<init>", "(Ljava/lang/Object;)V");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg == Optional.class) {
|
||||||
|
Class<?> klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false);
|
||||||
|
if (klass == null) return null;
|
||||||
|
|
||||||
|
if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) {
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||||
|
Reflect.loadInt(mw, argIndex);
|
||||||
|
mw.visitLdcInsn(Type.getType(klass));
|
||||||
|
mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe);
|
||||||
|
if (name != null) {
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||||
|
Reflect.loadInt(mw, argIndex);
|
||||||
|
mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) {
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||||
|
Reflect.loadInt(mw, argIndex);
|
||||||
|
mw.visitLdcInsn(Type.getType(arg));
|
||||||
|
mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;");
|
||||||
|
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe);
|
||||||
|
if (name != null) {
|
||||||
|
if (Reflect.getRawType(method, genericArg, false) == null) return null;
|
||||||
|
|
||||||
|
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||||
|
Reflect.loadInt(mw, argIndex);
|
||||||
|
mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CCTweaked.LOG.severe(String.format("Unknown parameter type %s for method %s.%s.", arg.getName(), method.getDeclaringClass().getName(), method.getName()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("Guava")
|
||||||
|
private static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
|
||||||
|
return x -> {
|
||||||
|
try {
|
||||||
|
return function.apply(x);
|
||||||
|
} catch (Exception | LinkageError e) {
|
||||||
|
// LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching
|
||||||
|
// methods on a class which references non-existent (i.e. client-only) types.
|
||||||
|
CCTweaked.LOG.log(Level.SEVERE, "Error generating @LuaFunctions", e);
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
12
src/main/java/dan200/computercraft/core/asm/LuaMethod.java
Normal file
12
src/main/java/dan200/computercraft/core/asm/LuaMethod.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
|
||||||
|
|
||||||
|
public interface LuaMethod {
|
||||||
|
Object[] apply(Object target, IArguments args) throws Exception;
|
||||||
|
}
|
27
src/main/java/dan200/computercraft/core/asm/Methods.java
Normal file
27
src/main/java/dan200/computercraft/core/asm/Methods.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
public final class Methods {
|
||||||
|
private Methods() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Generator<LuaMethod> LUA_METHOD = new Generator<>(LuaMethod.class, Collections.emptyList(), m -> {
|
||||||
|
throw new IllegalStateException("Impossible");
|
||||||
|
});
|
||||||
|
|
||||||
|
public static <T> void forEachMethod(Generator<T> generator, Object object, BiConsumer<Object, NamedMethod<T>> accept) {
|
||||||
|
for (NamedMethod<T> method : generator.getMethods(object.getClass())) accept.accept(object, method);
|
||||||
|
|
||||||
|
if (object instanceof ObjectSource) {
|
||||||
|
for (Object extra : ((ObjectSource) object).getExtra()) {
|
||||||
|
for (NamedMethod<T> method : generator.getMethods(extra.getClass())) accept.accept(extra, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/main/java/dan200/computercraft/core/asm/NamedMethod.java
Normal file
29
src/main/java/dan200/computercraft/core/asm/NamedMethod.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
public final class NamedMethod<T> {
|
||||||
|
private final String name;
|
||||||
|
private final T method;
|
||||||
|
private final boolean nonYielding;
|
||||||
|
|
||||||
|
NamedMethod(String name, T method, boolean nonYielding) {
|
||||||
|
this.name = name;
|
||||||
|
this.method = method;
|
||||||
|
this.nonYielding = nonYielding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean nonYielding() {
|
||||||
|
return nonYielding;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Lua object which exposes additional methods.
|
||||||
|
* <p>
|
||||||
|
* This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely
|
||||||
|
* happy with the interface - something I'd like to think about first.
|
||||||
|
*/
|
||||||
|
public interface ObjectSource {
|
||||||
|
Iterable<Object> getExtra();
|
||||||
|
}
|
75
src/main/java/dan200/computercraft/core/asm/Reflect.java
Normal file
75
src/main/java/dan200/computercraft/core/asm/Reflect.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.Coerced;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.objectweb.asm.Opcodes.ICONST_0;
|
||||||
|
|
||||||
|
final class Reflect {
|
||||||
|
static final java.lang.reflect.Type OPTIONAL_IN = Optional.class.getTypeParameters()[0];
|
||||||
|
static final java.lang.reflect.Type COERCED_IN = Coerced.class.getTypeParameters()[0];
|
||||||
|
|
||||||
|
private Reflect() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getLuaName(Class<?> klass, boolean unsafe) {
|
||||||
|
if (klass.isPrimitive()) {
|
||||||
|
if (klass == int.class) return "Int";
|
||||||
|
if (klass == boolean.class) return "Boolean";
|
||||||
|
if (klass == double.class) return "Double";
|
||||||
|
if (klass == long.class) return "Long";
|
||||||
|
} else {
|
||||||
|
if (klass == Map.class) return "Table";
|
||||||
|
if (klass == String.class) return "String";
|
||||||
|
if (klass == ByteBuffer.class) return "Bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Class<?> getRawType(Method method, Type root, boolean allowParameter) {
|
||||||
|
Type underlying = root;
|
||||||
|
while (true) {
|
||||||
|
if (underlying instanceof Class<?>) return (Class<?>) underlying;
|
||||||
|
|
||||||
|
if (underlying instanceof ParameterizedType) {
|
||||||
|
ParameterizedType type = (ParameterizedType) underlying;
|
||||||
|
if (!allowParameter) {
|
||||||
|
for (Type arg : type.getActualTypeArguments()) {
|
||||||
|
if (arg instanceof WildcardType) continue;
|
||||||
|
if (arg instanceof TypeVariable<?> && ((TypeVariable<?>) arg).getName().startsWith("capture#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOG.error("Method {}.{} has generic type {} with non-wildcard argument {}.", method.getDeclaringClass(), method.getName(), root, arg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue to extract from this child
|
||||||
|
underlying = type.getRawType();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOG.error("Method {}.{} has unknown generic type {}.", method.getDeclaringClass(), method.getName(), root);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void loadInt(MethodVisitor visitor, int value) {
|
||||||
|
if (value >= -1 && value <= 5) {
|
||||||
|
visitor.visitInsn(ICONST_0 + value);
|
||||||
|
} else {
|
||||||
|
visitor.visitLdcInsn(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/main/java/dan200/computercraft/core/asm/Support.java
Normal file
21
src/main/java/dan200/computercraft/core/asm/Support.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.asm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support methods used by the generated ASM.
|
||||||
|
*/
|
||||||
|
public final class Support {
|
||||||
|
private Support() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object[] of() {
|
||||||
|
return new Object[]{};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object[] of(Object value) {
|
||||||
|
return new Object[]{ value };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package dan200.computercraft.core.lua;
|
||||||
|
|
||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
|
||||||
|
import cc.tweaked.CCTweaked;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.core.asm.LuaMethod;
|
||||||
|
import org.squiddev.cobalt.LuaError;
|
||||||
|
import org.squiddev.cobalt.LuaState;
|
||||||
|
import org.squiddev.cobalt.Varargs;
|
||||||
|
import org.squiddev.cobalt.function.VarArgFunction;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An "optimised" version of {@code ResultInterpreterFunction} which is guaranteed to never yield.
|
||||||
|
* <p>
|
||||||
|
* As we never yield, we do not need to push a function to the stack, which removes a small amount of overhead.
|
||||||
|
*/
|
||||||
|
class BasicFunction extends VarArgFunction {
|
||||||
|
private final CobaltLuaMachine machine;
|
||||||
|
private final LuaMethod method;
|
||||||
|
private final Object instance;
|
||||||
|
private final String funcName;
|
||||||
|
|
||||||
|
BasicFunction(CobaltLuaMachine machine, LuaMethod method, Object instance, String name) {
|
||||||
|
this.machine = machine;
|
||||||
|
this.method = method;
|
||||||
|
this.instance = instance;
|
||||||
|
funcName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Varargs invoke(LuaState luaState, Varargs args) throws LuaError {
|
||||||
|
VarargArguments arguments = VarargArguments.of(args);
|
||||||
|
Object[] results;
|
||||||
|
try {
|
||||||
|
results = method.apply(instance, arguments);
|
||||||
|
} catch (LuaException e) {
|
||||||
|
throw wrap(e);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
CCTweaked.LOG.log(Level.SEVERE, String.format("Error calling %s on %s", funcName, instance), t);
|
||||||
|
throw new LuaError("Java Exception Thrown: " + t, 0);
|
||||||
|
} finally {
|
||||||
|
arguments.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return machine.toValues(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LuaError wrap(LuaException exception) {
|
||||||
|
return exception.hasLevel() ? new LuaError(exception.getMessage()) : new LuaError(exception.getMessage(), exception.getLevel());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,341 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.core.lua;
|
||||||
|
|
||||||
|
import cc.tweaked.CCTweaked;
|
||||||
|
import cpw.mods.fml.common.Loader;
|
||||||
|
import dan200.ComputerCraft;
|
||||||
|
import dan200.computer.core.ILuaAPI;
|
||||||
|
import dan200.computer.core.ILuaMachine;
|
||||||
|
import dan200.computer.core.ILuaObject;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.core.asm.Methods;
|
||||||
|
import org.squiddev.cobalt.*;
|
||||||
|
import org.squiddev.cobalt.compiler.LoadState;
|
||||||
|
import org.squiddev.cobalt.function.LuaFunction;
|
||||||
|
import org.squiddev.cobalt.function.VarArgFunction;
|
||||||
|
import org.squiddev.cobalt.interrupt.InterruptAction;
|
||||||
|
import org.squiddev.cobalt.lib.Bit32Lib;
|
||||||
|
import org.squiddev.cobalt.lib.CoreLibraries;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static org.squiddev.cobalt.ValueFactory.valueOf;
|
||||||
|
import static org.squiddev.cobalt.ValueFactory.varargsOf;
|
||||||
|
|
||||||
|
public class CobaltLuaMachine implements ILuaMachine {
|
||||||
|
private LuaState state;
|
||||||
|
private LuaTable globals;
|
||||||
|
|
||||||
|
private LuaThread mainRoutine = null;
|
||||||
|
private String eventFilter = null;
|
||||||
|
|
||||||
|
private volatile boolean isSoftAborted;
|
||||||
|
private volatile boolean isHardAborted;
|
||||||
|
private boolean thrownSoftAbort;
|
||||||
|
|
||||||
|
public CobaltLuaMachine() {
|
||||||
|
// Create an environment to run in
|
||||||
|
LuaState state = this.state = LuaState.builder()
|
||||||
|
.interruptHandler(() -> {
|
||||||
|
if (isHardAborted || CobaltLuaMachine.this.state == null) throw new HardAbortError();
|
||||||
|
if (isSoftAborted && !thrownSoftAbort) {
|
||||||
|
thrownSoftAbort = true;
|
||||||
|
throw new LuaError("Too long without yielding");
|
||||||
|
}
|
||||||
|
|
||||||
|
return InterruptAction.CONTINUE;
|
||||||
|
})
|
||||||
|
.errorReporter((e, m) -> CCTweaked.LOG.log(Level.SEVERE, "Error occurred in Lua VM. Execution will continue:\n" + m.get(), e))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
globals = state.getMainThread().getfenv();
|
||||||
|
CoreLibraries.debugGlobals(state);
|
||||||
|
Bit32Lib.add(state, globals);
|
||||||
|
|
||||||
|
globals.rawset("_HOST", valueOf("ComputerCraft " + ComputerCraft.getVersion() + " (" + Loader.instance().getMCVersionString() + ")"));
|
||||||
|
globals.rawset("_CC_DEFAULT_SETTINGS", valueOf(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAPI(ILuaAPI api) {
|
||||||
|
// Add the methods of an API to the global table
|
||||||
|
LuaTable table = wrapLuaObject(api);
|
||||||
|
String[] names = api.getNames();
|
||||||
|
for (String name : names) {
|
||||||
|
globals.rawset(name, table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadBios(InputStream bios) {
|
||||||
|
// Begin executing a file (ie, the bios)
|
||||||
|
if (mainRoutine != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
LuaFunction value = LoadState.load(state, bios, "@bios.lua", globals);
|
||||||
|
mainRoutine = new LuaThread(state, value, globals);
|
||||||
|
} catch (Exception e) {
|
||||||
|
CCTweaked.LOG.log(Level.SEVERE, "Failed to load bios.lua", e);
|
||||||
|
unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleEvent(String eventName, Object[] arguments) {
|
||||||
|
if (mainRoutine == null) return;
|
||||||
|
|
||||||
|
if (eventFilter != null && eventName != null && !eventName.equals(eventFilter) && !eventName.equals("terminate")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the soft abort has been cleared then we can reset our flag.
|
||||||
|
isSoftAborted = isHardAborted = thrownSoftAbort = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Varargs resumeArgs = Constants.NONE;
|
||||||
|
if (eventName != null) {
|
||||||
|
resumeArgs = varargsOf(valueOf(eventName), toValues(arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume the current thread, or the main one when first starting off.
|
||||||
|
LuaThread thread = state.getCurrentThread();
|
||||||
|
if (thread == null || thread == state.getMainThread()) thread = mainRoutine;
|
||||||
|
|
||||||
|
Varargs results = LuaThread.run(thread, resumeArgs);
|
||||||
|
if (isHardAborted) throw HardAbortError.INSTANCE;
|
||||||
|
if (results == null) return;
|
||||||
|
|
||||||
|
LuaValue filter = results.first();
|
||||||
|
eventFilter = filter.isString() ? filter.toString() : null;
|
||||||
|
|
||||||
|
if (!mainRoutine.isAlive()) unload();
|
||||||
|
} catch (HardAbortError e) {
|
||||||
|
unload();
|
||||||
|
} catch (LuaError e) {
|
||||||
|
unload();
|
||||||
|
CCTweaked.LOG.log(Level.WARNING, "Top level coroutine errored: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void softAbort(String s) {
|
||||||
|
isSoftAborted = true;
|
||||||
|
if (state != null) state.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hardAbort(String s) {
|
||||||
|
isHardAborted = true;
|
||||||
|
if (state != null) state.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean saveState(OutputStream outputStream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean restoreState(InputStream inputStream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
return state == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unload() {
|
||||||
|
LuaState state = this.state;
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
|
state.interrupt();
|
||||||
|
mainRoutine = null;
|
||||||
|
this.state = null;
|
||||||
|
globals = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LuaTable wrapLuaObject(Object value) {
|
||||||
|
LuaTable table = new LuaTable();
|
||||||
|
|
||||||
|
Methods.forEachMethod(Methods.LUA_METHOD, value,
|
||||||
|
(instance, method) -> table.rawset(method.getName(), new BasicFunction(this, method.getMethod(), instance, method.getName()))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(value instanceof ILuaObject)) {
|
||||||
|
try {
|
||||||
|
if (table.next(Constants.NIL).first().isNil()) return null;
|
||||||
|
} catch (LuaError ignored) {
|
||||||
|
// next should never throw on nil.
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
ILuaObject object = (ILuaObject) value;
|
||||||
|
|
||||||
|
String[] methods = object.getMethodNames();
|
||||||
|
for (int i = 0; i < methods.length; i++) {
|
||||||
|
if (methods[i] == null) continue;
|
||||||
|
|
||||||
|
final int method = i;
|
||||||
|
table.rawset(methods[i], new VarArgFunction() {
|
||||||
|
@Override
|
||||||
|
public Varargs invoke(final LuaState state, Varargs args) throws LuaError {
|
||||||
|
int count = args.count();
|
||||||
|
Object[] objects = new Object[count];
|
||||||
|
for (int n = 1; n <= count; n++) objects[n - 1] = toObject(args.arg(n), null);
|
||||||
|
|
||||||
|
Object[] results;
|
||||||
|
try {
|
||||||
|
results = object.callMethod(method, objects);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (!(e instanceof LuaException)) e.printStackTrace();
|
||||||
|
throw new LuaError(e.getMessage());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new LuaError("Java Exception Thrown: " + t, 0);
|
||||||
|
}
|
||||||
|
return toValues(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LuaValue toValue(Object object, Map<Object, LuaValue> values) {
|
||||||
|
if (object == null) return Constants.NIL;
|
||||||
|
if (object instanceof Number) return valueOf(((Number) object).doubleValue());
|
||||||
|
if (object instanceof Boolean) return valueOf((Boolean) object);
|
||||||
|
if (object instanceof String) return valueOf(object.toString());
|
||||||
|
if (object instanceof byte[]) {
|
||||||
|
byte[] b = (byte[]) object;
|
||||||
|
return valueOf(Arrays.copyOf(b, b.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaValue result = values.get(object);
|
||||||
|
if (result != null) return result;
|
||||||
|
|
||||||
|
if (object instanceof Map) {
|
||||||
|
LuaTable table = new LuaTable();
|
||||||
|
values.put(object, table);
|
||||||
|
|
||||||
|
for (Map.Entry<?, ?> pair : ((Map<?, ?>) object).entrySet()) {
|
||||||
|
LuaValue key = toValue(pair.getKey(), values);
|
||||||
|
LuaValue value = toValue(pair.getValue(), values);
|
||||||
|
if (!key.isNil() && !value.isNil()) table.rawset(key, value);
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object instanceof Collection) {
|
||||||
|
Collection<?> objects = (Collection<?>) object;
|
||||||
|
LuaTable table = new LuaTable(objects.size(), 0);
|
||||||
|
values.put(object, table);
|
||||||
|
int i = 0;
|
||||||
|
for (Object child : objects) table.rawset(++i, toValue(child, values));
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object instanceof Object[]) {
|
||||||
|
Object[] objects = (Object[]) object;
|
||||||
|
LuaTable table = new LuaTable(objects.length, 0);
|
||||||
|
values.put(object, table);
|
||||||
|
for (int i = 0; i < objects.length; i++) table.rawset(i + 1, toValue(objects[i], values));
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaTable wrapped = wrapLuaObject(object);
|
||||||
|
if (wrapped != null) {
|
||||||
|
values.put(object, wrapped);
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
CCTweaked.LOG.warning(String.format("Received unknown type '{}', returning nil.", object.getClass().getName()));
|
||||||
|
|
||||||
|
return Constants.NIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Varargs toValues(Object[] objects) {
|
||||||
|
if (objects == null || objects.length == 0) return Constants.NONE;
|
||||||
|
|
||||||
|
Map<Object, LuaValue> result = new IdentityHashMap<>(0);
|
||||||
|
LuaValue[] values = new LuaValue[objects.length];
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
Object object = objects[i];
|
||||||
|
values[i] = toValue(object, result);
|
||||||
|
}
|
||||||
|
return varargsOf(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object toObject(LuaValue value, Map<LuaValue, Object> objects) {
|
||||||
|
switch (value.type()) {
|
||||||
|
case Constants.TNIL:
|
||||||
|
return null;
|
||||||
|
case Constants.TINT:
|
||||||
|
case Constants.TNUMBER:
|
||||||
|
return value.toDouble();
|
||||||
|
case Constants.TBOOLEAN:
|
||||||
|
return value.toBoolean();
|
||||||
|
case Constants.TSTRING:
|
||||||
|
return value.toString();
|
||||||
|
case Constants.TTABLE: {
|
||||||
|
// Table:
|
||||||
|
// Start remembering stuff
|
||||||
|
if (objects == null) {
|
||||||
|
objects = new IdentityHashMap<>();
|
||||||
|
} else if (objects.containsKey(value)) {
|
||||||
|
return objects.get(value);
|
||||||
|
}
|
||||||
|
Map<Object, Object> table = new HashMap<Object, Object>();
|
||||||
|
objects.put(value, table);
|
||||||
|
|
||||||
|
LuaTable luaTable = (LuaTable) value;
|
||||||
|
|
||||||
|
// Convert all keys
|
||||||
|
LuaValue k = Constants.NIL;
|
||||||
|
while (true) {
|
||||||
|
Varargs keyValue;
|
||||||
|
try {
|
||||||
|
keyValue = luaTable.next(k);
|
||||||
|
} catch (LuaError luaError) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
k = keyValue.first();
|
||||||
|
if (k.isNil()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaValue v = keyValue.arg(2);
|
||||||
|
Object keyObject = toObject(k, objects);
|
||||||
|
Object valueObject = toObject(v, objects);
|
||||||
|
if (keyObject != null && valueObject != null) {
|
||||||
|
table.put(keyObject, valueObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HardAbortError extends Error {
|
||||||
|
private static final long serialVersionUID = 7954092008586367501L;
|
||||||
|
|
||||||
|
static final HardAbortError INSTANCE = new HardAbortError();
|
||||||
|
|
||||||
|
private HardAbortError() {
|
||||||
|
super("Hard Abort");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized Throwable fillInStackTrace() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
230
src/main/java/dan200/computercraft/core/lua/VarargArguments.java
Normal file
230
src/main/java/dan200/computercraft/core/lua/VarargArguments.java
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.lua;
|
||||||
|
|
||||||
|
import cc.tweaked.CCTweaked;
|
||||||
|
import dan200.computercraft.api.lua.IArguments;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaValues;
|
||||||
|
import org.squiddev.cobalt.*;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static org.squiddev.cobalt.Constants.NAME;
|
||||||
|
|
||||||
|
final class VarargArguments extends IArguments {
|
||||||
|
private static final VarargArguments EMPTY = new VarargArguments(Constants.NONE);
|
||||||
|
private static boolean reportedIllegalGet;
|
||||||
|
|
||||||
|
static {
|
||||||
|
EMPTY.escapes = EMPTY.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Varargs varargs;
|
||||||
|
|
||||||
|
private volatile boolean closed;
|
||||||
|
private final VarargArguments root;
|
||||||
|
|
||||||
|
private ArraySlice<Object> cache;
|
||||||
|
private ArraySlice<String> typeNames;
|
||||||
|
|
||||||
|
private boolean escapes;
|
||||||
|
|
||||||
|
private VarargArguments(Varargs varargs) {
|
||||||
|
this.varargs = varargs;
|
||||||
|
root = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VarargArguments(Varargs varargs, VarargArguments root, int offset) {
|
||||||
|
this.varargs = varargs;
|
||||||
|
this.root = root;
|
||||||
|
escapes = root.escapes;
|
||||||
|
cache = root.cache == null ? null : root.cache.drop(offset);
|
||||||
|
typeNames = root.typeNames == null ? null : root.typeNames.drop(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static VarargArguments of(Varargs values) {
|
||||||
|
return values == Constants.NONE ? EMPTY : new VarargArguments(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isClosed() {
|
||||||
|
return root.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAccessible() {
|
||||||
|
if (isClosed() && !escapes) throwInaccessible();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwInaccessible() {
|
||||||
|
IllegalStateException error = new IllegalStateException("Function arguments have escaped their original scope.");
|
||||||
|
if (!reportedIllegalGet) {
|
||||||
|
reportedIllegalGet = true;
|
||||||
|
CCTweaked.LOG.log(Level.SEVERE,
|
||||||
|
"A function attempted to access arguments outside the scope of the original function. This is probably " +
|
||||||
|
"caused by the function scheduling work on the main thread. You may need to call IArguments.escapes().",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int count() {
|
||||||
|
return varargs.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object get(int index) {
|
||||||
|
checkAccessible();
|
||||||
|
if (index < 0 || index >= varargs.count()) return null;
|
||||||
|
|
||||||
|
ArraySlice<Object> cache = this.cache;
|
||||||
|
if (cache == null) {
|
||||||
|
cache = this.cache = new ArraySlice<>(new Object[varargs.count()], 0);
|
||||||
|
} else {
|
||||||
|
Object existing = cache.get(index);
|
||||||
|
if (existing != null) return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaValue arg = varargs.arg(index + 1);
|
||||||
|
|
||||||
|
// This holds as either a) the arguments are not closed or b) the arguments were escaped, in which case
|
||||||
|
// tables should have been converted already.
|
||||||
|
assert !isClosed() || !(arg instanceof LuaTable) : "Converting a LuaTable after arguments were closed.";
|
||||||
|
|
||||||
|
Object converted = CobaltLuaMachine.toObject(arg, null);
|
||||||
|
cache.set(index, converted);
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStringCoerced(int index) {
|
||||||
|
checkAccessible();
|
||||||
|
// This doesn't run __tostring, which is _technically_ wrong, but avoids a lot of complexity.
|
||||||
|
return varargs.arg(index + 1).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(int index) {
|
||||||
|
checkAccessible();
|
||||||
|
|
||||||
|
LuaValue value = varargs.arg(index + 1);
|
||||||
|
|
||||||
|
// If we've escaped, read it from the precomputed list, otherwise get the custom name.
|
||||||
|
String name = escapes ? (typeNames == null ? null : typeNames.get(index)) : getCustomType(value);
|
||||||
|
if (name != null) return name;
|
||||||
|
|
||||||
|
return value.typeName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IArguments drop(int count) {
|
||||||
|
if (count < 0) throw new IllegalStateException("count cannot be negative");
|
||||||
|
if (count == 0) return this;
|
||||||
|
|
||||||
|
Varargs newArgs = varargs.subargs(count + 1);
|
||||||
|
if (newArgs == Constants.NONE) return EMPTY;
|
||||||
|
return new VarargArguments(newArgs, this, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getDouble(int index) throws LuaException {
|
||||||
|
checkAccessible();
|
||||||
|
LuaValue value = varargs.arg(index + 1);
|
||||||
|
if (!(value instanceof LuaNumber)) throw LuaValues.badArgument(index, "number", value.typeName());
|
||||||
|
return value.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLong(int index) throws LuaException {
|
||||||
|
checkAccessible();
|
||||||
|
LuaValue value = varargs.arg(index + 1);
|
||||||
|
if (!(value instanceof LuaNumber)) throw LuaValues.badArgument(index, "number", value.typeName());
|
||||||
|
return value instanceof LuaInteger ? value.toInteger() : (long) LuaValues.checkFinite(index, value.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getBytes(int index) throws LuaException {
|
||||||
|
checkAccessible();
|
||||||
|
LuaValue value = varargs.arg(index + 1);
|
||||||
|
if (!(value instanceof LuaString)) throw LuaValues.badArgument(index, "string", value.typeName());
|
||||||
|
return ((LuaString) value).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ByteBuffer> optBytes(int index) throws LuaException {
|
||||||
|
checkAccessible();
|
||||||
|
LuaValue value = varargs.arg(index + 1);
|
||||||
|
if (value.isNil()) return Optional.empty();
|
||||||
|
if (!(value instanceof LuaString)) throw LuaValues.badArgument(index, "string", value.typeName());
|
||||||
|
return Optional.of(((LuaString) value).toBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IArguments escapes() {
|
||||||
|
if (escapes) return this;
|
||||||
|
|
||||||
|
ArraySlice<Object> cache = this.cache;
|
||||||
|
ArraySlice<String> typeNames = this.typeNames;
|
||||||
|
|
||||||
|
for (int i = 0, count = varargs.count(); i < count; i++) {
|
||||||
|
LuaValue arg = varargs.arg(i + 1);
|
||||||
|
|
||||||
|
// Convert tables.
|
||||||
|
if (arg instanceof LuaTable) {
|
||||||
|
if (cache == null) cache = new ArraySlice<>(new Object[count], 0);
|
||||||
|
cache.set(i, CobaltLuaMachine.toObject(arg, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch custom type names.
|
||||||
|
String typeName = getCustomType(arg);
|
||||||
|
if (typeName != null) {
|
||||||
|
if (typeNames == null) typeNames = new ArraySlice<>(new String[count], 0);
|
||||||
|
typeNames.set(i, typeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapes = true;
|
||||||
|
this.cache = cache;
|
||||||
|
this.typeNames = typeNames;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCustomType(LuaValue arg) {
|
||||||
|
if (!(arg instanceof LuaTable) && !(arg instanceof LuaUserdata)) return null;
|
||||||
|
|
||||||
|
LuaTable metatable = arg.getMetatable(null);
|
||||||
|
return metatable != null && metatable.rawget(NAME) instanceof LuaString ? ((LuaString) metatable.rawget(NAME)).toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ArraySlice<T> {
|
||||||
|
private final T[] array;
|
||||||
|
private final int offset;
|
||||||
|
|
||||||
|
private ArraySlice(T[] array, int offset) {
|
||||||
|
this.array = array;
|
||||||
|
this.offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
T get(int index) {
|
||||||
|
return array[offset + index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(int index, T value) {
|
||||||
|
array[offset + index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArraySlice<T> drop(int count) {
|
||||||
|
return new ArraySlice<>(array, offset + count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/main/java/dan200/computercraft/core/util/StringUtil.java
Normal file
25
src/main/java/dan200/computercraft/core/util/StringUtil.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.util;
|
||||||
|
|
||||||
|
public final class StringUtil {
|
||||||
|
private StringUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normaliseLabel(String label) {
|
||||||
|
int length = Math.min(32, label.length());
|
||||||
|
StringBuilder builder = new StringBuilder(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
char c = label.charAt(i);
|
||||||
|
if ((c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255)) {
|
||||||
|
builder.append(c);
|
||||||
|
} else {
|
||||||
|
builder.append('?');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
package dan200.computercraft.shared.peripheral.monitor;
|
||||||
|
|
||||||
|
import dan200.computer.core.Terminal;
|
||||||
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
|
import dan200.computercraft.api.lua.LuaValues;
|
||||||
|
import dan200.computercraft.core.apis.TermMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors are a block which act as a terminal, displaying information on one side. This allows them to be read and
|
||||||
|
* interacted with in-world without opening a GUI.
|
||||||
|
* <p>
|
||||||
|
* Monitors act as @{term.Redirect|terminal redirects} and so expose the same methods, as well as several additional
|
||||||
|
* ones, which are documented below.
|
||||||
|
* <p>
|
||||||
|
* Like computers, monitors come in both normal (no colour) and advanced (colour) varieties.
|
||||||
|
* <p>
|
||||||
|
* ## Recipes
|
||||||
|
* <div class="recipe-container">
|
||||||
|
* <mc-recipe recipe="computercraft:monitor_normal"></mc-recipe>
|
||||||
|
* <mc-recipe recipe="computercraft:monitor_advanced"></mc-recipe>
|
||||||
|
* </div>
|
||||||
|
*
|
||||||
|
* @cc.module monitor
|
||||||
|
* @cc.usage Write "Hello, world!" to an adjacent monitor:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* local monitor = peripheral.find("monitor")
|
||||||
|
* monitor.setCursorPos(1, 1)
|
||||||
|
* monitor.write("Hello, world!")
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public class MonitorPeripheral extends TermMethods {
|
||||||
|
private final TileEntityMonitorAccessor monitor;
|
||||||
|
|
||||||
|
public MonitorPeripheral(TileEntityMonitorAccessor monitor) {
|
||||||
|
this.monitor = monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the scale of this monitor. A larger scale will result in the monitor having a lower resolution, but display
|
||||||
|
* text much larger.
|
||||||
|
*
|
||||||
|
* @param scaleArg The monitor's scale. This must be a multiple of 0.5 between 0.5 and 5.
|
||||||
|
* @throws LuaException If the scale is out of range.
|
||||||
|
* @see #getTextScale()
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final void setTextScale(double scaleArg) throws LuaException {
|
||||||
|
int scale = (int) (LuaValues.checkFinite(0, scaleArg) * 2.0);
|
||||||
|
if (scale < 1 || scale > 10) throw new LuaException("Expected number in range 0.5-5");
|
||||||
|
getMonitor().cct$setTextScale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the monitor's current text scale.
|
||||||
|
*
|
||||||
|
* @return The monitor's current scale.
|
||||||
|
* @cc.since 1.81.0
|
||||||
|
*/
|
||||||
|
@LuaFunction
|
||||||
|
public final double getTextScale() {
|
||||||
|
return getMonitor().cct$getTextScale() / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TileEntityMonitorAccessor getMonitor() {
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isColour() {
|
||||||
|
return monitor.isColour();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Terminal getTerminal() throws LuaException {
|
||||||
|
Terminal terminal = getMonitor().cct$getOriginTerminal();
|
||||||
|
if (terminal == null) throw new LuaException("Monitor has been detached");
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.shared.peripheral.monitor;
|
||||||
|
|
||||||
|
import dan200.computer.core.Terminal;
|
||||||
|
|
||||||
|
public interface TileEntityMonitorAccessor {
|
||||||
|
Terminal cct$getOriginTerminal();
|
||||||
|
|
||||||
|
void cct$setTextScale(int scale);
|
||||||
|
|
||||||
|
int cct$getTextScale();
|
||||||
|
|
||||||
|
boolean isColour();
|
||||||
|
}
|
61
src/main/java/dan200/computercraft/util/Colour.java
Normal file
61
src/main/java/dan200/computercraft/util/Colour.java
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
package dan200.computercraft.util;
|
||||||
|
|
||||||
|
public enum Colour {
|
||||||
|
BLACK(0x111111),
|
||||||
|
RED(0xcc4c4c),
|
||||||
|
GREEN(0x57A64E),
|
||||||
|
BROWN(0x7f664c),
|
||||||
|
BLUE(0x3366cc),
|
||||||
|
PURPLE(0xb266e5),
|
||||||
|
CYAN(0x4c99b2),
|
||||||
|
LIGHT_GREY(0x999999),
|
||||||
|
GREY(0x4c4c4c),
|
||||||
|
PINK(0xf2b2cc),
|
||||||
|
LIME(0x7fcc19),
|
||||||
|
YELLOW(0xdede6c),
|
||||||
|
LIGHT_BLUE(0x99b2f2),
|
||||||
|
MAGENTA(0xe57fd8),
|
||||||
|
ORANGE(0xf2b233),
|
||||||
|
WHITE(0xf0f0f0);
|
||||||
|
|
||||||
|
private static final Colour[] VALUES = values();
|
||||||
|
|
||||||
|
public static Colour fromInt(int colour) {
|
||||||
|
return Colour.VALUES[colour];
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int hex;
|
||||||
|
private final float[] rgb;
|
||||||
|
|
||||||
|
Colour(int hex) {
|
||||||
|
this.hex = hex;
|
||||||
|
rgb = new float[]{
|
||||||
|
((hex >> 16) & 0xFF) / 255.0f,
|
||||||
|
((hex >> 8) & 0xFF) / 255.0f,
|
||||||
|
(hex & 0xFF) / 255.0f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHex() {
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] getRGB() {
|
||||||
|
return rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getR() {
|
||||||
|
return rgb[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getG() {
|
||||||
|
return rgb[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getB() {
|
||||||
|
return rgb[2];
|
||||||
|
}
|
||||||
|
}
|
795
src/main/resources/assets/cctweaked/lua/bios.lua
Normal file
795
src/main/resources/assets/cctweaked/lua/bios.lua
Normal file
@ -0,0 +1,795 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
-- Load in expect from the module path.
|
||||||
|
--
|
||||||
|
-- Ideally we'd use require, but that is part of the shell, and so is not
|
||||||
|
-- available to the BIOS or any APIs. All APIs load this using dofile, but that
|
||||||
|
-- has not been defined at this point.
|
||||||
|
local expect
|
||||||
|
|
||||||
|
do
|
||||||
|
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
|
||||||
|
local f, err = loadstring(h.readAll(), "@/rom/modules/main/cc/expect.lua")
|
||||||
|
h.close()
|
||||||
|
|
||||||
|
if not f then error(err) end
|
||||||
|
expect = f().expect
|
||||||
|
end
|
||||||
|
|
||||||
|
if _VERSION == "Lua 5.1" then
|
||||||
|
-- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it
|
||||||
|
local nativeload = load
|
||||||
|
|
||||||
|
function load(x, name, mode, env)
|
||||||
|
expect(1, x, "function", "string")
|
||||||
|
expect(2, name, "string", "nil")
|
||||||
|
expect(3, mode, "string", "nil")
|
||||||
|
expect(4, env, "table", "nil")
|
||||||
|
|
||||||
|
local ok, p1, p2 = pcall(function()
|
||||||
|
local result, err = nativeload(x, name, mode, env)
|
||||||
|
if result and env then
|
||||||
|
env._ENV = env
|
||||||
|
end
|
||||||
|
return result, err
|
||||||
|
end)
|
||||||
|
if ok then
|
||||||
|
return p1, p2
|
||||||
|
else
|
||||||
|
error(p1, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if _CC_DISABLE_LUA51_FEATURES then
|
||||||
|
-- Remove the Lua 5.1 features that will be removed when we update to Lua 5.2, for compatibility testing.
|
||||||
|
-- See "disable_lua51_functions" in ComputerCraft.cfg
|
||||||
|
setfenv = nil
|
||||||
|
getfenv = nil
|
||||||
|
loadstring = nil
|
||||||
|
unpack = nil
|
||||||
|
math.log10 = nil
|
||||||
|
table.maxn = nil
|
||||||
|
else
|
||||||
|
loadstring = function(string, chunkname) return nativeload(string, chunkname) end
|
||||||
|
|
||||||
|
-- Inject a stub for the old bit library
|
||||||
|
_G.bit = {
|
||||||
|
bnot = bit32.bnot,
|
||||||
|
band = bit32.band,
|
||||||
|
bor = bit32.bor,
|
||||||
|
bxor = bit32.bxor,
|
||||||
|
brshift = bit32.arshift,
|
||||||
|
blshift = bit32.lshift,
|
||||||
|
blogic_rshift = bit32.rshift,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Install lua parts of the os api
|
||||||
|
function os.version()
|
||||||
|
return "CraftOS 1.8"
|
||||||
|
end
|
||||||
|
|
||||||
|
function os.pullEventRaw(sFilter)
|
||||||
|
return coroutine.yield(sFilter)
|
||||||
|
end
|
||||||
|
|
||||||
|
function os.pullEvent(sFilter)
|
||||||
|
local eventData = table.pack(os.pullEventRaw(sFilter))
|
||||||
|
if eventData[1] == "terminate" then
|
||||||
|
error("Terminated", 0)
|
||||||
|
end
|
||||||
|
return table.unpack(eventData, 1, eventData.n)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Install globals
|
||||||
|
function sleep(nTime)
|
||||||
|
expect(1, nTime, "number", "nil")
|
||||||
|
local timer = os.startTimer(nTime or 0)
|
||||||
|
repeat
|
||||||
|
local _, param = os.pullEvent("timer")
|
||||||
|
until param == timer
|
||||||
|
end
|
||||||
|
|
||||||
|
function write(sText)
|
||||||
|
expect(1, sText, "string", "number")
|
||||||
|
|
||||||
|
local w, h = term.getSize()
|
||||||
|
local x, y = term.getCursorPos()
|
||||||
|
|
||||||
|
local nLinesPrinted = 0
|
||||||
|
local function newLine()
|
||||||
|
if y + 1 <= h then
|
||||||
|
term.setCursorPos(1, y + 1)
|
||||||
|
else
|
||||||
|
term.setCursorPos(1, h)
|
||||||
|
term.scroll(1)
|
||||||
|
end
|
||||||
|
x, y = term.getCursorPos()
|
||||||
|
nLinesPrinted = nLinesPrinted + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Print the line with proper word wrapping
|
||||||
|
sText = tostring(sText)
|
||||||
|
while #sText > 0 do
|
||||||
|
local whitespace = string.match(sText, "^[ \t]+")
|
||||||
|
if whitespace then
|
||||||
|
-- Print whitespace
|
||||||
|
term.write(whitespace)
|
||||||
|
x, y = term.getCursorPos()
|
||||||
|
sText = string.sub(sText, #whitespace + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local newline = string.match(sText, "^\n")
|
||||||
|
if newline then
|
||||||
|
-- Print newlines
|
||||||
|
newLine()
|
||||||
|
sText = string.sub(sText, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local text = string.match(sText, "^[^ \t\n]+")
|
||||||
|
if text then
|
||||||
|
sText = string.sub(sText, #text + 1)
|
||||||
|
if #text > w then
|
||||||
|
-- Print a multiline word
|
||||||
|
while #text > 0 do
|
||||||
|
if x > w then
|
||||||
|
newLine()
|
||||||
|
end
|
||||||
|
term.write(text)
|
||||||
|
text = string.sub(text, w - x + 2)
|
||||||
|
x, y = term.getCursorPos()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Print a word normally
|
||||||
|
if x + #text - 1 > w then
|
||||||
|
newLine()
|
||||||
|
end
|
||||||
|
term.write(text)
|
||||||
|
x, y = term.getCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nLinesPrinted
|
||||||
|
end
|
||||||
|
|
||||||
|
function print(...)
|
||||||
|
local nLinesPrinted = 0
|
||||||
|
local nLimit = select("#", ...)
|
||||||
|
for n = 1, nLimit do
|
||||||
|
local s = tostring(select(n, ...))
|
||||||
|
if n < nLimit then
|
||||||
|
s = s .. "\t"
|
||||||
|
end
|
||||||
|
nLinesPrinted = nLinesPrinted + write(s)
|
||||||
|
end
|
||||||
|
nLinesPrinted = nLinesPrinted + write("\n")
|
||||||
|
return nLinesPrinted
|
||||||
|
end
|
||||||
|
|
||||||
|
function printError(...)
|
||||||
|
local oldColour
|
||||||
|
if term.isColour() then
|
||||||
|
oldColour = term.getTextColour()
|
||||||
|
term.setTextColour(colors.red)
|
||||||
|
end
|
||||||
|
print(...)
|
||||||
|
if term.isColour() then
|
||||||
|
term.setTextColour(oldColour)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function read(_sReplaceChar, _tHistory, _fnComplete, _sDefault)
|
||||||
|
expect(1, _sReplaceChar, "string", "nil")
|
||||||
|
expect(2, _tHistory, "table", "nil")
|
||||||
|
expect(3, _fnComplete, "function", "nil")
|
||||||
|
expect(4, _sDefault, "string", "nil")
|
||||||
|
|
||||||
|
term.setCursorBlink(true)
|
||||||
|
|
||||||
|
local sLine
|
||||||
|
if type(_sDefault) == "string" then
|
||||||
|
sLine = _sDefault
|
||||||
|
else
|
||||||
|
sLine = ""
|
||||||
|
end
|
||||||
|
local nHistoryPos
|
||||||
|
local nPos, nScroll = #sLine, 0
|
||||||
|
if _sReplaceChar then
|
||||||
|
_sReplaceChar = string.sub(_sReplaceChar, 1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tCompletions
|
||||||
|
local nCompletion
|
||||||
|
local function recomplete()
|
||||||
|
if _fnComplete and nPos == #sLine then
|
||||||
|
tCompletions = _fnComplete(sLine)
|
||||||
|
if tCompletions and #tCompletions > 0 then
|
||||||
|
nCompletion = 1
|
||||||
|
else
|
||||||
|
nCompletion = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tCompletions = nil
|
||||||
|
nCompletion = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function uncomplete()
|
||||||
|
tCompletions = nil
|
||||||
|
nCompletion = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local w = term.getSize()
|
||||||
|
local sx = term.getCursorPos()
|
||||||
|
|
||||||
|
local function redraw(_bClear)
|
||||||
|
local cursor_pos = nPos - nScroll
|
||||||
|
if sx + cursor_pos >= w then
|
||||||
|
-- We've moved beyond the RHS, ensure we're on the edge.
|
||||||
|
nScroll = sx + nPos - w
|
||||||
|
elseif cursor_pos < 0 then
|
||||||
|
-- We've moved beyond the LHS, ensure we're on the edge.
|
||||||
|
nScroll = nPos
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, cy = term.getCursorPos()
|
||||||
|
term.setCursorPos(sx, cy)
|
||||||
|
local sReplace = _bClear and " " or _sReplaceChar
|
||||||
|
if sReplace then
|
||||||
|
term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0)))
|
||||||
|
else
|
||||||
|
term.write(string.sub(sLine, nScroll + 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
if nCompletion then
|
||||||
|
local sCompletion = tCompletions[nCompletion]
|
||||||
|
local oldText, oldBg
|
||||||
|
if not _bClear then
|
||||||
|
oldText = term.getTextColor()
|
||||||
|
oldBg = term.getBackgroundColor()
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
term.setBackgroundColor(colors.gray)
|
||||||
|
end
|
||||||
|
if sReplace then
|
||||||
|
term.write(string.rep(sReplace, #sCompletion))
|
||||||
|
else
|
||||||
|
term.write(sCompletion)
|
||||||
|
end
|
||||||
|
if not _bClear then
|
||||||
|
term.setTextColor(oldText)
|
||||||
|
term.setBackgroundColor(oldBg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setCursorPos(sx + nPos - nScroll, cy)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clear()
|
||||||
|
redraw(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
local function acceptCompletion()
|
||||||
|
if nCompletion then
|
||||||
|
-- Clear
|
||||||
|
clear()
|
||||||
|
|
||||||
|
-- Find the common prefix of all the other suggestions which start with the same letter as the current one
|
||||||
|
local sCompletion = tCompletions[nCompletion]
|
||||||
|
sLine = sLine .. sCompletion
|
||||||
|
nPos = #sLine
|
||||||
|
|
||||||
|
-- Redraw
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
while true do
|
||||||
|
local sEvent, param, param1, param2 = os.pullEvent()
|
||||||
|
if sEvent == "char" then
|
||||||
|
-- Typed key
|
||||||
|
clear()
|
||||||
|
sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
|
||||||
|
nPos = nPos + 1
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
elseif sEvent == "paste" then
|
||||||
|
-- Pasted text
|
||||||
|
clear()
|
||||||
|
sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
|
||||||
|
nPos = nPos + #param
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
elseif sEvent == "key" then
|
||||||
|
if param == keys.enter or param == keys.numPadEnter then
|
||||||
|
-- Enter/Numpad Enter
|
||||||
|
if nCompletion then
|
||||||
|
clear()
|
||||||
|
uncomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
break
|
||||||
|
|
||||||
|
elseif param == keys.left then
|
||||||
|
-- Left
|
||||||
|
if nPos > 0 then
|
||||||
|
clear()
|
||||||
|
nPos = nPos - 1
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys.right then
|
||||||
|
-- Right
|
||||||
|
if nPos < #sLine then
|
||||||
|
-- Move right
|
||||||
|
clear()
|
||||||
|
nPos = nPos + 1
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
else
|
||||||
|
-- Accept autocomplete
|
||||||
|
acceptCompletion()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys.up or param == keys.down then
|
||||||
|
-- Up or down
|
||||||
|
if nCompletion then
|
||||||
|
-- Cycle completions
|
||||||
|
clear()
|
||||||
|
if param == keys.up then
|
||||||
|
nCompletion = nCompletion - 1
|
||||||
|
if nCompletion < 1 then
|
||||||
|
nCompletion = #tCompletions
|
||||||
|
end
|
||||||
|
elseif param == keys.down then
|
||||||
|
nCompletion = nCompletion + 1
|
||||||
|
if nCompletion > #tCompletions then
|
||||||
|
nCompletion = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
elseif _tHistory then
|
||||||
|
-- Cycle history
|
||||||
|
clear()
|
||||||
|
if param == keys.up then
|
||||||
|
-- Up
|
||||||
|
if nHistoryPos == nil then
|
||||||
|
if #_tHistory > 0 then
|
||||||
|
nHistoryPos = #_tHistory
|
||||||
|
end
|
||||||
|
elseif nHistoryPos > 1 then
|
||||||
|
nHistoryPos = nHistoryPos - 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Down
|
||||||
|
if nHistoryPos == #_tHistory then
|
||||||
|
nHistoryPos = nil
|
||||||
|
elseif nHistoryPos ~= nil then
|
||||||
|
nHistoryPos = nHistoryPos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if nHistoryPos then
|
||||||
|
sLine = _tHistory[nHistoryPos]
|
||||||
|
nPos, nScroll = #sLine, 0
|
||||||
|
else
|
||||||
|
sLine = ""
|
||||||
|
nPos, nScroll = 0, 0
|
||||||
|
end
|
||||||
|
uncomplete()
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys.backspace then
|
||||||
|
-- Backspace
|
||||||
|
if nPos > 0 then
|
||||||
|
clear()
|
||||||
|
sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1)
|
||||||
|
nPos = nPos - 1
|
||||||
|
if nScroll > 0 then nScroll = nScroll - 1 end
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys.home then
|
||||||
|
-- Home
|
||||||
|
if nPos > 0 then
|
||||||
|
clear()
|
||||||
|
nPos = 0
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys.delete then
|
||||||
|
-- Delete
|
||||||
|
if nPos < #sLine then
|
||||||
|
clear()
|
||||||
|
sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2)
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys["end"] then
|
||||||
|
-- End
|
||||||
|
if nPos < #sLine then
|
||||||
|
clear()
|
||||||
|
nPos = #sLine
|
||||||
|
recomplete()
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif param == keys.tab then
|
||||||
|
-- Tab (accept autocomplete)
|
||||||
|
acceptCompletion()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif sEvent == "mouse_click" or sEvent == "mouse_drag" and param == 1 then
|
||||||
|
local _, cy = term.getCursorPos()
|
||||||
|
if param1 >= sx and param1 <= w and param2 == cy then
|
||||||
|
-- Ensure we don't scroll beyond the current line
|
||||||
|
nPos = math.min(math.max(nScroll + param1 - sx, 0), #sLine)
|
||||||
|
redraw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif sEvent == "term_resize" then
|
||||||
|
-- Terminal resized
|
||||||
|
w = term.getSize()
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, cy = term.getCursorPos()
|
||||||
|
term.setCursorBlink(false)
|
||||||
|
term.setCursorPos(w + 1, cy)
|
||||||
|
print()
|
||||||
|
|
||||||
|
return sLine
|
||||||
|
end
|
||||||
|
|
||||||
|
function loadfile(filename, mode, env)
|
||||||
|
-- Support the previous `loadfile(filename, env)` form instead.
|
||||||
|
if type(mode) == "table" and env == nil then
|
||||||
|
mode, env = nil, mode
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(1, filename, "string")
|
||||||
|
expect(2, mode, "string", "nil")
|
||||||
|
expect(3, env, "table", "nil")
|
||||||
|
|
||||||
|
local file = fs.open(filename, "r")
|
||||||
|
if not file then return nil, "File not found" end
|
||||||
|
|
||||||
|
local func, err = load(file.readAll(), "@/" .. fs.combine(filename), mode, env)
|
||||||
|
file.close()
|
||||||
|
return func, err
|
||||||
|
end
|
||||||
|
|
||||||
|
function dofile(_sFile)
|
||||||
|
expect(1, _sFile, "string")
|
||||||
|
|
||||||
|
local fnFile, e = loadfile(_sFile, nil, _G)
|
||||||
|
if fnFile then
|
||||||
|
return fnFile()
|
||||||
|
else
|
||||||
|
error(e, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Install the rest of the OS api
|
||||||
|
function os.run(_tEnv, _sPath, ...)
|
||||||
|
expect(1, _tEnv, "table")
|
||||||
|
expect(2, _sPath, "string")
|
||||||
|
|
||||||
|
local tEnv = _tEnv
|
||||||
|
setmetatable(tEnv, { __index = _G })
|
||||||
|
|
||||||
|
if settings.get("bios.strict_globals", false) then
|
||||||
|
-- load will attempt to set _ENV on this environment, which
|
||||||
|
-- throws an error with this protection enabled. Thus we set it here first.
|
||||||
|
tEnv._ENV = tEnv
|
||||||
|
getmetatable(tEnv).__newindex = function(_, name)
|
||||||
|
error("Attempt to create global " .. tostring(name), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local fnFile, err = loadfile(_sPath, nil, tEnv)
|
||||||
|
if fnFile then
|
||||||
|
local ok, err = pcall(fnFile, ...)
|
||||||
|
if not ok then
|
||||||
|
if err and err ~= "" then
|
||||||
|
printError(err)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if err and err ~= "" then
|
||||||
|
printError(err)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local tAPIsLoading = {}
|
||||||
|
function os.loadAPI(_sPath)
|
||||||
|
expect(1, _sPath, "string")
|
||||||
|
local sName = fs.getName(_sPath)
|
||||||
|
if sName:sub(-4) == ".lua" then
|
||||||
|
sName = sName:sub(1, -5)
|
||||||
|
end
|
||||||
|
if tAPIsLoading[sName] == true then
|
||||||
|
printError("API " .. sName .. " is already being loaded")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
tAPIsLoading[sName] = true
|
||||||
|
|
||||||
|
local tEnv = {}
|
||||||
|
setmetatable(tEnv, { __index = _G })
|
||||||
|
local fnAPI, err = loadfile(_sPath, nil, tEnv)
|
||||||
|
if fnAPI then
|
||||||
|
local ok, err = pcall(fnAPI)
|
||||||
|
if not ok then
|
||||||
|
tAPIsLoading[sName] = nil
|
||||||
|
return error("Failed to load API " .. sName .. " due to " .. err, 1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tAPIsLoading[sName] = nil
|
||||||
|
return error("Failed to load API " .. sName .. " due to " .. err, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tAPI = {}
|
||||||
|
for k, v in pairs(tEnv) do
|
||||||
|
if k ~= "_ENV" then
|
||||||
|
tAPI[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
_G[sName] = tAPI
|
||||||
|
tAPIsLoading[sName] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function os.unloadAPI(_sName)
|
||||||
|
expect(1, _sName, "string")
|
||||||
|
if _sName ~= "_G" and type(_G[_sName]) == "table" then
|
||||||
|
_G[_sName] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function os.sleep(nTime)
|
||||||
|
sleep(nTime)
|
||||||
|
end
|
||||||
|
|
||||||
|
local nativeShutdown = os.shutdown
|
||||||
|
function os.shutdown()
|
||||||
|
nativeShutdown()
|
||||||
|
while true do
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local nativeReboot = os.reboot
|
||||||
|
function os.reboot()
|
||||||
|
nativeReboot()
|
||||||
|
while true do
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local bAPIError = false
|
||||||
|
|
||||||
|
local function load_apis(dir)
|
||||||
|
if not fs.isDir(dir) then return end
|
||||||
|
|
||||||
|
for _, file in ipairs(fs.list(dir)) do
|
||||||
|
if file:sub(1, 1) ~= "." then
|
||||||
|
local path = fs.combine(dir, file)
|
||||||
|
if not fs.isDir(path) then
|
||||||
|
if not os.loadAPI(path) then
|
||||||
|
bAPIError = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load APIs
|
||||||
|
load_apis("rom/apis")
|
||||||
|
if http then load_apis("rom/apis/http") end
|
||||||
|
if turtle then load_apis("rom/apis/turtle") end
|
||||||
|
if pocket then load_apis("rom/apis/pocket") end
|
||||||
|
|
||||||
|
if commands and fs.isDir("rom/apis/command") then
|
||||||
|
-- Load command APIs
|
||||||
|
if os.loadAPI("rom/apis/command/commands.lua") then
|
||||||
|
-- Add a special case-insensitive metatable to the commands api
|
||||||
|
local tCaseInsensitiveMetatable = {
|
||||||
|
__index = function(table, key)
|
||||||
|
local value = rawget(table, key)
|
||||||
|
if value ~= nil then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
if type(key) == "string" then
|
||||||
|
local value = rawget(table, string.lower(key))
|
||||||
|
if value ~= nil then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
setmetatable(commands, tCaseInsensitiveMetatable)
|
||||||
|
setmetatable(commands.async, tCaseInsensitiveMetatable)
|
||||||
|
|
||||||
|
-- Add global "exec" function
|
||||||
|
exec = commands.exec
|
||||||
|
else
|
||||||
|
bAPIError = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if bAPIError then
|
||||||
|
print("Press any key to continue")
|
||||||
|
os.pullEvent("key")
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set default settings
|
||||||
|
settings.define("shell.allow_startup", {
|
||||||
|
default = true,
|
||||||
|
description = "Run startup files when the computer turns on.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("shell.allow_disk_startup", {
|
||||||
|
default = commands == nil,
|
||||||
|
description = "Run startup files from disk drives when the computer turns on.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("shell.autocomplete", {
|
||||||
|
default = true,
|
||||||
|
description = "Autocomplete program and arguments in the shell.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("edit.autocomplete", {
|
||||||
|
default = true,
|
||||||
|
description = "Autocomplete API and function names in the editor.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("lua.autocomplete", {
|
||||||
|
default = true,
|
||||||
|
description = "Autocomplete API and function names in the Lua REPL.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("edit.default_extension", {
|
||||||
|
default = "lua",
|
||||||
|
description = [[The file extension the editor will use if none is given. Set to "" to disable.]],
|
||||||
|
type = "string",
|
||||||
|
})
|
||||||
|
settings.define("paint.default_extension", {
|
||||||
|
default = "nfp",
|
||||||
|
description = [[The file extension the paint program will use if none is given. Set to "" to disable.]],
|
||||||
|
type = "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("list.show_hidden", {
|
||||||
|
default = false,
|
||||||
|
description = [[Show hidden files (those starting with "." in the Lua REPL).]],
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("motd.enable", {
|
||||||
|
default = pocket == nil,
|
||||||
|
description = "Display a random message when the computer starts up.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("motd.path", {
|
||||||
|
default = "/rom/motd.txt:/motd.txt",
|
||||||
|
description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]],
|
||||||
|
type = "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("lua.warn_against_use_of_local", {
|
||||||
|
default = true,
|
||||||
|
description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessible on the next input.]],
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("lua.function_args", {
|
||||||
|
default = true,
|
||||||
|
description = "Show function arguments when printing functions.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("lua.function_source", {
|
||||||
|
default = false,
|
||||||
|
description = "Show where a function was defined when printing functions.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("bios.strict_globals", {
|
||||||
|
default = false,
|
||||||
|
description = "Prevents assigning variables into a program's environment. Make sure you use the local keyword or assign to _G explicitly.",
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
settings.define("shell.autocomplete_hidden", {
|
||||||
|
default = false,
|
||||||
|
description = [[Autocomplete hidden files and folders (those starting with ".").]],
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
|
||||||
|
if term.isColour() then
|
||||||
|
settings.define("bios.use_multishell", {
|
||||||
|
default = true,
|
||||||
|
description = [[Allow running multiple programs at once, through the use of the "fg" and "bg" programs.]],
|
||||||
|
type = "boolean",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
if _CC_DEFAULT_SETTINGS then
|
||||||
|
for sPair in string.gmatch(_CC_DEFAULT_SETTINGS, "[^,]+") do
|
||||||
|
local sName, sValue = string.match(sPair, "([^=]*)=(.*)")
|
||||||
|
if sName and sValue then
|
||||||
|
local value
|
||||||
|
if sValue == "true" then
|
||||||
|
value = true
|
||||||
|
elseif sValue == "false" then
|
||||||
|
value = false
|
||||||
|
elseif sValue == "nil" then
|
||||||
|
value = nil
|
||||||
|
elseif tonumber(sValue) then
|
||||||
|
value = tonumber(sValue)
|
||||||
|
else
|
||||||
|
value = sValue
|
||||||
|
end
|
||||||
|
if value ~= nil then
|
||||||
|
settings.set(sName, value)
|
||||||
|
else
|
||||||
|
settings.unset(sName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load user settings
|
||||||
|
if fs.exists(".settings") then
|
||||||
|
settings.load(".settings")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run the shell
|
||||||
|
local ok, err = pcall(parallel.waitForAny,
|
||||||
|
function()
|
||||||
|
local sShell
|
||||||
|
if term.isColour() and settings.get("bios.use_multishell") then
|
||||||
|
sShell = "rom/programs/advanced/multishell.lua"
|
||||||
|
else
|
||||||
|
sShell = "rom/programs/shell.lua"
|
||||||
|
end
|
||||||
|
os.run({}, sShell)
|
||||||
|
os.run({}, "rom/programs/shutdown.lua")
|
||||||
|
end,
|
||||||
|
rednet.run
|
||||||
|
)
|
||||||
|
|
||||||
|
-- If the shell errored, let the user read it.
|
||||||
|
term.redirect(term.native())
|
||||||
|
if not ok then
|
||||||
|
printError(err)
|
||||||
|
pcall(function()
|
||||||
|
term.setCursorBlink(false)
|
||||||
|
print("Press any key to continue")
|
||||||
|
os.pullEvent("key")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- End
|
||||||
|
os.shutdown()
|
395
src/main/resources/assets/cctweaked/lua/rom/apis/colors.lua
Normal file
395
src/main/resources/assets/cctweaked/lua/rom/apis/colors.lua
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Constants and functions for colour values, suitable for working with
|
||||||
|
@{term} and @{redstone}.
|
||||||
|
|
||||||
|
This is useful in conjunction with @{redstone.setBundledOutput|Bundled Cables}
|
||||||
|
from mods like Project Red, and @{term.setTextColour|colors on Advanced
|
||||||
|
Computers and Advanced Monitors}.
|
||||||
|
|
||||||
|
For the non-American English version just replace @{colors} with @{colours}.
|
||||||
|
This alternative API is exactly the same, except the colours use British English
|
||||||
|
(e.g. @{colors.gray} is spelt @{colours.grey}).
|
||||||
|
|
||||||
|
On basic terminals (such as the Computer and Monitor), all the colors are
|
||||||
|
converted to grayscale. This means you can still use all 16 colors on the
|
||||||
|
screen, but they will appear as the nearest tint of gray. You can check if a
|
||||||
|
terminal supports color by using the function @{term.isColor}.
|
||||||
|
|
||||||
|
Grayscale colors are calculated by taking the average of the three components,
|
||||||
|
i.e. `(red + green + blue) / 3`.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th colspan="8" align="center">Default Colors</th></tr>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2" align="center">Color</th>
|
||||||
|
<th colspan="3" align="center">Value</th>
|
||||||
|
<th colspan="4" align="center">Default Palette Color</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Dec</th><th>Hex</th><th>Paint/Blit</th>
|
||||||
|
<th>Preview</th><th>Hex</th><th>RGB</th><th>Grayscale</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.white</code></td>
|
||||||
|
<td align="right">1</td><td align="right">0x1</td><td align="right">0</td>
|
||||||
|
<td style="background:#F0F0F0"></td><td>#F0F0F0</td><td>240, 240, 240</td>
|
||||||
|
<td style="background:#F0F0F0"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.orange</code></td>
|
||||||
|
<td align="right">2</td><td align="right">0x2</td><td align="right">1</td>
|
||||||
|
<td style="background:#F2B233"></td><td>#F2B233</td><td>242, 178, 51</td>
|
||||||
|
<td style="background:#9D9D9D"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.magenta</code></td>
|
||||||
|
<td align="right">4</td><td align="right">0x4</td><td align="right">2</td>
|
||||||
|
<td style="background:#E57FD8"></td><td>#E57FD8</td><td>229, 127, 216</td>
|
||||||
|
<td style="background:#BEBEBE"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.lightBlue</code></td>
|
||||||
|
<td align="right">8</td><td align="right">0x8</td><td align="right">3</td>
|
||||||
|
<td style="background:#99B2F2"></td><td>#99B2F2</td><td>153, 178, 242</td>
|
||||||
|
<td style="background:#BFBFBF"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.yellow</code></td>
|
||||||
|
<td align="right">16</td><td align="right">0x10</td><td align="right">4</td>
|
||||||
|
<td style="background:#DEDE6C"></td><td>#DEDE6C</td><td>222, 222, 108</td>
|
||||||
|
<td style="background:#B8B8B8"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.lime</code></td>
|
||||||
|
<td align="right">32</td><td align="right">0x20</td><td align="right">5</td>
|
||||||
|
<td style="background:#7FCC19"></td><td>#7FCC19</td><td>127, 204, 25</td>
|
||||||
|
<td style="background:#767676"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.pink</code></td>
|
||||||
|
<td align="right">64</td><td align="right">0x40</td><td align="right">6</td>
|
||||||
|
<td style="background:#F2B2CC"></td><td>#F2B2CC</td><td>242, 178, 204</td>
|
||||||
|
<td style="background:#D0D0D0"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.gray</code></td>
|
||||||
|
<td align="right">128</td><td align="right">0x80</td><td align="right">7</td>
|
||||||
|
<td style="background:#4C4C4C"></td><td>#4C4C4C</td><td>76, 76, 76</td>
|
||||||
|
<td style="background:#4C4C4C"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.lightGray</code></td>
|
||||||
|
<td align="right">256</td><td align="right">0x100</td><td align="right">8</td>
|
||||||
|
<td style="background:#999999"></td><td>#999999</td><td>153, 153, 153</td>
|
||||||
|
<td style="background:#999999"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.cyan</code></td>
|
||||||
|
<td align="right">512</td><td align="right">0x200</td><td align="right">9</td>
|
||||||
|
<td style="background:#4C99B2"></td><td>#4C99B2</td><td>76, 153, 178</td>
|
||||||
|
<td style="background:#878787"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.purple</code></td>
|
||||||
|
<td align="right">1024</td><td align="right">0x400</td><td align="right">a</td>
|
||||||
|
<td style="background:#B266E5"></td><td>#B266E5</td><td>178, 102, 229</td>
|
||||||
|
<td style="background:#A9A9A9"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.blue</code></td>
|
||||||
|
<td align="right">2048</td><td align="right">0x800</td><td align="right">b</td>
|
||||||
|
<td style="background:#3366CC"></td><td>#3366CC</td><td>51, 102, 204</td>
|
||||||
|
<td style="background:#777777"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.brown</code></td>
|
||||||
|
<td align="right">4096</td><td align="right">0x1000</td><td align="right">c</td>
|
||||||
|
<td style="background:#7F664C"></td><td>#7F664C</td><td>127, 102, 76</td>
|
||||||
|
<td style="background:#656565"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.green</code></td>
|
||||||
|
<td align="right">8192</td><td align="right">0x2000</td><td align="right">d</td>
|
||||||
|
<td style="background:#57A64E"></td><td>#57A64E</td><td>87, 166, 78</td>
|
||||||
|
<td style="background:#6E6E6E"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.red</code></td>
|
||||||
|
<td align="right">16384</td><td align="right">0x4000</td><td align="right">e</td>
|
||||||
|
<td style="background:#CC4C4C"></td><td>#CC4C4C</td><td>204, 76, 76</td>
|
||||||
|
<td style="background:#767676"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>colors.black</code></td>
|
||||||
|
<td align="right">32768</td><td align="right">0x8000</td><td align="right">f</td>
|
||||||
|
<td style="background:#111111"></td><td>#111111</td><td>17, 17, 17</td>
|
||||||
|
<td style="background:#111111"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
@see colours
|
||||||
|
@module colors
|
||||||
|
]]
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
--- White: Written as `0` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #F0F0F0.
|
||||||
|
white = 0x1
|
||||||
|
|
||||||
|
--- Orange: Written as `1` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #F2B233.
|
||||||
|
orange = 0x2
|
||||||
|
|
||||||
|
--- Magenta: Written as `2` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #E57FD8.
|
||||||
|
magenta = 0x4
|
||||||
|
|
||||||
|
--- Light blue: Written as `3` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #99B2F2.
|
||||||
|
lightBlue = 0x8
|
||||||
|
|
||||||
|
--- Yellow: Written as `4` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #DEDE6C.
|
||||||
|
yellow = 0x10
|
||||||
|
|
||||||
|
--- Lime: Written as `5` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #7FCC19.
|
||||||
|
lime = 0x20
|
||||||
|
|
||||||
|
--- Pink: Written as `6` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #F2B2CC.
|
||||||
|
pink = 0x40
|
||||||
|
|
||||||
|
--- Gray: Written as `7` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #4C4C4C.
|
||||||
|
gray = 0x80
|
||||||
|
|
||||||
|
--- Light gray: Written as `8` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #999999.
|
||||||
|
lightGray = 0x100
|
||||||
|
|
||||||
|
--- Cyan: Written as `9` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #4C99B2.
|
||||||
|
cyan = 0x200
|
||||||
|
|
||||||
|
--- Purple: Written as `a` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #B266E5.
|
||||||
|
purple = 0x400
|
||||||
|
|
||||||
|
--- Blue: Written as `b` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #3366CC.
|
||||||
|
blue = 0x800
|
||||||
|
|
||||||
|
--- Brown: Written as `c` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #7F664C.
|
||||||
|
brown = 0x1000
|
||||||
|
|
||||||
|
--- Green: Written as `d` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #57A64E.
|
||||||
|
green = 0x2000
|
||||||
|
|
||||||
|
--- Red: Written as `e` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #CC4C4C.
|
||||||
|
red = 0x4000
|
||||||
|
|
||||||
|
--- Black: Written as `f` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #111111.
|
||||||
|
black = 0x8000
|
||||||
|
|
||||||
|
--- Combines a set of colors (or sets of colors) into a larger set. Useful for
|
||||||
|
-- Bundled Cables.
|
||||||
|
--
|
||||||
|
-- @tparam number ... The colors to combine.
|
||||||
|
-- @treturn number The union of the color sets given in `...`
|
||||||
|
-- @since 1.2
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colors.combine(colors.white, colors.magenta, colours.lightBlue)
|
||||||
|
-- -- => 13
|
||||||
|
-- ```
|
||||||
|
function combine(...)
|
||||||
|
local r = 0
|
||||||
|
for i = 1, select('#', ...) do
|
||||||
|
local c = select(i, ...)
|
||||||
|
expect(i, c, "number")
|
||||||
|
r = bit32.bor(r, c)
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Removes one or more colors (or sets of colors) from an initial set. Useful
|
||||||
|
-- for Bundled Cables.
|
||||||
|
--
|
||||||
|
-- Each parameter beyond the first may be a single color or may be a set of
|
||||||
|
-- colors (in the latter case, all colors in the set are removed from the
|
||||||
|
-- original set).
|
||||||
|
--
|
||||||
|
-- @tparam number colors The color from which to subtract.
|
||||||
|
-- @tparam number ... The colors to subtract.
|
||||||
|
-- @treturn number The resulting color.
|
||||||
|
-- @since 1.2
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colours.subtract(colours.lime, colours.orange, colours.white)
|
||||||
|
-- -- => 32
|
||||||
|
-- ```
|
||||||
|
function subtract(colors, ...)
|
||||||
|
expect(1, colors, "number")
|
||||||
|
local r = colors
|
||||||
|
for i = 1, select('#', ...) do
|
||||||
|
local c = select(i, ...)
|
||||||
|
expect(i + 1, c, "number")
|
||||||
|
r = bit32.band(r, bit32.bnot(c))
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Tests whether `color` is contained within `colors`. Useful for Bundled
|
||||||
|
-- Cables.
|
||||||
|
--
|
||||||
|
-- @tparam number colors A color, or color set
|
||||||
|
-- @tparam number color A color or set of colors that `colors` should contain.
|
||||||
|
-- @treturn boolean If `colors` contains all colors within `color`.
|
||||||
|
-- @since 1.2
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colors.test(colors.combine(colors.white, colors.magenta, colours.lightBlue), colors.lightBlue)
|
||||||
|
-- -- => true
|
||||||
|
-- ```
|
||||||
|
function test(colors, color)
|
||||||
|
expect(1, colors, "number")
|
||||||
|
expect(2, color, "number")
|
||||||
|
return bit32.band(colors, color) == color
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Combine a three-colour RGB value into one hexadecimal representation.
|
||||||
|
--
|
||||||
|
-- @tparam number r The red channel, should be between 0 and 1.
|
||||||
|
-- @tparam number g The green channel, should be between 0 and 1.
|
||||||
|
-- @tparam number b The blue channel, should be between 0 and 1.
|
||||||
|
-- @treturn number The combined hexadecimal colour.
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colors.packRGB(0.7, 0.2, 0.6)
|
||||||
|
-- -- => 0xb23399
|
||||||
|
-- ```
|
||||||
|
-- @since 1.81.0
|
||||||
|
function packRGB(r, g, b)
|
||||||
|
expect(1, r, "number")
|
||||||
|
expect(2, g, "number")
|
||||||
|
expect(3, b, "number")
|
||||||
|
return
|
||||||
|
bit32.band(r * 255, 0xFF) * 2 ^ 16 +
|
||||||
|
bit32.band(g * 255, 0xFF) * 2 ^ 8 +
|
||||||
|
bit32.band(b * 255, 0xFF)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Separate a hexadecimal RGB colour into its three constituent channels.
|
||||||
|
--
|
||||||
|
-- @tparam number rgb The combined hexadecimal colour.
|
||||||
|
-- @treturn number The red channel, will be between 0 and 1.
|
||||||
|
-- @treturn number The green channel, will be between 0 and 1.
|
||||||
|
-- @treturn number The blue channel, will be between 0 and 1.
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colors.unpackRGB(0xb23399)
|
||||||
|
-- -- => 0.7, 0.2, 0.6
|
||||||
|
-- ```
|
||||||
|
-- @see colors.packRGB
|
||||||
|
-- @since 1.81.0
|
||||||
|
function unpackRGB(rgb)
|
||||||
|
expect(1, rgb, "number")
|
||||||
|
return
|
||||||
|
bit32.band(bit32.rshift(rgb, 16), 0xFF) / 255,
|
||||||
|
bit32.band(bit32.rshift(rgb, 8), 0xFF) / 255,
|
||||||
|
bit32.band(rgb, 0xFF) / 255
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Either calls @{colors.packRGB} or @{colors.unpackRGB}, depending on how many
|
||||||
|
-- arguments it receives.
|
||||||
|
--
|
||||||
|
-- @tparam[1] number r The red channel, as an argument to @{colors.packRGB}.
|
||||||
|
-- @tparam[1] number g The green channel, as an argument to @{colors.packRGB}.
|
||||||
|
-- @tparam[1] number b The blue channel, as an argument to @{colors.packRGB}.
|
||||||
|
-- @tparam[2] number rgb The combined hexadecimal color, as an argument to @{colors.unpackRGB}.
|
||||||
|
-- @treturn[1] number The combined hexadecimal colour, as returned by @{colors.packRGB}.
|
||||||
|
-- @treturn[2] number The red channel, as returned by @{colors.unpackRGB}
|
||||||
|
-- @treturn[2] number The green channel, as returned by @{colors.unpackRGB}
|
||||||
|
-- @treturn[2] number The blue channel, as returned by @{colors.unpackRGB}
|
||||||
|
-- @deprecated Use @{packRGB} or @{unpackRGB} directly.
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colors.rgb8(0xb23399)
|
||||||
|
-- -- => 0.7, 0.2, 0.6
|
||||||
|
-- ```
|
||||||
|
-- @usage
|
||||||
|
-- ```lua
|
||||||
|
-- colors.rgb8(0.7, 0.2, 0.6)
|
||||||
|
-- -- => 0xb23399
|
||||||
|
-- ```
|
||||||
|
-- @since 1.80pr1
|
||||||
|
-- @changed 1.81.0 Deprecated in favor of colors.(un)packRGB.
|
||||||
|
function rgb8(r, g, b)
|
||||||
|
if g == nil and b == nil then
|
||||||
|
return unpackRGB(r)
|
||||||
|
else
|
||||||
|
return packRGB(r, g, b)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Colour to hex lookup table for toBlit
|
||||||
|
local color_hex_lookup = {}
|
||||||
|
for i = 0, 15 do
|
||||||
|
color_hex_lookup[2 ^ i] = string.format("%x", i)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Converts the given color to a paint/blit hex character (0-9a-f).
|
||||||
|
|
||||||
|
This is equivalent to converting floor(log_2(color)) to hexadecimal.
|
||||||
|
|
||||||
|
@tparam number color The color to convert.
|
||||||
|
@treturn string The blit hex code of the color.
|
||||||
|
@usage
|
||||||
|
```lua
|
||||||
|
colors.toBlit(colors.red)
|
||||||
|
-- => "c"
|
||||||
|
```
|
||||||
|
@see colors.fromBlit
|
||||||
|
@since 1.94.0
|
||||||
|
]]
|
||||||
|
function toBlit(color)
|
||||||
|
expect(1, color, "number")
|
||||||
|
return color_hex_lookup[color] or string.format("%x", math.floor(math.log(color, 2)))
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Converts the given paint/blit hex character (0-9a-f) to a color.
|
||||||
|
|
||||||
|
This is equivalent to converting the hex character to a number and then 2 ^ decimal
|
||||||
|
|
||||||
|
@tparam string hex The paint/blit hex character to convert
|
||||||
|
@treturn number The color
|
||||||
|
@usage
|
||||||
|
```lua
|
||||||
|
colors.fromBlit("e")
|
||||||
|
-- => 16384
|
||||||
|
```
|
||||||
|
@see colors.toBlit
|
||||||
|
@since 1.105.0
|
||||||
|
]]
|
||||||
|
function fromBlit(hex)
|
||||||
|
expect(1, hex, "string")
|
||||||
|
|
||||||
|
if #hex ~= 1 then return nil end
|
||||||
|
local value = tonumber(hex, 16)
|
||||||
|
if not value then return nil end
|
||||||
|
|
||||||
|
return 2 ^ value
|
||||||
|
end
|
28
src/main/resources/assets/cctweaked/lua/rom/apis/colours.lua
Normal file
28
src/main/resources/assets/cctweaked/lua/rom/apis/colours.lua
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- An alternative version of @{colors} for lovers of British spelling.
|
||||||
|
--
|
||||||
|
-- @see colors
|
||||||
|
-- @module colours
|
||||||
|
-- @since 1.2
|
||||||
|
|
||||||
|
local colours = _ENV
|
||||||
|
for k, v in pairs(colors) do
|
||||||
|
colours[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Grey. Written as `7` in paint files and @{term.blit}, has a default
|
||||||
|
-- terminal colour of #4C4C4C.
|
||||||
|
--
|
||||||
|
-- @see colors.gray
|
||||||
|
colours.grey = colors.gray
|
||||||
|
colours.gray = nil --- @local
|
||||||
|
|
||||||
|
--- Light grey. Written as `8` in paint files and @{term.blit}, has a
|
||||||
|
-- default terminal colour of #999999.
|
||||||
|
--
|
||||||
|
-- @see colors.lightGray
|
||||||
|
colours.lightGrey = colors.lightGray
|
||||||
|
colours.lightGray = nil --- @local
|
@ -0,0 +1,121 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Execute [Minecraft commands][mc] and gather data from the results from
|
||||||
|
a command computer.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
This API is only available on Command computers. It is not accessible to normal
|
||||||
|
players.
|
||||||
|
:::
|
||||||
|
|
||||||
|
While one may use @{commands.exec} directly to execute a command, the
|
||||||
|
commands API also provides helper methods to execute every command. For
|
||||||
|
instance, `commands.say("Hi!")` is equivalent to `commands.exec("say Hi!")`.
|
||||||
|
|
||||||
|
@{commands.async} provides a similar interface to execute asynchronous
|
||||||
|
commands. `commands.async.say("Hi!")` is equivalent to
|
||||||
|
`commands.execAsync("say Hi!")`.
|
||||||
|
|
||||||
|
[mc]: https://minecraft.gamepedia.com/Commands
|
||||||
|
|
||||||
|
@module commands
|
||||||
|
@usage Set the block above this computer to stone:
|
||||||
|
|
||||||
|
commands.setblock("~", "~1", "~", "minecraft:stone")
|
||||||
|
]]
|
||||||
|
if not commands then
|
||||||
|
error("Cannot load command API on normal computer", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- The builtin commands API, without any generated command helper functions
|
||||||
|
--
|
||||||
|
-- This may be useful if a built-in function (such as @{commands.list}) has been
|
||||||
|
-- overwritten by a command.
|
||||||
|
local native = commands.native or commands
|
||||||
|
|
||||||
|
local function collapseArgs(bJSONIsNBT, ...)
|
||||||
|
local args = table.pack(...)
|
||||||
|
for i = 1, #args do
|
||||||
|
local arg = args[i]
|
||||||
|
if type(arg) == "boolean" or type(arg) == "number" or type(arg) == "string" then
|
||||||
|
args[i] = tostring(arg)
|
||||||
|
elseif type(arg) == "table" then
|
||||||
|
args[i] = textutils.serialiseJSON(arg, bJSONIsNBT)
|
||||||
|
else
|
||||||
|
error("Expected string, number, boolean or table", 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(args, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put native functions into the environment
|
||||||
|
local env = _ENV
|
||||||
|
env.native = native
|
||||||
|
for k, v in pairs(native) do
|
||||||
|
env[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create wrapper functions for all the commands
|
||||||
|
local tAsync = {}
|
||||||
|
local tNonNBTJSONCommands = {
|
||||||
|
["tellraw"] = true,
|
||||||
|
["title"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local command_mt = {}
|
||||||
|
function command_mt.__call(self, ...)
|
||||||
|
local meta = self[command_mt]
|
||||||
|
local sCommand = collapseArgs(meta.json, table.concat(meta.name, " "), ...)
|
||||||
|
return meta.func(sCommand)
|
||||||
|
end
|
||||||
|
|
||||||
|
function command_mt.__tostring(self)
|
||||||
|
local meta = self[command_mt]
|
||||||
|
return ("command %q"):format("/" .. table.concat(meta.name, " "))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mk_command(name, json, func)
|
||||||
|
return setmetatable({
|
||||||
|
[command_mt] = {
|
||||||
|
name = name,
|
||||||
|
func = func,
|
||||||
|
json = json,
|
||||||
|
},
|
||||||
|
}, command_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function command_mt.__index(self, key)
|
||||||
|
local meta = self[command_mt]
|
||||||
|
if meta.children then return nil end
|
||||||
|
meta.children = true
|
||||||
|
|
||||||
|
local name = meta.name
|
||||||
|
for _, child in ipairs(native.list(table.unpack(name))) do
|
||||||
|
local child_name = { table.unpack(name) }
|
||||||
|
child_name[#child_name + 1] = child
|
||||||
|
self[child] = mk_command(child_name, meta.json, meta.func)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, sCommandName in ipairs(native.list()) do
|
||||||
|
if env[sCommandName] == nil then
|
||||||
|
local bJSONIsNBT = tNonNBTJSONCommands[sCommandName] == nil
|
||||||
|
env[sCommandName] = mk_command({ sCommandName }, bJSONIsNBT, native.exec)
|
||||||
|
tAsync[sCommandName] = mk_command({ sCommandName }, bJSONIsNBT, native.execAsync)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- A table containing asynchronous wrappers for all commands.
|
||||||
|
--
|
||||||
|
-- As with @{commands.execAsync}, this returns the "task id" of the enqueued
|
||||||
|
-- command.
|
||||||
|
-- @see execAsync
|
||||||
|
-- @usage Asynchronously sets the block above the computer to stone.
|
||||||
|
--
|
||||||
|
-- commands.async.setblock("~", "~1", "~", "minecraft:stone")
|
||||||
|
env.async = tAsync
|
179
src/main/resources/assets/cctweaked/lua/rom/apis/disk.lua
Normal file
179
src/main/resources/assets/cctweaked/lua/rom/apis/disk.lua
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Interact with disk drives.
|
||||||
|
|
||||||
|
These functions can operate on locally attached or remote disk drives. To use a
|
||||||
|
locally attached drive, specify “side” as one of the six sides (e.g. `left`); to
|
||||||
|
use a remote disk drive, specify its name as printed when enabling its modem
|
||||||
|
(e.g. `drive_0`).
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
All computers (except command computers), turtles and pocket computers can be
|
||||||
|
placed within a disk drive to access it's internal storage like a disk.
|
||||||
|
:::
|
||||||
|
|
||||||
|
@module disk
|
||||||
|
@since 1.2
|
||||||
|
]]
|
||||||
|
|
||||||
|
local function isDrive(name)
|
||||||
|
if type(name) ~= "string" then
|
||||||
|
error("bad argument #1 (string expected, got " .. type(name) .. ")", 3)
|
||||||
|
end
|
||||||
|
return peripheral.getType(name) == "drive"
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Checks whether any item at all is in the disk drive
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn boolean If something is in the disk drive.
|
||||||
|
-- @usage disk.isPresent("top")
|
||||||
|
function isPresent(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "isDiskPresent")
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the label of the floppy disk, record, or other media within the given
|
||||||
|
-- disk drive.
|
||||||
|
--
|
||||||
|
-- If there is a computer or turtle within the drive, this will set the label as
|
||||||
|
-- read by `os.getComputerLabel`.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn string|nil The name of the current media, or `nil` if the drive is
|
||||||
|
-- not present or empty.
|
||||||
|
-- @see disk.setLabel
|
||||||
|
function getLabel(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "getDiskLabel")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the label of the floppy disk or other media
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @tparam string|nil label The new label of the disk
|
||||||
|
function setLabel(name, label)
|
||||||
|
if isDrive(name) then
|
||||||
|
peripheral.call(name, "setDiskLabel", label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Check whether the current disk provides a mount.
|
||||||
|
--
|
||||||
|
-- This will return true for disks and computers, but not records.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn boolean If the disk is present and provides a mount.
|
||||||
|
-- @see disk.getMountPath
|
||||||
|
function hasData(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "hasData")
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Find the directory name on the local computer where the contents of the
|
||||||
|
-- current floppy disk (or other mount) can be found.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn string|nil The mount's directory, or `nil` if the drive does not
|
||||||
|
-- contain a floppy or computer.
|
||||||
|
-- @see disk.hasData
|
||||||
|
function getMountPath(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "getMountPath")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Whether the current disk is a [music disk][disk] as opposed to a floppy disk
|
||||||
|
-- or other item.
|
||||||
|
--
|
||||||
|
-- If this returns true, you will can @{disk.playAudio|play} the record.
|
||||||
|
--
|
||||||
|
-- [disk]: https://minecraft.gamepedia.com/Music_Disc
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn boolean If the disk is present and has audio saved on it.
|
||||||
|
function hasAudio(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "hasAudio")
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the title of the audio track from the music record in the drive.
|
||||||
|
--
|
||||||
|
-- This generally returns the same as @{disk.getLabel} for records.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn string|false|nil The track title, @{false} if there is not a music
|
||||||
|
-- record in the drive or `nil` if no drive is present.
|
||||||
|
function getAudioTitle(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "getAudioTitle")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Starts playing the music record in the drive.
|
||||||
|
--
|
||||||
|
-- If any record is already playing on any disk drive, it stops before the
|
||||||
|
-- target drive starts playing. The record stops when it reaches the end of the
|
||||||
|
-- track, when it is removed from the drive, when @{disk.stopAudio} is called, or
|
||||||
|
-- when another record is started.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @usage disk.playAudio("bottom")
|
||||||
|
function playAudio(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
peripheral.call(name, "playAudio")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stops the music record in the drive from playing, if it was started with
|
||||||
|
-- @{disk.playAudio}.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name o the disk drive.
|
||||||
|
function stopAudio(name)
|
||||||
|
if not name then
|
||||||
|
for _, sName in ipairs(peripheral.getNames()) do
|
||||||
|
stopAudio(sName)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if isDrive(name) then
|
||||||
|
peripheral.call(name, "stopAudio")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Ejects any item currently in the drive, spilling it into the world as a loose item.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @usage disk.eject("bottom")
|
||||||
|
function eject(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
peripheral.call(name, "ejectDisk")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a number which uniquely identifies the disk in the drive.
|
||||||
|
--
|
||||||
|
-- Note, unlike @{disk.getLabel}, this does not return anything for other media,
|
||||||
|
-- such as computers or turtles.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the disk drive.
|
||||||
|
-- @treturn string|nil The disk ID, or `nil` if the drive does not contain a floppy disk.
|
||||||
|
-- @since 1.4
|
||||||
|
function getID(name)
|
||||||
|
if isDrive(name) then
|
||||||
|
return peripheral.call(name, "getDiskID")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
151
src/main/resources/assets/cctweaked/lua/rom/apis/fs.lua
Normal file
151
src/main/resources/assets/cctweaked/lua/rom/apis/fs.lua
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- @module fs
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua")
|
||||||
|
local expect, field = expect.expect, expect.field
|
||||||
|
|
||||||
|
local native = fs
|
||||||
|
|
||||||
|
local fs = _ENV
|
||||||
|
for k, v in pairs(native) do fs[k] = v end
|
||||||
|
|
||||||
|
--[[- Provides completion for a file or directory name, suitable for use with
|
||||||
|
@{_G.read}.
|
||||||
|
|
||||||
|
When a directory is a possible candidate for completion, two entries are
|
||||||
|
included - one with a trailing slash (indicating that entries within this
|
||||||
|
directory exist) and one without it (meaning this entry is an immediate
|
||||||
|
completion candidate). `include_dirs` can be set to @{false} to only include
|
||||||
|
those with a trailing slash.
|
||||||
|
|
||||||
|
@tparam[1] string path The path to complete.
|
||||||
|
@tparam[1] string location The location where paths are resolved from.
|
||||||
|
@tparam[1,opt=true] boolean include_files When @{false}, only directories will
|
||||||
|
be included in the returned list.
|
||||||
|
@tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will
|
||||||
|
not be included in the returned list.
|
||||||
|
|
||||||
|
@tparam[2] string path The path to complete.
|
||||||
|
@tparam[2] string location The location where paths are resolved from.
|
||||||
|
@tparam[2] {
|
||||||
|
include_dirs? = boolean, include_files? = boolean,
|
||||||
|
include_hidden? = boolean
|
||||||
|
} options
|
||||||
|
This table form is an expanded version of the previous syntax. The
|
||||||
|
`include_files` and `include_dirs` arguments from above are passed in as fields.
|
||||||
|
|
||||||
|
This table also accepts the following options:
|
||||||
|
- `include_hidden`: Whether to include hidden files (those starting with `.`)
|
||||||
|
by default. They will still be shown when typing a `.`.
|
||||||
|
|
||||||
|
@treturn { string... } A list of possible completion candidates.
|
||||||
|
@since 1.74
|
||||||
|
@changed 1.101.0
|
||||||
|
@usage Complete files in the root directory.
|
||||||
|
|
||||||
|
read(nil, nil, function(str)
|
||||||
|
return fs.complete(str, "", true, false)
|
||||||
|
end)
|
||||||
|
|
||||||
|
@usage Complete files in the root directory, hiding hidden files by default.
|
||||||
|
|
||||||
|
read(nil, nil, function(str)
|
||||||
|
return fs.complete(str, "", {
|
||||||
|
include_files = true,
|
||||||
|
include_dirs = false,
|
||||||
|
include_hidden = false,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
]]
|
||||||
|
function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
|
||||||
|
expect(1, sPath, "string")
|
||||||
|
expect(2, sLocation, "string")
|
||||||
|
local bIncludeHidden = nil
|
||||||
|
if type(bIncludeFiles) == "table" then
|
||||||
|
bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil")
|
||||||
|
bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil")
|
||||||
|
bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil")
|
||||||
|
else
|
||||||
|
expect(3, bIncludeFiles, "boolean", "nil")
|
||||||
|
expect(4, bIncludeDirs, "boolean", "nil")
|
||||||
|
end
|
||||||
|
|
||||||
|
bIncludeHidden = bIncludeHidden ~= false
|
||||||
|
bIncludeFiles = bIncludeFiles ~= false
|
||||||
|
bIncludeDirs = bIncludeDirs ~= false
|
||||||
|
local sDir = sLocation
|
||||||
|
local nStart = 1
|
||||||
|
local nSlash = string.find(sPath, "[/\\]", nStart)
|
||||||
|
if nSlash == 1 then
|
||||||
|
sDir = ""
|
||||||
|
nStart = 2
|
||||||
|
end
|
||||||
|
local sName
|
||||||
|
while not sName do
|
||||||
|
local nSlash = string.find(sPath, "[/\\]", nStart)
|
||||||
|
if nSlash then
|
||||||
|
local sPart = string.sub(sPath, nStart, nSlash - 1)
|
||||||
|
sDir = fs.combine(sDir, sPart)
|
||||||
|
nStart = nSlash + 1
|
||||||
|
else
|
||||||
|
sName = string.sub(sPath, nStart)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if fs.isDir(sDir) then
|
||||||
|
local tResults = {}
|
||||||
|
if bIncludeDirs and sPath == "" then
|
||||||
|
table.insert(tResults, ".")
|
||||||
|
end
|
||||||
|
if sDir ~= "" then
|
||||||
|
if sPath == "" then
|
||||||
|
table.insert(tResults, bIncludeDirs and ".." or "../")
|
||||||
|
elseif sPath == "." then
|
||||||
|
table.insert(tResults, bIncludeDirs and "." or "./")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local tFiles = fs.list(sDir)
|
||||||
|
for n = 1, #tFiles do
|
||||||
|
local sFile = tFiles[n]
|
||||||
|
if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and (
|
||||||
|
bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "."
|
||||||
|
) then
|
||||||
|
local bIsDir = fs.isDir(fs.combine(sDir, sFile))
|
||||||
|
local sResult = string.sub(sFile, #sName + 1)
|
||||||
|
if bIsDir then
|
||||||
|
table.insert(tResults, sResult .. "/")
|
||||||
|
if bIncludeDirs and #sResult > 0 then
|
||||||
|
table.insert(tResults, sResult)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if bIncludeFiles and #sResult > 0 then
|
||||||
|
table.insert(tResults, sResult)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return tResults
|
||||||
|
end
|
||||||
|
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns true if a path is mounted to the parent filesystem.
|
||||||
|
--
|
||||||
|
-- The root filesystem "/" is considered a mount, along with disk folders and
|
||||||
|
-- the rom folder. Other programs (such as network shares) can exstend this to
|
||||||
|
-- make other mount types by correctly assigning their return value for getDrive.
|
||||||
|
--
|
||||||
|
-- @tparam string path The path to check.
|
||||||
|
-- @treturn boolean If the path is mounted, rather than a normal file/folder.
|
||||||
|
-- @throws If the path does not exist.
|
||||||
|
-- @see getDrive
|
||||||
|
-- @since 1.87.0
|
||||||
|
function fs.isDriveRoot(sPath)
|
||||||
|
expect(1, sPath, "string")
|
||||||
|
-- Force the root directory to be a mount.
|
||||||
|
return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath))
|
||||||
|
end
|
218
src/main/resources/assets/cctweaked/lua/rom/apis/gps.lua
Normal file
218
src/main/resources/assets/cctweaked/lua/rom/apis/gps.lua
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Use @{modem|modems} to locate the position of the current turtle or
|
||||||
|
computers.
|
||||||
|
|
||||||
|
It broadcasts a PING message over @{rednet} and wait for responses. In order for
|
||||||
|
this system to work, there must be at least 4 computers used as gps hosts which
|
||||||
|
will respond and allow trilateration. Three of these hosts should be in a plane,
|
||||||
|
and the fourth should be either above or below the other three. The three in a
|
||||||
|
plane should not be in a line with each other. You can set up hosts using the
|
||||||
|
gps program.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
When entering in the coordinates for the host you need to put in the `x`, `y`,
|
||||||
|
and `z` coordinates of the block that the modem is connected to, not the modem.
|
||||||
|
All modem distances are measured from the block that the modem is placed on.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Also note that you may choose which axes x, y, or z refers to - so long as your
|
||||||
|
systems have the same definition as any GPS servers that're in range, it works
|
||||||
|
just the same. For example, you might build a GPS cluster according to [this
|
||||||
|
tutorial][1], using z to account for height, or you might use y to account for
|
||||||
|
height in the way that Minecraft's debug screen displays.
|
||||||
|
|
||||||
|
[1]: http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/
|
||||||
|
|
||||||
|
@module gps
|
||||||
|
@since 1.31
|
||||||
|
@see gps_setup For more detailed instructions on setting up GPS
|
||||||
|
]]
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
--- The channel which GPS requests and responses are broadcast on.
|
||||||
|
CHANNEL_GPS = 65534
|
||||||
|
|
||||||
|
local function trilaterate(A, B, C)
|
||||||
|
local a2b = B.vPosition - A.vPosition
|
||||||
|
local a2c = C.vPosition - A.vPosition
|
||||||
|
|
||||||
|
if math.abs(a2b:normalize():dot(a2c:normalize())) > 0.999 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local d = a2b:length()
|
||||||
|
local ex = a2b:normalize( )
|
||||||
|
local i = ex:dot(a2c)
|
||||||
|
local ey = (a2c - ex * i):normalize()
|
||||||
|
local j = ey:dot(a2c)
|
||||||
|
local ez = ex:cross(ey)
|
||||||
|
|
||||||
|
local r1 = A.nDistance
|
||||||
|
local r2 = B.nDistance
|
||||||
|
local r3 = C.nDistance
|
||||||
|
|
||||||
|
local x = (r1 * r1 - r2 * r2 + d * d) / (2 * d)
|
||||||
|
local y = (r1 * r1 - r3 * r3 - x * x + (x - i) * (x - i) + j * j) / (2 * j)
|
||||||
|
|
||||||
|
local result = A.vPosition + ex * x + ey * y
|
||||||
|
|
||||||
|
local zSquared = r1 * r1 - x * x - y * y
|
||||||
|
if zSquared > 0 then
|
||||||
|
local z = math.sqrt(zSquared)
|
||||||
|
local result1 = result + ez * z
|
||||||
|
local result2 = result - ez * z
|
||||||
|
|
||||||
|
local rounded1, rounded2 = result1:round(0.01), result2:round(0.01)
|
||||||
|
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
|
||||||
|
return rounded1, rounded2
|
||||||
|
else
|
||||||
|
return rounded1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result:round(0.01)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function narrow(p1, p2, fix)
|
||||||
|
local dist1 = math.abs((p1 - fix.vPosition):length() - fix.nDistance)
|
||||||
|
local dist2 = math.abs((p2 - fix.vPosition):length() - fix.nDistance)
|
||||||
|
|
||||||
|
if math.abs(dist1 - dist2) < 0.01 then
|
||||||
|
return p1, p2
|
||||||
|
elseif dist1 < dist2 then
|
||||||
|
return p1:round(0.01)
|
||||||
|
else
|
||||||
|
return p2:round(0.01)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Tries to retrieve the computer or turtles own location.
|
||||||
|
--
|
||||||
|
-- @tparam[opt=2] number timeout The maximum time in seconds taken to establish our
|
||||||
|
-- position.
|
||||||
|
-- @tparam[opt=false] boolean debug Print debugging messages
|
||||||
|
-- @treturn[1] number This computer's `x` position.
|
||||||
|
-- @treturn[1] number This computer's `y` position.
|
||||||
|
-- @treturn[1] number This computer's `z` position.
|
||||||
|
-- @treturn[2] nil If the position could not be established.
|
||||||
|
function locate(_nTimeout, _bDebug)
|
||||||
|
expect(1, _nTimeout, "number", "nil")
|
||||||
|
expect(2, _bDebug, "boolean", "nil")
|
||||||
|
-- Let command computers use their magic fourth-wall-breaking special abilities
|
||||||
|
if commands then
|
||||||
|
return commands.getBlockPosition()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find a modem
|
||||||
|
local sModemSide = nil
|
||||||
|
for _, sSide in ipairs(rs.getSides()) do
|
||||||
|
if peripheral.getType(sSide) == "modem" and peripheral.call(sSide, "isWireless") then
|
||||||
|
sModemSide = sSide
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if sModemSide == nil then
|
||||||
|
if _bDebug then
|
||||||
|
print("No wireless modem attached")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if _bDebug then
|
||||||
|
print("Finding position...")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Open GPS channel to listen for ping responses
|
||||||
|
local modem = peripheral.wrap(sModemSide)
|
||||||
|
local bCloseChannel = false
|
||||||
|
if not modem.isOpen(CHANNEL_GPS) then
|
||||||
|
modem.open(CHANNEL_GPS)
|
||||||
|
bCloseChannel = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Send a ping to listening GPS hosts
|
||||||
|
modem.transmit(CHANNEL_GPS, CHANNEL_GPS, "PING")
|
||||||
|
|
||||||
|
-- Wait for the responses
|
||||||
|
local tFixes = {}
|
||||||
|
local pos1, pos2 = nil, nil
|
||||||
|
local timeout = os.startTimer(_nTimeout or 2)
|
||||||
|
while true do
|
||||||
|
local e, p1, p2, p3, p4, p5 = os.pullEvent()
|
||||||
|
if e == "modem_message" then
|
||||||
|
-- We received a reply from a modem
|
||||||
|
local sSide, sChannel, sReplyChannel, tMessage, nDistance = p1, p2, p3, p4, p5
|
||||||
|
if sSide == sModemSide and sChannel == CHANNEL_GPS and sReplyChannel == CHANNEL_GPS and nDistance then
|
||||||
|
-- Received the correct message from the correct modem: use it to determine position
|
||||||
|
if type(tMessage) == "table" and #tMessage == 3 and tonumber(tMessage[1]) and tonumber(tMessage[2]) and tonumber(tMessage[3]) then
|
||||||
|
local tFix = { vPosition = vector.new(tMessage[1], tMessage[2], tMessage[3]), nDistance = nDistance }
|
||||||
|
if _bDebug then
|
||||||
|
print(tFix.nDistance .. " metres from " .. tostring(tFix.vPosition))
|
||||||
|
end
|
||||||
|
if tFix.nDistance == 0 then
|
||||||
|
pos1, pos2 = tFix.vPosition, nil
|
||||||
|
else
|
||||||
|
-- Insert our new position in our table, with a maximum of three items. If this is close to a
|
||||||
|
-- previous position, replace that instead of inserting.
|
||||||
|
local insIndex = math.min(3, #tFixes + 1)
|
||||||
|
for i, older in pairs(tFixes) do
|
||||||
|
if (older.vPosition - tFix.vPosition):length() < 1 then
|
||||||
|
insIndex = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tFixes[insIndex] = tFix
|
||||||
|
|
||||||
|
if #tFixes >= 3 then
|
||||||
|
if not pos1 then
|
||||||
|
pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[3])
|
||||||
|
else
|
||||||
|
pos1, pos2 = narrow(pos1, pos2, tFixes[3])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if pos1 and not pos2 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif e == "timer" then
|
||||||
|
-- We received a timeout
|
||||||
|
local timer = p1
|
||||||
|
if timer == timeout then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Close the channel, if we opened one
|
||||||
|
if bCloseChannel then
|
||||||
|
modem.close(CHANNEL_GPS)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the response
|
||||||
|
if pos1 and pos2 then
|
||||||
|
if _bDebug then
|
||||||
|
print("Ambiguous position")
|
||||||
|
print("Could be " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z .. " or " .. pos2.x .. "," .. pos2.y .. "," .. pos2.z)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
elseif pos1 then
|
||||||
|
if _bDebug then
|
||||||
|
print("Position is " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z)
|
||||||
|
end
|
||||||
|
return pos1.x, pos1.y, pos1.z
|
||||||
|
else
|
||||||
|
if _bDebug then
|
||||||
|
print("Could not determine position")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
118
src/main/resources/assets/cctweaked/lua/rom/apis/help.lua
Normal file
118
src/main/resources/assets/cctweaked/lua/rom/apis/help.lua
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Find help files on the current computer.
|
||||||
|
--
|
||||||
|
-- @module help
|
||||||
|
-- @since 1.2
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local sPath = "/rom/help"
|
||||||
|
|
||||||
|
--- Returns a colon-separated list of directories where help files are searched
|
||||||
|
-- for. All directories are absolute.
|
||||||
|
--
|
||||||
|
-- @treturn string The current help search path, separated by colons.
|
||||||
|
-- @see help.setPath
|
||||||
|
function path()
|
||||||
|
return sPath
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the colon-separated list of directories where help files are searched
|
||||||
|
-- for to `newPath`
|
||||||
|
--
|
||||||
|
-- @tparam string newPath The new path to use.
|
||||||
|
-- @usage help.setPath( "/disk/help/" )
|
||||||
|
-- @usage help.setPath( help.path() .. ":/myfolder/help/" )
|
||||||
|
-- @see help.path
|
||||||
|
function setPath(_sPath)
|
||||||
|
expect(1, _sPath, "string")
|
||||||
|
sPath = _sPath
|
||||||
|
end
|
||||||
|
|
||||||
|
local extensions = { "", ".md", ".txt" }
|
||||||
|
|
||||||
|
--- Returns the location of the help file for the given topic.
|
||||||
|
--
|
||||||
|
-- @tparam string topic The topic to find
|
||||||
|
-- @treturn string|nil The path to the given topic's help file, or `nil` if it
|
||||||
|
-- cannot be found.
|
||||||
|
-- @usage help.lookup("disk")
|
||||||
|
-- @changed 1.80pr1 Now supports finding .txt files.
|
||||||
|
-- @changed 1.97.0 Now supports finding Markdown files.
|
||||||
|
function lookup(topic)
|
||||||
|
expect(1, topic, "string")
|
||||||
|
-- Look on the path variable
|
||||||
|
for path in string.gmatch(sPath, "[^:]+") do
|
||||||
|
path = fs.combine(path, topic)
|
||||||
|
for _, extension in ipairs(extensions) do
|
||||||
|
local file = path .. extension
|
||||||
|
if fs.exists(file) and not fs.isDir(file) then
|
||||||
|
return file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Not found
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a list of topics that can be looked up and/or displayed.
|
||||||
|
--
|
||||||
|
-- @treturn table A list of topics in alphabetical order.
|
||||||
|
-- @usage help.topics()
|
||||||
|
function topics()
|
||||||
|
-- Add index
|
||||||
|
local tItems = {
|
||||||
|
["index"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Add topics from the path
|
||||||
|
for sPath in string.gmatch(sPath, "[^:]+") do
|
||||||
|
if fs.isDir(sPath) then
|
||||||
|
local tList = fs.list(sPath)
|
||||||
|
for _, sFile in pairs(tList) do
|
||||||
|
if string.sub(sFile, 1, 1) ~= "." then
|
||||||
|
if not fs.isDir(fs.combine(sPath, sFile)) then
|
||||||
|
for i = 2, #extensions do
|
||||||
|
local extension = extensions[i]
|
||||||
|
if #sFile > #extension and sFile:sub(-#extension) == extension then
|
||||||
|
sFile = sFile:sub(1, -#extension - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tItems[sFile] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort and return
|
||||||
|
local tItemList = {}
|
||||||
|
for sItem in pairs(tItems) do
|
||||||
|
table.insert(tItemList, sItem)
|
||||||
|
end
|
||||||
|
table.sort(tItemList)
|
||||||
|
return tItemList
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a list of topic endings that match the prefix. Can be used with
|
||||||
|
-- `read` to allow input of a help topic.
|
||||||
|
--
|
||||||
|
-- @tparam string prefix The prefix to match
|
||||||
|
-- @treturn table A list of matching topics.
|
||||||
|
-- @since 1.74
|
||||||
|
function completeTopic(sText)
|
||||||
|
expect(1, sText, "string")
|
||||||
|
local tTopics = topics()
|
||||||
|
local tResults = {}
|
||||||
|
for n = 1, #tTopics do
|
||||||
|
local sTopic = tTopics[n]
|
||||||
|
if #sTopic > #sText and string.sub(sTopic, 1, #sText) == sText then
|
||||||
|
table.insert(tResults, string.sub(sTopic, #sText + 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return tResults
|
||||||
|
end
|
380
src/main/resources/assets/cctweaked/lua/rom/apis/http/http.lua
Normal file
380
src/main/resources/assets/cctweaked/lua/rom/apis/http/http.lua
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Make HTTP requests, sending and receiving data to a remote web server.
|
||||||
|
|
||||||
|
@module http
|
||||||
|
@since 1.1
|
||||||
|
@see local_ips To allow accessing servers running on your local network.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local native = http
|
||||||
|
local nativeHTTPRequest = http.request
|
||||||
|
|
||||||
|
local methods = {
|
||||||
|
GET = true, POST = true, HEAD = true,
|
||||||
|
OPTIONS = true, PUT = true, DELETE = true,
|
||||||
|
PATCH = true, TRACE = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function check_key(options, key, ty, opt)
|
||||||
|
local value = options[key]
|
||||||
|
local valueTy = type(value)
|
||||||
|
|
||||||
|
if (value ~= nil or not opt) and valueTy ~= ty then
|
||||||
|
error(("bad field '%s' (%s expected, got %s"):format(key, ty, valueTy), 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_request_options(options, body)
|
||||||
|
check_key(options, "url", "string")
|
||||||
|
if body == false then
|
||||||
|
check_key(options, "body", "nil")
|
||||||
|
else
|
||||||
|
check_key(options, "body", "string", not body)
|
||||||
|
end
|
||||||
|
check_key(options, "headers", "table", true)
|
||||||
|
check_key(options, "method", "string", true)
|
||||||
|
check_key(options, "redirect", "boolean", true)
|
||||||
|
check_key(options, "timeout", "number", true)
|
||||||
|
|
||||||
|
if options.method and not methods[options.method] then
|
||||||
|
error("Unsupported HTTP method", 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrap_request(_url, ...)
|
||||||
|
local ok, err = nativeHTTPRequest(...)
|
||||||
|
if ok then
|
||||||
|
while true do
|
||||||
|
local event, param1, param2, param3 = os.pullEvent()
|
||||||
|
if event == "http_success" and param1 == _url then
|
||||||
|
return param2
|
||||||
|
elseif event == "http_failure" and param1 == _url then
|
||||||
|
return nil, param2, param3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Make a HTTP GET request to the given url.
|
||||||
|
|
||||||
|
@tparam string url The url to request
|
||||||
|
@tparam[opt] { [string] = string } headers Additional headers to send as part
|
||||||
|
of this request.
|
||||||
|
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
|
||||||
|
the body will not be UTF-8 encoded, and the received response will not be
|
||||||
|
decoded.
|
||||||
|
|
||||||
|
@tparam[2] {
|
||||||
|
url = string, headers? = { [string] = string },
|
||||||
|
binary? = boolean, method? = string, redirect? = boolean,
|
||||||
|
timeout? = number,
|
||||||
|
} request Options for the request. See @{http.request} for details on how
|
||||||
|
these options behave.
|
||||||
|
|
||||||
|
@treturn Response The resulting http response, which can be read from.
|
||||||
|
@treturn[2] nil When the http request failed, such as in the event of a 404
|
||||||
|
error or connection timeout.
|
||||||
|
@treturn string A message detailing why the request failed.
|
||||||
|
@treturn Response|nil The failing http response, if available.
|
||||||
|
|
||||||
|
@changed 1.63 Added argument for headers.
|
||||||
|
@changed 1.80pr1 Response handles are now returned on error if available.
|
||||||
|
@changed 1.80pr1 Added argument for binary handles.
|
||||||
|
@changed 1.80pr1.6 Added support for table argument.
|
||||||
|
@changed 1.86.0 Added PATCH and TRACE methods.
|
||||||
|
@changed 1.105.0 Added support for custom timeouts.
|
||||||
|
|
||||||
|
@usage Make a request to [example.tweaked.cc](https://example.tweaked.cc),
|
||||||
|
and print the returned page.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local request = http.get("https://example.tweaked.cc")
|
||||||
|
print(request.readAll())
|
||||||
|
-- => HTTP is working!
|
||||||
|
request.close()
|
||||||
|
```
|
||||||
|
]]
|
||||||
|
function get(_url, _headers, _binary)
|
||||||
|
if type(_url) == "table" then
|
||||||
|
check_request_options(_url, false)
|
||||||
|
return wrap_request(_url.url, _url)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(1, _url, "string")
|
||||||
|
expect(2, _headers, "table", "nil")
|
||||||
|
expect(3, _binary, "boolean", "nil")
|
||||||
|
return wrap_request(_url, _url, nil, _headers, _binary)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Make a HTTP POST request to the given url.
|
||||||
|
|
||||||
|
@tparam string url The url to request
|
||||||
|
@tparam string body The body of the POST request.
|
||||||
|
@tparam[opt] { [string] = string } headers Additional headers to send as part
|
||||||
|
of this request.
|
||||||
|
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
|
||||||
|
the body will not be UTF-8 encoded, and the received response will not be
|
||||||
|
decoded.
|
||||||
|
|
||||||
|
@tparam[2] {
|
||||||
|
url = string, body? = string, headers? = { [string] = string },
|
||||||
|
binary? = boolean, method? = string, redirect? = boolean,
|
||||||
|
timeout? = number,
|
||||||
|
} request Options for the request. See @{http.request} for details on how
|
||||||
|
these options behave.
|
||||||
|
|
||||||
|
@treturn Response The resulting http response, which can be read from.
|
||||||
|
@treturn[2] nil When the http request failed, such as in the event of a 404
|
||||||
|
error or connection timeout.
|
||||||
|
@treturn string A message detailing why the request failed.
|
||||||
|
@treturn Response|nil The failing http response, if available.
|
||||||
|
|
||||||
|
@since 1.31
|
||||||
|
@changed 1.63 Added argument for headers.
|
||||||
|
@changed 1.80pr1 Response handles are now returned on error if available.
|
||||||
|
@changed 1.80pr1 Added argument for binary handles.
|
||||||
|
@changed 1.80pr1.6 Added support for table argument.
|
||||||
|
@changed 1.86.0 Added PATCH and TRACE methods.
|
||||||
|
@changed 1.105.0 Added support for custom timeouts.
|
||||||
|
]]
|
||||||
|
function post(_url, _post, _headers, _binary)
|
||||||
|
if type(_url) == "table" then
|
||||||
|
check_request_options(_url, true)
|
||||||
|
return wrap_request(_url.url, _url)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(1, _url, "string")
|
||||||
|
expect(2, _post, "string")
|
||||||
|
expect(3, _headers, "table", "nil")
|
||||||
|
expect(4, _binary, "boolean", "nil")
|
||||||
|
return wrap_request(_url, _url, _post, _headers, _binary)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Asynchronously make a HTTP request to the given url.
|
||||||
|
|
||||||
|
This returns immediately, a @{http_success} or @{http_failure} will be queued
|
||||||
|
once the request has completed.
|
||||||
|
|
||||||
|
@tparam string url The url to request
|
||||||
|
@tparam[opt] string body An optional string containing the body of the
|
||||||
|
request. If specified, a `POST` request will be made instead.
|
||||||
|
@tparam[opt] { [string] = string } headers Additional headers to send as part
|
||||||
|
of this request.
|
||||||
|
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
|
||||||
|
the body will not be UTF-8 encoded, and the received response will not be
|
||||||
|
decoded.
|
||||||
|
|
||||||
|
@tparam[2] {
|
||||||
|
url = string, body? = string, headers? = { [string] = string },
|
||||||
|
binary? = boolean, method? = string, redirect? = boolean,
|
||||||
|
timeout? = number,
|
||||||
|
} request Options for the request.
|
||||||
|
|
||||||
|
This table form is an expanded version of the previous syntax. All arguments
|
||||||
|
from above are passed in as fields instead (for instance,
|
||||||
|
`http.request("https://example.com")` becomes `http.request { url =
|
||||||
|
"https://example.com" }`).
|
||||||
|
This table also accepts several additional options:
|
||||||
|
|
||||||
|
- `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`.
|
||||||
|
- `redirect`: Whether to follow HTTP redirects. Defaults to true.
|
||||||
|
- `timeout`: The connection timeout, in seconds.
|
||||||
|
|
||||||
|
@see http.get For a synchronous way to make GET requests.
|
||||||
|
@see http.post For a synchronous way to make POST requests.
|
||||||
|
|
||||||
|
@changed 1.63 Added argument for headers.
|
||||||
|
@changed 1.80pr1 Added argument for binary handles.
|
||||||
|
@changed 1.80pr1.6 Added support for table argument.
|
||||||
|
@changed 1.86.0 Added PATCH and TRACE methods.
|
||||||
|
@changed 1.105.0 Added support for custom timeouts.
|
||||||
|
]]
|
||||||
|
function request(_url, _post, _headers, _binary)
|
||||||
|
local url
|
||||||
|
if type(_url) == "table" then
|
||||||
|
check_request_options(_url)
|
||||||
|
url = _url.url
|
||||||
|
else
|
||||||
|
expect(1, _url, "string")
|
||||||
|
expect(2, _post, "string", "nil")
|
||||||
|
expect(3, _headers, "table", "nil")
|
||||||
|
expect(4, _binary, "boolean", "nil")
|
||||||
|
url = _url
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = nativeHTTPRequest(_url, _post, _headers, _binary)
|
||||||
|
if not ok then
|
||||||
|
os.queueEvent("http_failure", url, err)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on.
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local nativeCheckURL = native.checkURL
|
||||||
|
|
||||||
|
--[[- Asynchronously determine whether a URL can be requested.
|
||||||
|
|
||||||
|
If this returns `true`, one should also listen for @{http_check} which will
|
||||||
|
container further information about whether the URL is allowed or not.
|
||||||
|
|
||||||
|
@tparam string url The URL to check.
|
||||||
|
@treturn true When this url is not invalid. This does not imply that it is
|
||||||
|
allowed - see the comment above.
|
||||||
|
@treturn[2] false When this url is invalid.
|
||||||
|
@treturn string A reason why this URL is not valid (for instance, if it is
|
||||||
|
malformed, or blocked).
|
||||||
|
|
||||||
|
@see http.checkURL For a synchronous version.
|
||||||
|
]]
|
||||||
|
checkURLAsync = nativeCheckURL
|
||||||
|
|
||||||
|
--[[- Determine whether a URL can be requested.
|
||||||
|
|
||||||
|
If this returns `true`, one should also listen for @{http_check} which will
|
||||||
|
container further information about whether the URL is allowed or not.
|
||||||
|
|
||||||
|
@tparam string url The URL to check.
|
||||||
|
@treturn true When this url is valid and can be requested via @{http.request}.
|
||||||
|
@treturn[2] false When this url is invalid.
|
||||||
|
@treturn string A reason why this URL is not valid (for instance, if it is
|
||||||
|
malformed, or blocked).
|
||||||
|
|
||||||
|
@see http.checkURLAsync For an asynchronous version.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
```lua
|
||||||
|
print(http.checkURL("https://example.tweaked.cc/"))
|
||||||
|
-- => true
|
||||||
|
print(http.checkURL("http://localhost/"))
|
||||||
|
-- => false Domain not permitted
|
||||||
|
print(http.checkURL("not a url"))
|
||||||
|
-- => false URL malformed
|
||||||
|
```
|
||||||
|
]]
|
||||||
|
function checkURL(_url)
|
||||||
|
expect(1, _url, "string")
|
||||||
|
local ok, err = nativeCheckURL(_url)
|
||||||
|
if not ok then return ok, err end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local _, url, ok, err = os.pullEvent("http_check")
|
||||||
|
if url == _url then return ok, err end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local nativeWebsocket = native.websocket
|
||||||
|
|
||||||
|
local function check_websocket_options(options, body)
|
||||||
|
check_key(options, "url", "string")
|
||||||
|
check_key(options, "headers", "table", true)
|
||||||
|
check_key(options, "timeout", "number", true)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--[[- Asynchronously open a websocket.
|
||||||
|
|
||||||
|
This returns immediately, a @{websocket_success} or @{websocket_failure}
|
||||||
|
will be queued once the request has completed.
|
||||||
|
|
||||||
|
@tparam[1] string url The websocket url to connect to. This should have the
|
||||||
|
`ws://` or `wss://` protocol.
|
||||||
|
@tparam[1, opt] { [string] = string } headers Additional headers to send as part
|
||||||
|
of the initial websocket connection.
|
||||||
|
|
||||||
|
@tparam[2] {
|
||||||
|
url = string, headers? = { [string] = string }, timeout ?= number,
|
||||||
|
} request Options for the websocket. See @{http.websocket} for details on how
|
||||||
|
these options behave.
|
||||||
|
|
||||||
|
@since 1.80pr1.3
|
||||||
|
@changed 1.95.3 Added User-Agent to default headers.
|
||||||
|
@changed 1.105.0 Added support for table argument and custom timeout.
|
||||||
|
@see websocket_success
|
||||||
|
@see websocket_failure
|
||||||
|
]]
|
||||||
|
function websocketAsync(url, headers)
|
||||||
|
local actual_url
|
||||||
|
if type(url) == "table" then
|
||||||
|
check_websocket_options(url)
|
||||||
|
actual_url = url.url
|
||||||
|
else
|
||||||
|
expect(1, url, "string")
|
||||||
|
expect(2, headers, "table", "nil")
|
||||||
|
actual_url = url
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = nativeWebsocket(url, headers)
|
||||||
|
if not ok then
|
||||||
|
os.queueEvent("websocket_failure", actual_url, err)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on.
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Open a websocket.
|
||||||
|
|
||||||
|
@tparam[1] string url The websocket url to connect to. This should have the
|
||||||
|
`ws://` or `wss://` protocol.
|
||||||
|
@tparam[1,opt] { [string] = string } headers Additional headers to send as part
|
||||||
|
of the initial websocket connection.
|
||||||
|
|
||||||
|
@tparam[2] {
|
||||||
|
url = string, headers? = { [string] = string }, timeout ?= number,
|
||||||
|
} request Options for the websocket.
|
||||||
|
|
||||||
|
This table form is an expanded version of the previous syntax. All arguments
|
||||||
|
from above are passed in as fields instead (for instance,
|
||||||
|
`http.websocket("https://example.com")` becomes `http.websocket { url =
|
||||||
|
"https://example.com" }`).
|
||||||
|
This table also accepts the following additional options:
|
||||||
|
|
||||||
|
- `timeout`: The connection timeout, in seconds.
|
||||||
|
|
||||||
|
@treturn Websocket The websocket connection.
|
||||||
|
@treturn[2] false If the websocket connection failed.
|
||||||
|
@treturn string An error message describing why the connection failed.
|
||||||
|
|
||||||
|
@since 1.80pr1.1
|
||||||
|
@changed 1.80pr1.3 No longer asynchronous.
|
||||||
|
@changed 1.95.3 Added User-Agent to default headers.
|
||||||
|
@changed 1.105.0 Added support for table argument and custom timeout.
|
||||||
|
|
||||||
|
@usage Connect to an echo websocket and send a message.
|
||||||
|
|
||||||
|
local ws = assert(http.websocket("wss://example.tweaked.cc/echo"))
|
||||||
|
ws.send("Hello!") -- Send a message
|
||||||
|
print(ws.receive()) -- And receive the reply
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
]]
|
||||||
|
function websocket(url, headers)
|
||||||
|
local actual_url
|
||||||
|
if type(url) == "table" then
|
||||||
|
check_websocket_options(url)
|
||||||
|
actual_url = url.url
|
||||||
|
else
|
||||||
|
expect(1, url, "string")
|
||||||
|
expect(2, headers, "table", "nil")
|
||||||
|
actual_url = url
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = nativeWebsocket(url, headers)
|
||||||
|
if not ok then return ok, err end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local event, url, param = os.pullEvent( )
|
||||||
|
if event == "websocket_success" and url == actual_url then
|
||||||
|
return param
|
||||||
|
elseif event == "websocket_failure" and url == actual_url then
|
||||||
|
return false, param
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
448
src/main/resources/assets/cctweaked/lua/rom/apis/io.lua
Normal file
448
src/main/resources/assets/cctweaked/lua/rom/apis/io.lua
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Emulates Lua's standard [io library][io].
|
||||||
|
--
|
||||||
|
-- [io]: https://www.lua.org/manual/5.1/manual.html#5.7
|
||||||
|
--
|
||||||
|
-- @module io
|
||||||
|
|
||||||
|
local expect, type_of = dofile("rom/modules/main/cc/expect.lua").expect, _G.type
|
||||||
|
|
||||||
|
--- If we return nil then close the file, as we've reached the end.
|
||||||
|
-- We use this weird wrapper function as we wish to preserve the varargs
|
||||||
|
local function checkResult(handle, ...)
|
||||||
|
if ... == nil and handle._autoclose and not handle._closed then handle:close() end
|
||||||
|
return ...
|
||||||
|
end
|
||||||
|
|
||||||
|
--- A file handle which can be read or written to.
|
||||||
|
--
|
||||||
|
-- @type Handle
|
||||||
|
local handleMetatable
|
||||||
|
handleMetatable = {
|
||||||
|
__name = "FILE*",
|
||||||
|
__tostring = function(self)
|
||||||
|
if self._closed then
|
||||||
|
return "file (closed)"
|
||||||
|
else
|
||||||
|
local hash = tostring(self._handle):match("table: (%x+)")
|
||||||
|
return "file (" .. hash .. ")"
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
__index = {
|
||||||
|
--- Close this file handle, freeing any resources it uses.
|
||||||
|
--
|
||||||
|
-- @treturn[1] true If this handle was successfully closed.
|
||||||
|
-- @treturn[2] nil If this file handle could not be closed.
|
||||||
|
-- @treturn[2] string The reason it could not be closed.
|
||||||
|
-- @throws If this handle was already closed.
|
||||||
|
close = function(self)
|
||||||
|
if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
|
||||||
|
end
|
||||||
|
if self._closed then error("attempt to use a closed file", 2) end
|
||||||
|
|
||||||
|
local handle = self._handle
|
||||||
|
if handle.close then
|
||||||
|
self._closed = true
|
||||||
|
handle.close()
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return nil, "attempt to close standard stream"
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Flush any buffered output, forcing it to be written to the file
|
||||||
|
--
|
||||||
|
-- @throws If the handle has been closed
|
||||||
|
flush = function(self)
|
||||||
|
if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
|
||||||
|
end
|
||||||
|
if self._closed then error("attempt to use a closed file", 2) end
|
||||||
|
|
||||||
|
local handle = self._handle
|
||||||
|
if handle.flush then handle.flush() end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
|
||||||
|
--[[- Returns an iterator that, each time it is called, returns a new
|
||||||
|
line from the file.
|
||||||
|
|
||||||
|
This can be used in a for loop to iterate over all lines of a file
|
||||||
|
|
||||||
|
Once the end of the file has been reached, @{nil} will be returned. The file is
|
||||||
|
*not* automatically closed.
|
||||||
|
|
||||||
|
@param ... The argument to pass to @{Handle:read} for each line.
|
||||||
|
@treturn function():string|nil The line iterator.
|
||||||
|
@throws If the file cannot be opened for reading
|
||||||
|
@since 1.3
|
||||||
|
|
||||||
|
@see io.lines
|
||||||
|
@usage Iterate over every line in a file and print it out.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local file = io.open("/rom/help/intro.txt")
|
||||||
|
for line in file:lines() do
|
||||||
|
print(line)
|
||||||
|
end
|
||||||
|
file:close()
|
||||||
|
```
|
||||||
|
]]
|
||||||
|
lines = function(self, ...)
|
||||||
|
if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
|
||||||
|
end
|
||||||
|
if self._closed then error("attempt to use a closed file", 2) end
|
||||||
|
|
||||||
|
local handle = self._handle
|
||||||
|
if not handle.read then return nil, "file is not readable" end
|
||||||
|
|
||||||
|
local args = table.pack(...)
|
||||||
|
return function()
|
||||||
|
if self._closed then error("file is already closed", 2) end
|
||||||
|
return checkResult(self, self:read(table.unpack(args, 1, args.n)))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--[[- Reads data from the file, using the specified formats. For each
|
||||||
|
format provided, the function returns either the data read, or `nil` if
|
||||||
|
no data could be read.
|
||||||
|
|
||||||
|
The following formats are available:
|
||||||
|
- `l`: Returns the next line (without a newline on the end).
|
||||||
|
- `L`: Returns the next line (with a newline on the end).
|
||||||
|
- `a`: Returns the entire rest of the file.
|
||||||
|
- ~~`n`: Returns a number~~ (not implemented in CC).
|
||||||
|
|
||||||
|
These formats can be preceded by a `*` to make it compatible with Lua 5.1.
|
||||||
|
|
||||||
|
If no format is provided, `l` is assumed.
|
||||||
|
|
||||||
|
@param ... The formats to use.
|
||||||
|
@treturn (string|nil)... The data read from the file.
|
||||||
|
]]
|
||||||
|
read = function(self, ...)
|
||||||
|
if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
|
||||||
|
end
|
||||||
|
if self._closed then error("attempt to use a closed file", 2) end
|
||||||
|
|
||||||
|
local handle = self._handle
|
||||||
|
if not handle.read and not handle.readLine then return nil, "Not opened for reading" end
|
||||||
|
|
||||||
|
local n = select("#", ...)
|
||||||
|
local output = {}
|
||||||
|
for i = 1, n do
|
||||||
|
local arg = select(i, ...)
|
||||||
|
local res
|
||||||
|
if type_of(arg) == "number" then
|
||||||
|
if handle.read then res = handle.read(arg) end
|
||||||
|
elseif type_of(arg) == "string" then
|
||||||
|
local format = arg:gsub("^%*", ""):sub(1, 1)
|
||||||
|
|
||||||
|
if format == "l" then
|
||||||
|
if handle.readLine then res = handle.readLine() end
|
||||||
|
elseif format == "L" and handle.readLine then
|
||||||
|
if handle.readLine then res = handle.readLine(true) end
|
||||||
|
elseif format == "a" then
|
||||||
|
if handle.readAll then res = handle.readAll() or "" end
|
||||||
|
elseif format == "n" then
|
||||||
|
res = nil -- Skip this format as we can't really handle it
|
||||||
|
else
|
||||||
|
error("bad argument #" .. i .. " (invalid format)", 2)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error("bad argument #" .. i .. " (string expected, got " .. type_of(arg) .. ")", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
output[i] = res
|
||||||
|
if not res then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default to "l" if possible
|
||||||
|
if n == 0 and handle.readLine then return handle.readLine() end
|
||||||
|
return table.unpack(output, 1, n)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--[[- Seeks the file cursor to the specified position, and returns the
|
||||||
|
new position.
|
||||||
|
|
||||||
|
`whence` controls where the seek operation starts, and is a string that
|
||||||
|
may be one of these three values:
|
||||||
|
- `set`: base position is 0 (beginning of the file)
|
||||||
|
- `cur`: base is current position
|
||||||
|
- `end`: base is end of file
|
||||||
|
|
||||||
|
The default value of `whence` is `cur`, and the default value of `offset`
|
||||||
|
is 0. This means that `file:seek()` without arguments returns the current
|
||||||
|
position without moving.
|
||||||
|
|
||||||
|
@tparam[opt] string whence The place to set the cursor from.
|
||||||
|
@tparam[opt] number offset The offset from the start to move to.
|
||||||
|
@treturn number The new location of the file cursor.
|
||||||
|
]]
|
||||||
|
seek = function(self, whence, offset)
|
||||||
|
if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
|
||||||
|
end
|
||||||
|
if self._closed then error("attempt to use a closed file", 2) end
|
||||||
|
|
||||||
|
local handle = self._handle
|
||||||
|
if not handle.seek then return nil, "file is not seekable" end
|
||||||
|
|
||||||
|
-- It's a tail call, so error positions are preserved
|
||||||
|
return handle.seek(whence, offset)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--[[- Sets the buffering mode for an output file.
|
||||||
|
|
||||||
|
This has no effect under ComputerCraft, and exists with compatility
|
||||||
|
with base Lua.
|
||||||
|
@tparam string mode The buffering mode.
|
||||||
|
@tparam[opt] number size The size of the buffer.
|
||||||
|
@see file:setvbuf Lua's documentation for `setvbuf`.
|
||||||
|
@deprecated This has no effect in CC.
|
||||||
|
]]
|
||||||
|
setvbuf = function(self, mode, size) end,
|
||||||
|
|
||||||
|
--- Write one or more values to the file
|
||||||
|
--
|
||||||
|
-- @tparam string|number ... The values to write.
|
||||||
|
-- @treturn[1] Handle The current file, allowing chained calls.
|
||||||
|
-- @treturn[2] nil If the file could not be written to.
|
||||||
|
-- @treturn[2] string The error message which occurred while writing.
|
||||||
|
-- @changed 1.81.0 Multiple arguments are now allowed.
|
||||||
|
write = function(self, ...)
|
||||||
|
if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
|
||||||
|
end
|
||||||
|
if self._closed then error("attempt to use a closed file", 2) end
|
||||||
|
|
||||||
|
local handle = self._handle
|
||||||
|
if not handle.write then return nil, "file is not writable" end
|
||||||
|
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local arg = select(i, ...)
|
||||||
|
expect(i, arg, "string", "number")
|
||||||
|
handle.write(arg)
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function make_file(handle)
|
||||||
|
return setmetatable({ _handle = handle }, handleMetatable)
|
||||||
|
end
|
||||||
|
|
||||||
|
local defaultInput = make_file({ readLine = _G.read })
|
||||||
|
|
||||||
|
local defaultOutput = make_file({ write = _G.write })
|
||||||
|
|
||||||
|
local defaultError = make_file({
|
||||||
|
write = function(...)
|
||||||
|
local oldColour
|
||||||
|
if term.isColour() then
|
||||||
|
oldColour = term.getTextColour()
|
||||||
|
term.setTextColour(colors.red)
|
||||||
|
end
|
||||||
|
_G.write(...)
|
||||||
|
if term.isColour() then term.setTextColour(oldColour) end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
local currentInput = defaultInput
|
||||||
|
local currentOutput = defaultOutput
|
||||||
|
|
||||||
|
--- A file handle representing the "standard input". Reading from this
|
||||||
|
-- file will prompt the user for input.
|
||||||
|
stdin = defaultInput
|
||||||
|
|
||||||
|
--- A file handle representing the "standard output". Writing to this
|
||||||
|
-- file will display the written text to the screen.
|
||||||
|
stdout = defaultOutput
|
||||||
|
|
||||||
|
--- A file handle representing the "standard error" stream.
|
||||||
|
--
|
||||||
|
-- One may use this to display error messages, writing to it will display
|
||||||
|
-- them on the terminal.
|
||||||
|
stderr = defaultError
|
||||||
|
|
||||||
|
--- Closes the provided file handle.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] Handle file The file handle to close, defaults to the
|
||||||
|
-- current output file.
|
||||||
|
--
|
||||||
|
-- @see Handle:close
|
||||||
|
-- @see io.output
|
||||||
|
-- @since 1.55
|
||||||
|
function close(file)
|
||||||
|
if file == nil then return currentOutput:close() end
|
||||||
|
|
||||||
|
if type_of(file) ~= "table" or getmetatable(file) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(file) .. ")", 2)
|
||||||
|
end
|
||||||
|
return file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Flushes the current output file.
|
||||||
|
--
|
||||||
|
-- @see Handle:flush
|
||||||
|
-- @see io.output
|
||||||
|
-- @since 1.55
|
||||||
|
function flush()
|
||||||
|
return currentOutput:flush()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get or set the current input file.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] Handle|string file The new input file, either as a file path or pre-existing handle.
|
||||||
|
-- @treturn Handle The current input file.
|
||||||
|
-- @throws If the provided filename cannot be opened for reading.
|
||||||
|
-- @since 1.55
|
||||||
|
function input(file)
|
||||||
|
if type_of(file) == "string" then
|
||||||
|
local res, err = open(file, "rb")
|
||||||
|
if not res then error(err, 2) end
|
||||||
|
currentInput = res
|
||||||
|
elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then
|
||||||
|
currentInput = file
|
||||||
|
elseif file ~= nil then
|
||||||
|
error("bad fileument #1 (FILE expected, got " .. type_of(file) .. ")", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return currentInput
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Opens the given file name in read mode and returns an iterator that,
|
||||||
|
each time it is called, returns a new line from the file.
|
||||||
|
|
||||||
|
This can be used in a for loop to iterate over all lines of a file
|
||||||
|
|
||||||
|
Once the end of the file has been reached, @{nil} will be returned. The file is
|
||||||
|
automatically closed.
|
||||||
|
|
||||||
|
If no file name is given, the @{io.input|current input} will be used instead.
|
||||||
|
In this case, the handle is not used.
|
||||||
|
|
||||||
|
@tparam[opt] string filename The name of the file to extract lines from
|
||||||
|
@param ... The argument to pass to @{Handle:read} for each line.
|
||||||
|
@treturn function():string|nil The line iterator.
|
||||||
|
@throws If the file cannot be opened for reading
|
||||||
|
|
||||||
|
@see Handle:lines
|
||||||
|
@see io.input
|
||||||
|
@since 1.55
|
||||||
|
@usage Iterate over every line in a file and print it out.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for line in io.lines("/rom/help/intro.txt") do
|
||||||
|
print(line)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
]]
|
||||||
|
function lines(filename, ...)
|
||||||
|
expect(1, filename, "string", "nil")
|
||||||
|
if filename then
|
||||||
|
local ok, err = open(filename, "rb")
|
||||||
|
if not ok then error(err, 2) end
|
||||||
|
|
||||||
|
-- We set this magic flag to mark this file as being opened by io.lines and so should be
|
||||||
|
-- closed automatically
|
||||||
|
ok._autoclose = true
|
||||||
|
return ok:lines(...)
|
||||||
|
else
|
||||||
|
return currentInput:lines(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Open a file with the given mode, either returning a new file handle
|
||||||
|
-- or @{nil}, plus an error message.
|
||||||
|
--
|
||||||
|
-- The `mode` string can be any of the following:
|
||||||
|
-- - **"r"**: Read mode
|
||||||
|
-- - **"w"**: Write mode
|
||||||
|
-- - **"a"**: Append mode
|
||||||
|
--
|
||||||
|
-- The mode may also have a `b` at the end, which opens the file in "binary
|
||||||
|
-- mode". This allows you to read binary files, as well as seek within a file.
|
||||||
|
--
|
||||||
|
-- @tparam string filename The name of the file to open.
|
||||||
|
-- @tparam[opt] string mode The mode to open the file with. This defaults to `rb`.
|
||||||
|
-- @treturn[1] Handle The opened file.
|
||||||
|
-- @treturn[2] nil In case of an error.
|
||||||
|
-- @treturn[2] string The reason the file could not be opened.
|
||||||
|
function open(filename, mode)
|
||||||
|
expect(1, filename, "string")
|
||||||
|
expect(2, mode, "string", "nil")
|
||||||
|
|
||||||
|
local sMode = mode and mode:gsub("%+", "") or "rb"
|
||||||
|
local file, err = fs.open(filename, sMode)
|
||||||
|
if not file then return nil, err end
|
||||||
|
|
||||||
|
return make_file(file)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get or set the current output file.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] Handle|string file The new output file, either as a file path or pre-existing handle.
|
||||||
|
-- @treturn Handle The current output file.
|
||||||
|
-- @throws If the provided filename cannot be opened for writing.
|
||||||
|
-- @since 1.55
|
||||||
|
function output(file)
|
||||||
|
if type_of(file) == "string" then
|
||||||
|
local res, err = open(file, "wb")
|
||||||
|
if not res then error(err, 2) end
|
||||||
|
currentOutput = res
|
||||||
|
elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then
|
||||||
|
currentOutput = file
|
||||||
|
elseif file ~= nil then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. type_of(file) .. ")", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return currentOutput
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Read from the currently opened input file.
|
||||||
|
--
|
||||||
|
-- This is equivalent to `io.input():read(...)`. See @{Handle:read|the
|
||||||
|
-- documentation} there for full details.
|
||||||
|
--
|
||||||
|
-- @tparam string ... The formats to read, defaulting to a whole line.
|
||||||
|
-- @treturn (string|nil)... The data read, or @{nil} if nothing can be read.
|
||||||
|
function read(...)
|
||||||
|
return currentInput:read(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Checks whether `handle` is a given file handle, and determine if it is open
|
||||||
|
-- or not.
|
||||||
|
--
|
||||||
|
-- @param obj The value to check
|
||||||
|
-- @treturn string|nil `"file"` if this is an open file, `"closed file"` if it
|
||||||
|
-- is a closed file handle, or `nil` if not a file handle.
|
||||||
|
function type(obj)
|
||||||
|
if type_of(obj) == "table" and getmetatable(obj) == handleMetatable then
|
||||||
|
if obj._closed then
|
||||||
|
return "closed file"
|
||||||
|
else
|
||||||
|
return "file"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write to the currently opened output file.
|
||||||
|
--
|
||||||
|
-- This is equivalent to `io.output():write(...)`. See @{Handle:write|the
|
||||||
|
-- documentation} there for full details.
|
||||||
|
--
|
||||||
|
-- @tparam string ... The strings to write
|
||||||
|
-- @changed 1.81.0 Multiple arguments are now allowed.
|
||||||
|
function write(...)
|
||||||
|
return currentOutput:write(...)
|
||||||
|
end
|
80
src/main/resources/assets/cctweaked/lua/rom/apis/keys.lua
Normal file
80
src/main/resources/assets/cctweaked/lua/rom/apis/keys.lua
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Constants for all keyboard "key codes", as queued by the @{key} event.
|
||||||
|
--
|
||||||
|
-- These values are not guaranteed to remain the same between versions. It is
|
||||||
|
-- recommended that you use the constants provided by this file, rather than
|
||||||
|
-- the underlying numerical values.
|
||||||
|
--
|
||||||
|
-- @module keys
|
||||||
|
-- @since 1.4
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local tKeys = {
|
||||||
|
nil, "one", "two", "three", "four", -- 1
|
||||||
|
"five", "six", "seven", "eight", "nine", -- 6
|
||||||
|
"zero", "minus", "equals", "backspace", "tab", -- 11
|
||||||
|
"q", "w", "e", "r", "t", -- 16
|
||||||
|
"y", "u", "i", "o", "p", -- 21
|
||||||
|
"leftBracket", "rightBracket", "enter", "leftCtrl", "a", -- 26
|
||||||
|
"s", "d", "f", "g", "h", -- 31
|
||||||
|
"j", "k", "l", "semiColon", "apostrophe", -- 36
|
||||||
|
"grave", "leftShift", "backslash", "z", "x", -- 41
|
||||||
|
"c", "v", "b", "n", "m", -- 46
|
||||||
|
"comma", "period", "slash", "rightShift", "multiply", -- 51
|
||||||
|
"leftAlt", "space", "capsLock", "f1", "f2", -- 56
|
||||||
|
"f3", "f4", "f5", "f6", "f7", -- 61
|
||||||
|
"f8", "f9", "f10", "numLock", "scrollLock", -- 66
|
||||||
|
"numPad7", "numPad8", "numPad9", "numPadSubtract", "numPad4", -- 71
|
||||||
|
"numPad5", "numPad6", "numPadAdd", "numPad1", "numPad2", -- 76
|
||||||
|
"numPad3", "numPad0", "numPadDecimal", nil, nil, -- 81
|
||||||
|
nil, "f11", "f12", nil, nil, -- 86
|
||||||
|
nil, nil, nil, nil, nil, -- 91
|
||||||
|
nil, nil, nil, nil, "f13", -- 96
|
||||||
|
"f14", "f15", nil, nil, nil, -- 101
|
||||||
|
nil, nil, nil, nil, nil, -- 106
|
||||||
|
nil, "kana", nil, nil, nil, -- 111
|
||||||
|
nil, nil, nil, nil, nil, -- 116
|
||||||
|
"convert", nil, "noconvert", nil, "yen", -- 121
|
||||||
|
nil, nil, nil, nil, nil, -- 126
|
||||||
|
nil, nil, nil, nil, nil, -- 131
|
||||||
|
nil, nil, nil, nil, nil, -- 136
|
||||||
|
"numPadEquals", nil, nil, "circumflex", "at", -- 141
|
||||||
|
"colon", "underscore", "kanji", "stop", "ax", -- 146
|
||||||
|
nil, nil, nil, nil, nil, -- 151
|
||||||
|
"numPadEnter", "rightCtrl", nil, nil, nil, -- 156
|
||||||
|
nil, nil, nil, nil, nil, -- 161
|
||||||
|
nil, nil, nil, nil, nil, -- 166
|
||||||
|
nil, nil, nil, nil, nil, -- 171
|
||||||
|
nil, nil, nil, "numPadComma", nil, -- 176
|
||||||
|
"numPadDivide", nil, nil, "rightAlt", nil, -- 181
|
||||||
|
nil, nil, nil, nil, nil, -- 186
|
||||||
|
nil, nil, nil, nil, nil, -- 191
|
||||||
|
nil, "pause", nil, "home", "up", -- 196
|
||||||
|
"pageUp", nil, "left", nil, "right", -- 201
|
||||||
|
nil, "end", "down", "pageDown", "insert", -- 206
|
||||||
|
"delete", -- 211
|
||||||
|
}
|
||||||
|
|
||||||
|
local keys = _ENV
|
||||||
|
for nKey, sKey in pairs(tKeys) do
|
||||||
|
keys[sKey] = nKey
|
||||||
|
end
|
||||||
|
|
||||||
|
keys["return"] = keys.enter --- @local
|
||||||
|
--backwards compatibility to earlier, typo prone, versions
|
||||||
|
keys.scollLock = keys.scrollLock --- @local
|
||||||
|
keys.cimcumflex = keys.circumflex --- @local
|
||||||
|
|
||||||
|
--- Translates a numerical key code to a human-readable name. The human-readable
|
||||||
|
-- name is one of the constants in the keys API.
|
||||||
|
--
|
||||||
|
-- @tparam number code The key code to look up.
|
||||||
|
-- @treturn string|nil The name of the key, or `nil` if not a valid key code.
|
||||||
|
function getName(code)
|
||||||
|
expect(1, code, "number")
|
||||||
|
return tKeys[code]
|
||||||
|
end
|
299
src/main/resources/assets/cctweaked/lua/rom/apis/paintutils.lua
Normal file
299
src/main/resources/assets/cctweaked/lua/rom/apis/paintutils.lua
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Utilities for drawing more complex graphics, such as pixels, lines and
|
||||||
|
-- images.
|
||||||
|
--
|
||||||
|
-- @module paintutils
|
||||||
|
-- @since 1.45
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local function drawPixelInternal(xPos, yPos)
|
||||||
|
term.setCursorPos(xPos, yPos)
|
||||||
|
term.write(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local tColourLookup = {}
|
||||||
|
for n = 1, 16 do
|
||||||
|
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parseLine(tImageArg, sLine)
|
||||||
|
local tLine = {}
|
||||||
|
for x = 1, sLine:len() do
|
||||||
|
tLine[x] = tColourLookup[string.byte(sLine, x, x)] or 0
|
||||||
|
end
|
||||||
|
table.insert(tImageArg, tLine)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sorts pairs of startX/startY/endX/endY such that the start is always the min
|
||||||
|
local function sortCoords(startX, startY, endX, endY)
|
||||||
|
local minX, maxX, minY, maxY
|
||||||
|
|
||||||
|
if startX <= endX then
|
||||||
|
minX, maxX = startX, endX
|
||||||
|
else
|
||||||
|
minX, maxX = endX, startX
|
||||||
|
end
|
||||||
|
|
||||||
|
if startY <= endY then
|
||||||
|
minY, maxY = startY, endY
|
||||||
|
else
|
||||||
|
minY, maxY = endY, startY
|
||||||
|
end
|
||||||
|
|
||||||
|
return minX, maxX, minY, maxY
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Parses an image from a multi-line string
|
||||||
|
--
|
||||||
|
-- @tparam string image The string containing the raw-image data.
|
||||||
|
-- @treturn table The parsed image data, suitable for use with
|
||||||
|
-- @{paintutils.drawImage}.
|
||||||
|
-- @since 1.80pr1
|
||||||
|
function parseImage(image)
|
||||||
|
expect(1, image, "string")
|
||||||
|
local tImage = {}
|
||||||
|
for sLine in (image .. "\n"):gmatch("(.-)\n") do
|
||||||
|
parseLine(tImage, sLine)
|
||||||
|
end
|
||||||
|
return tImage
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Loads an image from a file.
|
||||||
|
--
|
||||||
|
-- You can create a file suitable for being loaded using the `paint` program.
|
||||||
|
--
|
||||||
|
-- @tparam string path The file to load.
|
||||||
|
--
|
||||||
|
-- @treturn table|nil The parsed image data, suitable for use with
|
||||||
|
-- @{paintutils.drawImage}, or `nil` if the file does not exist.
|
||||||
|
-- @usage Load an image and draw it.
|
||||||
|
--
|
||||||
|
-- local image = paintutils.loadImage("data/example.nfp")
|
||||||
|
-- paintutils.drawImage(image, term.getCursorPos())
|
||||||
|
function loadImage(path)
|
||||||
|
expect(1, path, "string")
|
||||||
|
|
||||||
|
if fs.exists(path) then
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
local sContent = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
return parseImage(sContent)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws a single pixel to the current term at the specified position.
|
||||||
|
--
|
||||||
|
-- Be warned, this may change the position of the cursor and the current
|
||||||
|
-- background colour. You should not expect either to be preserved.
|
||||||
|
--
|
||||||
|
-- @tparam number xPos The x position to draw at, where 1 is the far left.
|
||||||
|
-- @tparam number yPos The y position to draw at, where 1 is the very top.
|
||||||
|
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
|
||||||
|
-- the current background colour if not specified.
|
||||||
|
function drawPixel(xPos, yPos, colour)
|
||||||
|
expect(1, xPos, "number")
|
||||||
|
expect(2, yPos, "number")
|
||||||
|
expect(3, colour, "number", "nil")
|
||||||
|
|
||||||
|
if colour then
|
||||||
|
term.setBackgroundColor(colour)
|
||||||
|
end
|
||||||
|
return drawPixelInternal(xPos, yPos)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws a straight line from the start to end position.
|
||||||
|
--
|
||||||
|
-- Be warned, this may change the position of the cursor and the current
|
||||||
|
-- background colour. You should not expect either to be preserved.
|
||||||
|
--
|
||||||
|
-- @tparam number startX The starting x position of the line.
|
||||||
|
-- @tparam number startY The starting y position of the line.
|
||||||
|
-- @tparam number endX The end x position of the line.
|
||||||
|
-- @tparam number endY The end y position of the line.
|
||||||
|
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
|
||||||
|
-- the current background colour if not specified.
|
||||||
|
-- @usage paintutils.drawLine(2, 3, 30, 7, colors.red)
|
||||||
|
function drawLine(startX, startY, endX, endY, colour)
|
||||||
|
expect(1, startX, "number")
|
||||||
|
expect(2, startY, "number")
|
||||||
|
expect(3, endX, "number")
|
||||||
|
expect(4, endY, "number")
|
||||||
|
expect(5, colour, "number", "nil")
|
||||||
|
|
||||||
|
startX = math.floor(startX)
|
||||||
|
startY = math.floor(startY)
|
||||||
|
endX = math.floor(endX)
|
||||||
|
endY = math.floor(endY)
|
||||||
|
|
||||||
|
if colour then
|
||||||
|
term.setBackgroundColor(colour)
|
||||||
|
end
|
||||||
|
if startX == endX and startY == endY then
|
||||||
|
drawPixelInternal(startX, startY)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local minX = math.min(startX, endX)
|
||||||
|
local maxX, minY, maxY
|
||||||
|
if minX == startX then
|
||||||
|
minY = startY
|
||||||
|
maxX = endX
|
||||||
|
maxY = endY
|
||||||
|
else
|
||||||
|
minY = endY
|
||||||
|
maxX = startX
|
||||||
|
maxY = startY
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: clip to screen rectangle?
|
||||||
|
|
||||||
|
local xDiff = maxX - minX
|
||||||
|
local yDiff = maxY - minY
|
||||||
|
|
||||||
|
if xDiff > math.abs(yDiff) then
|
||||||
|
local y = minY
|
||||||
|
local dy = yDiff / xDiff
|
||||||
|
for x = minX, maxX do
|
||||||
|
drawPixelInternal(x, math.floor(y + 0.5))
|
||||||
|
y = y + dy
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local x = minX
|
||||||
|
local dx = xDiff / yDiff
|
||||||
|
if maxY >= minY then
|
||||||
|
for y = minY, maxY do
|
||||||
|
drawPixelInternal(math.floor(x + 0.5), y)
|
||||||
|
x = x + dx
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for y = minY, maxY, -1 do
|
||||||
|
drawPixelInternal(math.floor(x + 0.5), y)
|
||||||
|
x = x - dx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws the outline of a box on the current term from the specified start
|
||||||
|
-- position to the specified end position.
|
||||||
|
--
|
||||||
|
-- Be warned, this may change the position of the cursor and the current
|
||||||
|
-- background colour. You should not expect either to be preserved.
|
||||||
|
--
|
||||||
|
-- @tparam number startX The starting x position of the line.
|
||||||
|
-- @tparam number startY The starting y position of the line.
|
||||||
|
-- @tparam number endX The end x position of the line.
|
||||||
|
-- @tparam number endY The end y position of the line.
|
||||||
|
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
|
||||||
|
-- the current background colour if not specified.
|
||||||
|
-- @usage paintutils.drawBox(2, 3, 30, 7, colors.red)
|
||||||
|
function drawBox(startX, startY, endX, endY, nColour)
|
||||||
|
expect(1, startX, "number")
|
||||||
|
expect(2, startY, "number")
|
||||||
|
expect(3, endX, "number")
|
||||||
|
expect(4, endY, "number")
|
||||||
|
expect(5, nColour, "number", "nil")
|
||||||
|
|
||||||
|
startX = math.floor(startX)
|
||||||
|
startY = math.floor(startY)
|
||||||
|
endX = math.floor(endX)
|
||||||
|
endY = math.floor(endY)
|
||||||
|
|
||||||
|
if nColour then
|
||||||
|
term.setBackgroundColor(nColour) -- Maintain legacy behaviour
|
||||||
|
else
|
||||||
|
nColour = term.getBackgroundColour()
|
||||||
|
end
|
||||||
|
local colourHex = colours.toBlit(nColour)
|
||||||
|
|
||||||
|
if startX == endX and startY == endY then
|
||||||
|
drawPixelInternal(startX, startY)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY)
|
||||||
|
local width = maxX - minX + 1
|
||||||
|
|
||||||
|
for y = minY, maxY do
|
||||||
|
if y == minY or y == maxY then
|
||||||
|
term.setCursorPos(minX, y)
|
||||||
|
term.blit((" "):rep(width), colourHex:rep(width), colourHex:rep(width))
|
||||||
|
else
|
||||||
|
term.setCursorPos(minX, y)
|
||||||
|
term.blit(" ", colourHex, colourHex)
|
||||||
|
term.setCursorPos(maxX, y)
|
||||||
|
term.blit(" ", colourHex, colourHex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws a filled box on the current term from the specified start position to
|
||||||
|
-- the specified end position.
|
||||||
|
--
|
||||||
|
-- Be warned, this may change the position of the cursor and the current
|
||||||
|
-- background colour. You should not expect either to be preserved.
|
||||||
|
--
|
||||||
|
-- @tparam number startX The starting x position of the line.
|
||||||
|
-- @tparam number startY The starting y position of the line.
|
||||||
|
-- @tparam number endX The end x position of the line.
|
||||||
|
-- @tparam number endY The end y position of the line.
|
||||||
|
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
|
||||||
|
-- the current background colour if not specified.
|
||||||
|
-- @usage paintutils.drawFilledBox(2, 3, 30, 7, colors.red)
|
||||||
|
function drawFilledBox(startX, startY, endX, endY, nColour)
|
||||||
|
expect(1, startX, "number")
|
||||||
|
expect(2, startY, "number")
|
||||||
|
expect(3, endX, "number")
|
||||||
|
expect(4, endY, "number")
|
||||||
|
expect(5, nColour, "number", "nil")
|
||||||
|
|
||||||
|
startX = math.floor(startX)
|
||||||
|
startY = math.floor(startY)
|
||||||
|
endX = math.floor(endX)
|
||||||
|
endY = math.floor(endY)
|
||||||
|
|
||||||
|
if nColour then
|
||||||
|
term.setBackgroundColor(nColour) -- Maintain legacy behaviour
|
||||||
|
else
|
||||||
|
nColour = term.getBackgroundColour()
|
||||||
|
end
|
||||||
|
local colourHex = colours.toBlit(nColour)
|
||||||
|
|
||||||
|
if startX == endX and startY == endY then
|
||||||
|
drawPixelInternal(startX, startY)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY)
|
||||||
|
local width = maxX - minX + 1
|
||||||
|
|
||||||
|
for y = minY, maxY do
|
||||||
|
term.setCursorPos(minX, y)
|
||||||
|
term.blit((" "):rep(width), colourHex:rep(width), colourHex:rep(width))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draw an image loaded by @{paintutils.parseImage} or @{paintutils.loadImage}.
|
||||||
|
--
|
||||||
|
-- @tparam table image The parsed image data.
|
||||||
|
-- @tparam number xPos The x position to start drawing at.
|
||||||
|
-- @tparam number yPos The y position to start drawing at.
|
||||||
|
function drawImage(image, xPos, yPos)
|
||||||
|
expect(1, image, "table")
|
||||||
|
expect(2, xPos, "number")
|
||||||
|
expect(3, yPos, "number")
|
||||||
|
for y = 1, #image do
|
||||||
|
local tLine = image[y]
|
||||||
|
for x = 1, #tLine do
|
||||||
|
if tLine[x] > 0 then
|
||||||
|
term.setBackgroundColor(tLine[x])
|
||||||
|
drawPixelInternal(x + xPos - 1, y + yPos - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
151
src/main/resources/assets/cctweaked/lua/rom/apis/parallel.lua
Normal file
151
src/main/resources/assets/cctweaked/lua/rom/apis/parallel.lua
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- A simple way to run several functions at once.
|
||||||
|
|
||||||
|
Functions are not actually executed simultaneously, but rather this API will
|
||||||
|
automatically switch between them whenever they yield (e.g. whenever they call
|
||||||
|
@{coroutine.yield}, or functions that call that - such as @{os.pullEvent} - or
|
||||||
|
functions that call that, etc - basically, anything that causes the function
|
||||||
|
to "pause").
|
||||||
|
|
||||||
|
Each function executed in "parallel" gets its own copy of the event queue,
|
||||||
|
and so "event consuming" functions (again, mostly anything that causes the
|
||||||
|
script to pause - eg @{os.sleep}, @{rednet.receive}, most of the @{turtle} API,
|
||||||
|
etc) can safely be used in one without affecting the event queue accessed by
|
||||||
|
the other.
|
||||||
|
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
When using this API, be careful to pass the functions you want to run in
|
||||||
|
parallel, and _not_ the result of calling those functions.
|
||||||
|
|
||||||
|
For instance, the following is correct:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function do_sleep() sleep(1) end
|
||||||
|
parallel.waitForAny(do_sleep, rednet.receive)
|
||||||
|
```
|
||||||
|
|
||||||
|
but the following is **NOT**:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function do_sleep() sleep(1) end
|
||||||
|
parallel.waitForAny(do_sleep(), rednet.receive)
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
@module parallel
|
||||||
|
@since 1.2
|
||||||
|
]]
|
||||||
|
|
||||||
|
local function create(...)
|
||||||
|
local tFns = table.pack(...)
|
||||||
|
local tCos = {}
|
||||||
|
for i = 1, tFns.n, 1 do
|
||||||
|
local fn = tFns[i]
|
||||||
|
if type(fn) ~= "function" then
|
||||||
|
error("bad argument #" .. i .. " (function expected, got " .. type(fn) .. ")", 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
tCos[i] = coroutine.create(fn)
|
||||||
|
end
|
||||||
|
|
||||||
|
return tCos
|
||||||
|
end
|
||||||
|
|
||||||
|
local function runUntilLimit(_routines, _limit)
|
||||||
|
local count = #_routines
|
||||||
|
if count < 1 then return 0 end
|
||||||
|
local living = count
|
||||||
|
|
||||||
|
local tFilters = {}
|
||||||
|
local eventData = { n = 0 }
|
||||||
|
while true do
|
||||||
|
for n = 1, count do
|
||||||
|
local r = _routines[n]
|
||||||
|
if r then
|
||||||
|
if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then
|
||||||
|
local ok, param = coroutine.resume(r, table.unpack(eventData, 1, eventData.n))
|
||||||
|
if not ok then
|
||||||
|
error(param, 0)
|
||||||
|
else
|
||||||
|
tFilters[r] = param
|
||||||
|
end
|
||||||
|
if coroutine.status(r) == "dead" then
|
||||||
|
_routines[n] = nil
|
||||||
|
living = living - 1
|
||||||
|
if living <= _limit then
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for n = 1, count do
|
||||||
|
local r = _routines[n]
|
||||||
|
if r and coroutine.status(r) == "dead" then
|
||||||
|
_routines[n] = nil
|
||||||
|
living = living - 1
|
||||||
|
if living <= _limit then
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
eventData = table.pack(os.pullEventRaw())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Switches between execution of the functions, until any of them
|
||||||
|
finishes. If any of the functions errors, the message is propagated upwards
|
||||||
|
from the @{parallel.waitForAny} call.
|
||||||
|
|
||||||
|
@tparam function ... The functions this task will run
|
||||||
|
@usage Print a message every second until the `q` key is pressed.
|
||||||
|
|
||||||
|
local function tick()
|
||||||
|
while true do
|
||||||
|
os.sleep(1)
|
||||||
|
print("Tick")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function wait_for_q()
|
||||||
|
repeat
|
||||||
|
local _, key = os.pullEvent("key")
|
||||||
|
until key == keys.q
|
||||||
|
print("Q was pressed!")
|
||||||
|
end
|
||||||
|
|
||||||
|
parallel.waitForAny(tick, wait_for_q)
|
||||||
|
print("Everything done!")
|
||||||
|
]]
|
||||||
|
function waitForAny(...)
|
||||||
|
local routines = create(...)
|
||||||
|
return runUntilLimit(routines, #routines - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Switches between execution of the functions, until all of them are
|
||||||
|
finished. If any of the functions errors, the message is propagated upwards
|
||||||
|
from the @{parallel.waitForAll} call.
|
||||||
|
|
||||||
|
@tparam function ... The functions this task will run
|
||||||
|
@usage Start off two timers and wait for them both to run.
|
||||||
|
|
||||||
|
local function a()
|
||||||
|
os.sleep(1)
|
||||||
|
print("A is done")
|
||||||
|
end
|
||||||
|
local function b()
|
||||||
|
os.sleep(3)
|
||||||
|
print("B is done")
|
||||||
|
end
|
||||||
|
|
||||||
|
parallel.waitForAll(a, b)
|
||||||
|
print("Everything done!")
|
||||||
|
]]
|
||||||
|
function waitForAll(...)
|
||||||
|
local routines = create(...)
|
||||||
|
return runUntilLimit(routines, 0)
|
||||||
|
end
|
351
src/main/resources/assets/cctweaked/lua/rom/apis/peripheral.lua
Normal file
351
src/main/resources/assets/cctweaked/lua/rom/apis/peripheral.lua
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Find and control peripherals attached to this computer.
|
||||||
|
|
||||||
|
Peripherals are blocks (or turtle and pocket computer upgrades) which can
|
||||||
|
be controlled by a computer. For instance, the @{speaker} peripheral allows a
|
||||||
|
computer to play music and the @{monitor} peripheral allows you to display text
|
||||||
|
in the world.
|
||||||
|
|
||||||
|
## Referencing peripherals
|
||||||
|
|
||||||
|
Computers can interact with adjacent peripherals. Each peripheral is given a
|
||||||
|
name based on which direction it is in. For instance, a disk drive below your
|
||||||
|
computer will be called `"bottom"` in your Lua code, one to the left called
|
||||||
|
`"left"` , and so on for all 6 directions (`"bottom"`, `"top"`, `"left"`,
|
||||||
|
`"right"`, `"front"`, `"back"`).
|
||||||
|
|
||||||
|
You can list the names of all peripherals with the `peripherals` program, or the
|
||||||
|
@{peripheral.getNames} function.
|
||||||
|
|
||||||
|
It's also possible to use peripherals which are further away from your computer
|
||||||
|
through the use of @{modem|Wired Modems}. Place one modem against your computer
|
||||||
|
(you may need to sneak and right click), run Networking Cable to your
|
||||||
|
peripheral, and then place another modem against that block. You can then right
|
||||||
|
click the modem to use (or *attach*) the peripheral. This will print a
|
||||||
|
peripheral name to chat, which can then be used just like a direction name to
|
||||||
|
access the peripheral. You can click on the message to copy the name to your
|
||||||
|
clipboard.
|
||||||
|
|
||||||
|
## Using peripherals
|
||||||
|
|
||||||
|
Once you have the name of a peripheral, you can call functions on it using the
|
||||||
|
@{peripheral.call} function. This takes the name of our peripheral, the name of
|
||||||
|
the function we want to call, and then its arguments.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Some bits of the peripheral API call peripheral functions *methods* instead
|
||||||
|
(for example, the @{peripheral.getMethods} function). Don't worry, they're the
|
||||||
|
same thing!
|
||||||
|
:::
|
||||||
|
|
||||||
|
Let's say we have a monitor above our computer (and so "top") and want to
|
||||||
|
@{monitor.write|write some text to it}. We'd write the following:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
peripheral.call("top", "write", "This is displayed on a monitor!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you start calling making a couple of peripheral calls this can get very
|
||||||
|
repetitive, and so we can @{peripheral.wrap|wrap} a peripheral. This builds a
|
||||||
|
table of all the peripheral's functions so you can use it like an API or module.
|
||||||
|
|
||||||
|
For instance, we could have written the above example as follows:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local my_monitor = peripheral.wrap("top")
|
||||||
|
my_monitor.write("This is displayed on a monitor!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finding peripherals
|
||||||
|
|
||||||
|
Sometimes when you're writing a program you don't care what a peripheral is
|
||||||
|
called, you just need to know it's there. For instance, if you're writing a
|
||||||
|
music player, you just need a speaker - it doesn't matter if it's above or below
|
||||||
|
the computer.
|
||||||
|
|
||||||
|
Thankfully there's a quick way to do this: @{peripheral.find}. This takes a
|
||||||
|
*peripheral type* and returns all the attached peripherals which are of this
|
||||||
|
type.
|
||||||
|
|
||||||
|
What is a peripheral type though? This is a string which describes what a
|
||||||
|
peripheral is, and so what functions are available on it. For instance, speakers
|
||||||
|
are just called `"speaker"`, and monitors `"monitor"`. Some peripherals might
|
||||||
|
have more than one type - a Minecraft chest is both a `"minecraft:chest"` and
|
||||||
|
`"inventory"`.
|
||||||
|
|
||||||
|
You can get all the types a peripheral has with @{peripheral.getType}, and check
|
||||||
|
a peripheral is a specific type with @{peripheral.hasType}.
|
||||||
|
|
||||||
|
To return to our original example, let's use @{peripheral.find} to find an
|
||||||
|
attached speaker:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local speaker = peripheral.find("speaker")
|
||||||
|
speaker.playNote("harp")
|
||||||
|
```
|
||||||
|
|
||||||
|
@module peripheral
|
||||||
|
@see event!peripheral This event is fired whenever a new peripheral is attached.
|
||||||
|
@see event!peripheral_detach This event is fired whenever a peripheral is detached.
|
||||||
|
@since 1.3
|
||||||
|
@changed 1.51 Add support for wired modems.
|
||||||
|
@changed 1.99 Peripherals can have multiple types.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local native = peripheral
|
||||||
|
|
||||||
|
-- Stub in peripheral.hasType
|
||||||
|
function native.hasType(p, type) return peripheral.getType(p) == type end
|
||||||
|
|
||||||
|
local sides = rs.getSides()
|
||||||
|
|
||||||
|
--- Provides a list of all peripherals available.
|
||||||
|
--
|
||||||
|
-- If a device is located directly next to the system, then its name will be
|
||||||
|
-- listed as the side it is attached to. If a device is attached via a Wired
|
||||||
|
-- Modem, then it'll be reported according to its name on the wired network.
|
||||||
|
--
|
||||||
|
-- @treturn { string... } A list of the names of all attached peripherals.
|
||||||
|
-- @since 1.51
|
||||||
|
function getNames()
|
||||||
|
local results = {}
|
||||||
|
for n = 1, #sides do
|
||||||
|
local side = sides[n]
|
||||||
|
if native.isPresent(side) then
|
||||||
|
table.insert(results, side)
|
||||||
|
if native.hasType(side, "peripheral_hub") then
|
||||||
|
local remote = native.call(side, "getNamesRemote")
|
||||||
|
for _, name in ipairs(remote) do
|
||||||
|
table.insert(results, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Determines if a peripheral is present with the given name.
|
||||||
|
--
|
||||||
|
-- @tparam string name The side or network name that you want to check.
|
||||||
|
-- @treturn boolean If a peripheral is present with the given name.
|
||||||
|
-- @usage peripheral.isPresent("top")
|
||||||
|
-- @usage peripheral.isPresent("monitor_0")
|
||||||
|
function isPresent(name)
|
||||||
|
expect(1, name, "string")
|
||||||
|
if native.isPresent(name) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
for n = 1, #sides do
|
||||||
|
local side = sides[n]
|
||||||
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Get the types of a named or wrapped peripheral.
|
||||||
|
|
||||||
|
@tparam string|table peripheral The name of the peripheral to find, or a
|
||||||
|
wrapped peripheral instance.
|
||||||
|
@treturn string... The peripheral's types, or `nil` if it is not present.
|
||||||
|
@changed 1.88.0 Accepts a wrapped peripheral as an argument.
|
||||||
|
@changed 1.99 Now returns multiple types.
|
||||||
|
@usage Get the type of a peripheral above this computer.
|
||||||
|
|
||||||
|
peripheral.getType("top")
|
||||||
|
]]
|
||||||
|
function getType(peripheral)
|
||||||
|
expect(1, peripheral, "string", "table")
|
||||||
|
if type(peripheral) == "string" then -- Peripheral name passed
|
||||||
|
if native.isPresent(peripheral) then
|
||||||
|
return native.getType(peripheral)
|
||||||
|
end
|
||||||
|
for n = 1, #sides do
|
||||||
|
local side = sides[n]
|
||||||
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
|
||||||
|
return native.call(side, "getTypeRemote", peripheral)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
local mt = getmetatable(peripheral)
|
||||||
|
if not mt or mt.__name ~= "peripheral" or type(mt.types) ~= "table" then
|
||||||
|
error("bad argument #1 (table is not a peripheral)", 2)
|
||||||
|
end
|
||||||
|
return table.unpack(mt.types)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Check if a peripheral is of a particular type.
|
||||||
|
|
||||||
|
@tparam string|table peripheral The name of the peripheral or a wrapped peripheral instance.
|
||||||
|
@tparam string peripheral_type The type to check.
|
||||||
|
|
||||||
|
@treturn boolean|nil If a peripheral has a particular type, or `nil` if it is not present.
|
||||||
|
@since 1.99
|
||||||
|
]]
|
||||||
|
function hasType(peripheral, peripheral_type)
|
||||||
|
expect(1, peripheral, "string", "table")
|
||||||
|
expect(2, peripheral_type, "string")
|
||||||
|
if type(peripheral) == "string" then -- Peripheral name passed
|
||||||
|
if native.isPresent(peripheral) then
|
||||||
|
return native.hasType(peripheral, peripheral_type)
|
||||||
|
end
|
||||||
|
for n = 1, #sides do
|
||||||
|
local side = sides[n]
|
||||||
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then
|
||||||
|
return native.call(side, "hasTypeRemote", peripheral, peripheral_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
local mt = getmetatable(peripheral)
|
||||||
|
if not mt or mt.__name ~= "peripheral" or type(mt.types) ~= "table" then
|
||||||
|
error("bad argument #1 (table is not a peripheral)", 2)
|
||||||
|
end
|
||||||
|
return mt.types[peripheral_type] ~= nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get all available methods for the peripheral with the given name.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the peripheral to find.
|
||||||
|
-- @treturn { string... }|nil A list of methods provided by this peripheral, or `nil` if
|
||||||
|
-- it is not present.
|
||||||
|
function getMethods(name)
|
||||||
|
expect(1, name, "string")
|
||||||
|
if native.isPresent(name) then
|
||||||
|
return native.getMethods(name)
|
||||||
|
end
|
||||||
|
for n = 1, #sides do
|
||||||
|
local side = sides[n]
|
||||||
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
|
||||||
|
return native.call(side, "getMethodsRemote", name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the name of a peripheral wrapped with @{peripheral.wrap}.
|
||||||
|
--
|
||||||
|
-- @tparam table peripheral The peripheral to get the name of.
|
||||||
|
-- @treturn string The name of the given peripheral.
|
||||||
|
-- @since 1.88.0
|
||||||
|
function getName(peripheral)
|
||||||
|
expect(1, peripheral, "table")
|
||||||
|
local mt = getmetatable(peripheral)
|
||||||
|
if not mt or mt.__name ~= "peripheral" or type(mt.name) ~= "string" then
|
||||||
|
error("bad argument #1 (table is not a peripheral)", 2)
|
||||||
|
end
|
||||||
|
return mt.name
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Call a method on the peripheral with the given name.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the peripheral to invoke the method on.
|
||||||
|
-- @tparam string method The name of the method
|
||||||
|
-- @param ... Additional arguments to pass to the method
|
||||||
|
-- @return The return values of the peripheral method.
|
||||||
|
--
|
||||||
|
-- @usage Open the modem on the top of this computer.
|
||||||
|
--
|
||||||
|
-- peripheral.call("top", "open", 1)
|
||||||
|
function call(name, method, ...)
|
||||||
|
expect(1, name, "string")
|
||||||
|
expect(2, method, "string")
|
||||||
|
if native.isPresent(name) then
|
||||||
|
return native.call(name, method, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
for n = 1, #sides do
|
||||||
|
local side = sides[n]
|
||||||
|
if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then
|
||||||
|
return native.call(side, "callRemote", name, method, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get a table containing all functions available on a peripheral. These can
|
||||||
|
-- then be called instead of using @{peripheral.call} every time.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the peripheral to wrap.
|
||||||
|
-- @treturn table|nil The table containing the peripheral's methods, or `nil` if
|
||||||
|
-- there is no peripheral present with the given name.
|
||||||
|
-- @usage Open the modem on the top of this computer.
|
||||||
|
--
|
||||||
|
-- local modem = peripheral.wrap("top")
|
||||||
|
-- modem.open(1)
|
||||||
|
function wrap(name)
|
||||||
|
expect(1, name, "string")
|
||||||
|
|
||||||
|
local methods = peripheral.getMethods(name)
|
||||||
|
if not methods then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We store our types array as a list (for getType) and a lookup table (for hasType).
|
||||||
|
local types = { peripheral.getType(name) }
|
||||||
|
for i = 1, #types do types[types[i]] = true end
|
||||||
|
local result = setmetatable({}, {
|
||||||
|
__name = "peripheral",
|
||||||
|
name = name,
|
||||||
|
type = types[1],
|
||||||
|
types = types,
|
||||||
|
})
|
||||||
|
for _, method in ipairs(methods) do
|
||||||
|
result[method] = function(...)
|
||||||
|
return peripheral.call(name, method, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Find all peripherals of a specific type, and return the
|
||||||
|
@{peripheral.wrap|wrapped} peripherals.
|
||||||
|
|
||||||
|
@tparam string ty The type of peripheral to look for.
|
||||||
|
@tparam[opt] function(name:string, wrapped:table):boolean filter A
|
||||||
|
filter function, which takes the peripheral's name and wrapped table
|
||||||
|
and returns if it should be included in the result.
|
||||||
|
@treturn table... 0 or more wrapped peripherals matching the given filters.
|
||||||
|
@usage Find all monitors and store them in a table, writing "Hello" on each one.
|
||||||
|
|
||||||
|
local monitors = { peripheral.find("monitor") }
|
||||||
|
for _, monitor in pairs(monitors) do
|
||||||
|
monitor.write("Hello")
|
||||||
|
end
|
||||||
|
|
||||||
|
@usage Find all wireless modems connected to this computer.
|
||||||
|
|
||||||
|
local modems = { peripheral.find("modem", function(name, modem)
|
||||||
|
return modem.isWireless() -- Check this modem is wireless.
|
||||||
|
end) }
|
||||||
|
|
||||||
|
@usage This abuses the `filter` argument to call @{rednet.open} on every modem.
|
||||||
|
|
||||||
|
peripheral.find("modem", rednet.open)
|
||||||
|
@since 1.6
|
||||||
|
]]
|
||||||
|
function find(ty, filter)
|
||||||
|
expect(1, ty, "string")
|
||||||
|
expect(2, filter, "function", "nil")
|
||||||
|
|
||||||
|
local results = {}
|
||||||
|
for _, name in ipairs(peripheral.getNames()) do
|
||||||
|
if peripheral.hasType(name, ty) then
|
||||||
|
local wrapped = peripheral.wrap(name)
|
||||||
|
if filter == nil or filter(name, wrapped) then
|
||||||
|
table.insert(results, wrapped)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.unpack(results)
|
||||||
|
end
|
506
src/main/resources/assets/cctweaked/lua/rom/apis/rednet.lua
Normal file
506
src/main/resources/assets/cctweaked/lua/rom/apis/rednet.lua
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- Communicate with other computers by using @{modem|modems}. @{rednet}
|
||||||
|
provides a layer of abstraction on top of the main @{modem} peripheral, making
|
||||||
|
it slightly easier to use.
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
In order to send a message between two computers, each computer must have a
|
||||||
|
modem on one of its sides (or in the case of pocket computers and turtles, the
|
||||||
|
modem must be equipped as an upgrade). The two computers should then call
|
||||||
|
@{rednet.open}, which sets up the modems ready to send and receive messages.
|
||||||
|
|
||||||
|
Once rednet is opened, you can send messages using @{rednet.send} and receive
|
||||||
|
them using @{rednet.receive}. It's also possible to send a message to _every_
|
||||||
|
rednet-using computer using @{rednet.broadcast}.
|
||||||
|
|
||||||
|
:::caution Network security
|
||||||
|
|
||||||
|
While rednet provides a friendly way to send messages to specific computers, it
|
||||||
|
doesn't provide any guarantees about security. Other computers could be
|
||||||
|
listening in to your messages, or even pretending to send messages from other computers!
|
||||||
|
|
||||||
|
If you're playing on a multi-player server (or at least one where you don't
|
||||||
|
trust other players), it's worth encrypting or signing your rednet messages.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Protocols and hostnames
|
||||||
|
Several rednet messages accept "protocol"s - simple string names describing what
|
||||||
|
a message is about. When sending messages using @{rednet.send} and
|
||||||
|
@{rednet.broadcast}, you can optionally specify a protocol for the message. This
|
||||||
|
same protocol can then be given to @{rednet.receive}, to ignore all messages not
|
||||||
|
using this protocol.
|
||||||
|
|
||||||
|
It's also possible to look-up computers based on protocols, providing a basic
|
||||||
|
system for service discovery and [DNS]. A computer can advertise that it
|
||||||
|
supports a particular protocol with @{rednet.host}, also providing a friendly
|
||||||
|
"hostname". Other computers may then find all computers which support this
|
||||||
|
protocol using @{rednet.lookup}.
|
||||||
|
|
||||||
|
[DNS]: https://en.wikipedia.org/wiki/Domain_Name_System "Domain Name System"
|
||||||
|
|
||||||
|
@module rednet
|
||||||
|
@since 1.2
|
||||||
|
@see rednet_message Queued when a rednet message is received.
|
||||||
|
@see modem Rednet is built on top of the modem peripheral. Modems provide a more
|
||||||
|
bare-bones but flexible interface.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
--- The channel used by the Rednet API to @{broadcast} messages.
|
||||||
|
CHANNEL_BROADCAST = 65535
|
||||||
|
|
||||||
|
--- The channel used by the Rednet API to repeat messages.
|
||||||
|
CHANNEL_REPEAT = 65533
|
||||||
|
|
||||||
|
--- The number of channels rednet reserves for computer IDs. Computers with IDs
|
||||||
|
-- greater or equal to this limit wrap around to 0.
|
||||||
|
MAX_ID_CHANNELS = 65500
|
||||||
|
|
||||||
|
local received_messages = {}
|
||||||
|
local hostnames = {}
|
||||||
|
local prune_received_timer
|
||||||
|
|
||||||
|
local function id_as_channel(id)
|
||||||
|
return (id or os.getComputerID()) % MAX_ID_CHANNELS
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Opens a modem with the given @{peripheral} name, allowing it to send and
|
||||||
|
receive messages over rednet.
|
||||||
|
|
||||||
|
This will open the modem on two channels: one which has the same
|
||||||
|
@{os.getComputerID|ID} as the computer, and another on
|
||||||
|
@{CHANNEL_BROADCAST|the broadcast channel}.
|
||||||
|
|
||||||
|
@tparam string modem The name of the modem to open.
|
||||||
|
@throws If there is no such modem with the given name
|
||||||
|
@usage Open rednet on the back of the computer, allowing you to send and receive
|
||||||
|
rednet messages using it.
|
||||||
|
|
||||||
|
rednet.open("back")
|
||||||
|
|
||||||
|
@usage Open rednet on all attached modems. This abuses the "filter" argument to
|
||||||
|
@{peripheral.find}.
|
||||||
|
|
||||||
|
peripheral.find("modem", rednet.open)
|
||||||
|
@see rednet.close
|
||||||
|
@see rednet.isOpen
|
||||||
|
]]
|
||||||
|
function open(modem)
|
||||||
|
expect(1, modem, "string")
|
||||||
|
if peripheral.getType(modem) ~= "modem" then
|
||||||
|
error("No such modem: " .. modem, 2)
|
||||||
|
end
|
||||||
|
peripheral.call(modem, "open", id_as_channel())
|
||||||
|
peripheral.call(modem, "open", CHANNEL_BROADCAST)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Close a modem with the given @{peripheral} name, meaning it can no longer
|
||||||
|
-- send and receive rednet messages.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] string modem The side the modem exists on. If not given, all
|
||||||
|
-- open modems will be closed.
|
||||||
|
-- @throws If there is no such modem with the given name
|
||||||
|
-- @see rednet.open
|
||||||
|
function close(modem)
|
||||||
|
expect(1, modem, "string", "nil")
|
||||||
|
if modem then
|
||||||
|
-- Close a specific modem
|
||||||
|
if peripheral.getType(modem) ~= "modem" then
|
||||||
|
error("No such modem: " .. modem, 2)
|
||||||
|
end
|
||||||
|
peripheral.call(modem, "close", id_as_channel())
|
||||||
|
peripheral.call(modem, "close", CHANNEL_BROADCAST)
|
||||||
|
else
|
||||||
|
-- Close all modems
|
||||||
|
for _, modem in ipairs(peripheral.getNames()) do
|
||||||
|
if isOpen(modem) then
|
||||||
|
close(modem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Determine if rednet is currently open.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] string modem Which modem to check. If not given, all connected
|
||||||
|
-- modems will be checked.
|
||||||
|
-- @treturn boolean If the given modem is open.
|
||||||
|
-- @since 1.31
|
||||||
|
-- @see rednet.open
|
||||||
|
function isOpen(modem)
|
||||||
|
expect(1, modem, "string", "nil")
|
||||||
|
if modem then
|
||||||
|
-- Check if a specific modem is open
|
||||||
|
if peripheral.getType(modem) == "modem" then
|
||||||
|
return peripheral.call(modem, "isOpen", id_as_channel()) and peripheral.call(modem, "isOpen", CHANNEL_BROADCAST)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Check if any modem is open
|
||||||
|
for _, modem in ipairs(peripheral.getNames()) do
|
||||||
|
if isOpen(modem) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Allows a computer or turtle with an attached modem to send a message
|
||||||
|
intended for a sycomputer with a specific ID. At least one such modem must first
|
||||||
|
be @{rednet.open|opened} before sending is possible.
|
||||||
|
|
||||||
|
Assuming the target was in range and also had a correctly opened modem, the
|
||||||
|
target computer may then use @{rednet.receive} to collect the message.
|
||||||
|
|
||||||
|
@tparam number recipient The ID of the receiving computer.
|
||||||
|
@param message The message to send. Like with @{modem.transmit}, this can
|
||||||
|
contain any primitive type (numbers, booleans and strings) as well as
|
||||||
|
tables. Other types (like functions), as well as metatables, will not be
|
||||||
|
transmitted.
|
||||||
|
@tparam[opt] string protocol The "protocol" to send this message under. When
|
||||||
|
using @{rednet.receive} one can filter to only receive messages sent under a
|
||||||
|
particular protocol.
|
||||||
|
@treturn boolean If this message was successfully sent (i.e. if rednet is
|
||||||
|
currently @{rednet.open|open}). Note, this does not guarantee the message was
|
||||||
|
actually _received_.
|
||||||
|
@changed 1.6 Added protocol parameter.
|
||||||
|
@changed 1.82.0 Now returns whether the message was successfully sent.
|
||||||
|
@see rednet.receive
|
||||||
|
@usage Send a message to computer #2.
|
||||||
|
|
||||||
|
rednet.send(2, "Hello from rednet!")
|
||||||
|
]]
|
||||||
|
function send(recipient, message, protocol)
|
||||||
|
expect(1, recipient, "number")
|
||||||
|
expect(3, protocol, "string", "nil")
|
||||||
|
-- Generate a (probably) unique message ID
|
||||||
|
-- We could do other things to guarantee uniqueness, but we really don't need to
|
||||||
|
-- Store it to ensure we don't get our own messages back
|
||||||
|
local message_id = math.random(1, 2147483647)
|
||||||
|
received_messages[message_id] = os.clock() + 9.5
|
||||||
|
if not prune_received_timer then prune_received_timer = os.startTimer(10) end
|
||||||
|
|
||||||
|
-- Create the message
|
||||||
|
local reply_channel = id_as_channel()
|
||||||
|
local message_wrapper = {
|
||||||
|
nMessageID = message_id,
|
||||||
|
nRecipient = recipient,
|
||||||
|
nSender = os.getComputerID(),
|
||||||
|
message = message,
|
||||||
|
sProtocol = protocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
local sent = false
|
||||||
|
if recipient == os.getComputerID() then
|
||||||
|
-- Loopback to ourselves
|
||||||
|
os.queueEvent("rednet_message", os.getComputerID(), message, protocol)
|
||||||
|
sent = true
|
||||||
|
else
|
||||||
|
-- Send on all open modems, to the target and to repeaters
|
||||||
|
if recipient ~= CHANNEL_BROADCAST then
|
||||||
|
recipient = id_as_channel(recipient)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, modem in ipairs(peripheral.getNames()) do
|
||||||
|
if isOpen(modem) then
|
||||||
|
peripheral.call(modem, "transmit", recipient, reply_channel, message_wrapper)
|
||||||
|
peripheral.call(modem, "transmit", CHANNEL_REPEAT, reply_channel, message_wrapper)
|
||||||
|
sent = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return sent
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Broadcasts a string message over the predefined @{CHANNEL_BROADCAST}
|
||||||
|
channel. The message will be received by every device listening to rednet.
|
||||||
|
|
||||||
|
@param message The message to send. This should not contain coroutines or
|
||||||
|
functions, as they will be converted to @{nil}.
|
||||||
|
@tparam[opt] string protocol The "protocol" to send this message under. When
|
||||||
|
using @{rednet.receive} one can filter to only receive messages sent under a
|
||||||
|
particular protocol.
|
||||||
|
@see rednet.receive
|
||||||
|
@changed 1.6 Added protocol parameter.
|
||||||
|
@usage Broadcast the words "Hello, world!" to every computer using rednet.
|
||||||
|
|
||||||
|
rednet.broadcast("Hello, world!")
|
||||||
|
]]
|
||||||
|
function broadcast(message, protocol)
|
||||||
|
expect(2, protocol, "string", "nil")
|
||||||
|
send(CHANNEL_BROADCAST, message, protocol)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Wait for a rednet message to be received, or until `nTimeout` seconds have
|
||||||
|
elapsed.
|
||||||
|
|
||||||
|
@tparam[opt] string protocol_filter The protocol the received message must be
|
||||||
|
sent with. If specified, any messages not sent under this protocol will be
|
||||||
|
discarded.
|
||||||
|
@tparam[opt] number timeout The number of seconds to wait if no message is
|
||||||
|
received.
|
||||||
|
@treturn[1] number The computer which sent this message
|
||||||
|
@return[1] The received message
|
||||||
|
@treturn[1] string|nil The protocol this message was sent under.
|
||||||
|
@treturn[2] nil If the timeout elapsed and no message was received.
|
||||||
|
@see rednet.broadcast
|
||||||
|
@see rednet.send
|
||||||
|
@changed 1.6 Added protocol filter parameter.
|
||||||
|
@usage Receive a rednet message.
|
||||||
|
|
||||||
|
local id, message = rednet.receive()
|
||||||
|
print(("Computer %d sent message %s"):format(id, message))
|
||||||
|
|
||||||
|
@usage Receive a message, stopping after 5 seconds if no message was received.
|
||||||
|
|
||||||
|
local id, message = rednet.receive(nil, 5)
|
||||||
|
if not id then
|
||||||
|
printError("No message received")
|
||||||
|
else
|
||||||
|
print(("Computer %d sent message %s"):format(id, message))
|
||||||
|
end
|
||||||
|
|
||||||
|
@usage Receive a message from computer #2.
|
||||||
|
|
||||||
|
local id, message
|
||||||
|
repeat
|
||||||
|
id, message = rednet.receive()
|
||||||
|
until id == 2
|
||||||
|
|
||||||
|
print(message)
|
||||||
|
]]
|
||||||
|
function receive(protocol_filter, timeout)
|
||||||
|
-- The parameters used to be ( nTimeout ), detect this case for backwards compatibility
|
||||||
|
if type(protocol_filter) == "number" and timeout == nil then
|
||||||
|
protocol_filter, timeout = nil, protocol_filter
|
||||||
|
end
|
||||||
|
expect(1, protocol_filter, "string", "nil")
|
||||||
|
expect(2, timeout, "number", "nil")
|
||||||
|
|
||||||
|
-- Start the timer
|
||||||
|
local timer = nil
|
||||||
|
local event_filter = nil
|
||||||
|
if timeout then
|
||||||
|
timer = os.startTimer(timeout)
|
||||||
|
event_filter = nil
|
||||||
|
else
|
||||||
|
event_filter = "rednet_message"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Wait for events
|
||||||
|
while true do
|
||||||
|
local event, p1, p2, p3 = os.pullEvent(event_filter)
|
||||||
|
if event == "rednet_message" then
|
||||||
|
-- Return the first matching rednet_message
|
||||||
|
local sender_id, message, protocol = p1, p2, p3
|
||||||
|
if protocol_filter == nil or protocol == protocol_filter then
|
||||||
|
return sender_id, message, protocol
|
||||||
|
end
|
||||||
|
elseif event == "timer" then
|
||||||
|
-- Return nil if we timeout
|
||||||
|
if p1 == timer then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Register the system as "hosting" the desired protocol under the specified
|
||||||
|
name. If a rednet @{rednet.lookup|lookup} is performed for that protocol (and
|
||||||
|
maybe name) on the same network, the registered system will automatically
|
||||||
|
respond via a background process, hence providing the system performing the
|
||||||
|
lookup with its ID number.
|
||||||
|
|
||||||
|
Multiple computers may not register themselves on the same network as having the
|
||||||
|
same names against the same protocols, and the title `localhost` is specifically
|
||||||
|
reserved. They may, however, share names as long as their hosted protocols are
|
||||||
|
different, or if they only join a given network after "registering" themselves
|
||||||
|
before doing so (eg while offline or part of a different network).
|
||||||
|
|
||||||
|
@tparam string protocol The protocol this computer provides.
|
||||||
|
@tparam string hostname The name this computer exposes for the given protocol.
|
||||||
|
@throws If trying to register a hostname which is reserved, or currently in use.
|
||||||
|
@see rednet.unhost
|
||||||
|
@see rednet.lookup
|
||||||
|
@since 1.6
|
||||||
|
]]
|
||||||
|
function host(protocol, hostname)
|
||||||
|
expect(1, protocol, "string")
|
||||||
|
expect(2, hostname, "string")
|
||||||
|
if hostname == "localhost" then
|
||||||
|
error("Reserved hostname", 2)
|
||||||
|
end
|
||||||
|
if hostnames[protocol] ~= hostname then
|
||||||
|
if lookup(protocol, hostname) ~= nil then
|
||||||
|
error("Hostname in use", 2)
|
||||||
|
end
|
||||||
|
hostnames[protocol] = hostname
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer
|
||||||
|
-- respond to @{rednet.lookup} requests.
|
||||||
|
--
|
||||||
|
-- @tparam string protocol The protocol to unregister your self from.
|
||||||
|
-- @since 1.6
|
||||||
|
function unhost(protocol)
|
||||||
|
expect(1, protocol, "string")
|
||||||
|
hostnames[protocol] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Search the local rednet network for systems @{rednet.host|hosting} the
|
||||||
|
desired protocol and returns any computer IDs that respond as "registered"
|
||||||
|
against it.
|
||||||
|
|
||||||
|
If a hostname is specified, only one ID will be returned (assuming an exact
|
||||||
|
match is found).
|
||||||
|
|
||||||
|
@tparam string protocol The protocol to search for.
|
||||||
|
@tparam[opt] string hostname The hostname to search for.
|
||||||
|
|
||||||
|
@treturn[1] number... A list of computer IDs hosting the given protocol.
|
||||||
|
@treturn[2] number|nil The computer ID with the provided hostname and protocol,
|
||||||
|
or @{nil} if none exists.
|
||||||
|
@since 1.6
|
||||||
|
@usage Find all computers which are hosting the `"chat"` protocol.
|
||||||
|
|
||||||
|
local computers = {rednet.lookup("chat")}
|
||||||
|
print(#computers .. " computers available to chat")
|
||||||
|
for _, computer in pairs(computers) do
|
||||||
|
print("Computer #" .. computer)
|
||||||
|
end
|
||||||
|
|
||||||
|
@usage Find a computer hosting the `"chat"` protocol with a hostname of `"my_host"`.
|
||||||
|
|
||||||
|
local id = rednet.lookup("chat", "my_host")
|
||||||
|
if id then
|
||||||
|
print("Found my_host at computer #" .. id)
|
||||||
|
else
|
||||||
|
printError("Cannot find my_host")
|
||||||
|
end
|
||||||
|
|
||||||
|
]]
|
||||||
|
function lookup(protocol, hostname)
|
||||||
|
expect(1, protocol, "string")
|
||||||
|
expect(2, hostname, "string", "nil")
|
||||||
|
|
||||||
|
-- Build list of host IDs
|
||||||
|
local results = nil
|
||||||
|
if hostname == nil then
|
||||||
|
results = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check localhost first
|
||||||
|
if hostnames[protocol] then
|
||||||
|
if hostname == nil then
|
||||||
|
table.insert(results, os.getComputerID())
|
||||||
|
elseif hostname == "localhost" or hostname == hostnames[protocol] then
|
||||||
|
return os.getComputerID()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isOpen() then
|
||||||
|
if results then
|
||||||
|
return table.unpack(results)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Broadcast a lookup packet
|
||||||
|
broadcast({
|
||||||
|
sType = "lookup",
|
||||||
|
sProtocol = protocol,
|
||||||
|
sHostname = hostname,
|
||||||
|
}, "dns")
|
||||||
|
|
||||||
|
-- Start a timer
|
||||||
|
local timer = os.startTimer(2)
|
||||||
|
|
||||||
|
-- Wait for events
|
||||||
|
while true do
|
||||||
|
local event, p1, p2, p3 = os.pullEvent()
|
||||||
|
if event == "rednet_message" then
|
||||||
|
-- Got a rednet message, check if it's the response to our request
|
||||||
|
local sender_id, message, message_protocol = p1, p2, p3
|
||||||
|
if message_protocol == "dns" and type(message) == "table" and message.sType == "lookup response" then
|
||||||
|
if message.sProtocol == protocol then
|
||||||
|
if hostname == nil then
|
||||||
|
table.insert(results, sender_id)
|
||||||
|
elseif message.sHostname == hostname then
|
||||||
|
return sender_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif event == "timer" and p1 == timer then
|
||||||
|
-- Got a timer event, check it's the end of our timeout
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if results then
|
||||||
|
return table.unpack(results)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local started = false
|
||||||
|
|
||||||
|
--- Listen for modem messages and converts them into rednet messages, which may
|
||||||
|
-- then be @{receive|received}.
|
||||||
|
--
|
||||||
|
-- This is automatically started in the background on computer startup, and
|
||||||
|
-- should not be called manually.
|
||||||
|
function run()
|
||||||
|
if started then
|
||||||
|
error("rednet is already running", 2)
|
||||||
|
end
|
||||||
|
started = true
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local event, p1, p2, p3, p4 = os.pullEventRaw()
|
||||||
|
if event == "modem_message" then
|
||||||
|
-- Got a modem message, process it and add it to the rednet event queue
|
||||||
|
local modem, channel, reply_channel, message = p1, p2, p3, p4
|
||||||
|
if channel == id_as_channel() or channel == CHANNEL_BROADCAST then
|
||||||
|
if type(message) == "table" and type(message.nMessageID) == "number"
|
||||||
|
and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID]
|
||||||
|
and (type(message.nSender) == "nil" or (type(message.nSender) == "number" and message.nSender == message.nSender))
|
||||||
|
and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST)
|
||||||
|
and isOpen(modem)
|
||||||
|
then
|
||||||
|
received_messages[message.nMessageID] = os.clock() + 9.5
|
||||||
|
if not prune_received_timer then prune_received_timer = os.startTimer(10) end
|
||||||
|
os.queueEvent("rednet_message", message.nSender or reply_channel, message.message, message.sProtocol)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event == "rednet_message" then
|
||||||
|
-- Got a rednet message (queued from above), respond to dns lookup
|
||||||
|
local sender, message, protocol = p1, p2, p3
|
||||||
|
if protocol == "dns" and type(message) == "table" and message.sType == "lookup" then
|
||||||
|
local hostname = hostnames[message.sProtocol]
|
||||||
|
if hostname ~= nil and (message.sHostname == nil or message.sHostname == hostname) then
|
||||||
|
send(sender, {
|
||||||
|
sType = "lookup response",
|
||||||
|
sHostname = hostname,
|
||||||
|
sProtocol = message.sProtocol,
|
||||||
|
}, "dns")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event == "timer" and p1 == prune_received_timer then
|
||||||
|
-- Got a timer event, use it to prune the set of received messages
|
||||||
|
prune_received_timer = nil
|
||||||
|
local now, has_more = os.clock(), nil
|
||||||
|
for message_id, deadline in pairs(received_messages) do
|
||||||
|
if deadline <= now then received_messages[message_id] = nil
|
||||||
|
else has_more = true end
|
||||||
|
end
|
||||||
|
prune_received_timer = has_more and os.startTimer(10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
251
src/main/resources/assets/cctweaked/lua/rom/apis/settings.lua
Normal file
251
src/main/resources/assets/cctweaked/lua/rom/apis/settings.lua
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Read and write configuration options for CraftOS and your programs.
|
||||||
|
--
|
||||||
|
-- By default, the settings API will load its configuration from the
|
||||||
|
-- `/.settings` file. One can then use @{settings.save} to update the file.
|
||||||
|
--
|
||||||
|
-- @module settings
|
||||||
|
-- @since 1.78
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua")
|
||||||
|
local type, expect, field = type, expect.expect, expect.field
|
||||||
|
|
||||||
|
local details, values = {}, {}
|
||||||
|
|
||||||
|
local function reserialize(value)
|
||||||
|
if type(value) ~= "table" then return value end
|
||||||
|
return textutils.unserialize(textutils.serialize(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function copy(value)
|
||||||
|
if type(value) ~= "table" then return value end
|
||||||
|
local result = {}
|
||||||
|
for k, v in pairs(value) do result[k] = copy(v) end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local valid_types = { "number", "string", "boolean", "table" }
|
||||||
|
for _, v in ipairs(valid_types) do valid_types[v] = true end
|
||||||
|
|
||||||
|
--- Define a new setting, optional specifying various properties about it.
|
||||||
|
--
|
||||||
|
-- While settings do not have to be added before being used, doing so allows
|
||||||
|
-- you to provide defaults and additional metadata.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of this option
|
||||||
|
-- @tparam[opt] { description? = string, default? = any, type? = string } options
|
||||||
|
-- Options for this setting. This table accepts the following fields:
|
||||||
|
--
|
||||||
|
-- - `description`: A description which may be printed when running the `set` program.
|
||||||
|
-- - `default`: A default value, which is returned by @{settings.get} if the
|
||||||
|
-- setting has not been changed.
|
||||||
|
-- - `type`: Require values to be of this type. @{set|Setting} the value to another type
|
||||||
|
-- will error.
|
||||||
|
-- @since 1.87.0
|
||||||
|
function define(name, options)
|
||||||
|
expect(1, name, "string")
|
||||||
|
expect(2, options, "table", "nil")
|
||||||
|
|
||||||
|
if options then
|
||||||
|
options = {
|
||||||
|
description = field(options, "description", "string", "nil"),
|
||||||
|
default = reserialize(field(options, "default", "number", "string", "boolean", "table", "nil")),
|
||||||
|
type = field(options, "type", "string", "nil"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.type and not valid_types[options.type] then
|
||||||
|
error(("Unknown type %q. Expected one of %s."):format(options.type, table.concat(valid_types, ", ")), 2)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
options = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
details[name] = options
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Remove a @{define|definition} of a setting.
|
||||||
|
--
|
||||||
|
-- If a setting has been changed, this does not remove its value. Use @{settings.unset}
|
||||||
|
-- for that.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of this option
|
||||||
|
-- @since 1.87.0
|
||||||
|
function undefine(name)
|
||||||
|
expect(1, name, "string")
|
||||||
|
details[name] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set_value(name, new)
|
||||||
|
local old = values[name]
|
||||||
|
if old == nil then
|
||||||
|
local opt = details[name]
|
||||||
|
old = opt and opt.default
|
||||||
|
end
|
||||||
|
|
||||||
|
values[name] = new
|
||||||
|
if old ~= new then
|
||||||
|
-- This should be safe, as os.queueEvent copies values anyway.
|
||||||
|
os.queueEvent("setting_changed", name, new, old)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the value of a setting.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the setting to set
|
||||||
|
-- @param value The setting's value. This cannot be `nil`, and must be
|
||||||
|
-- serialisable by @{textutils.serialize}.
|
||||||
|
-- @throws If this value cannot be serialised
|
||||||
|
-- @see settings.unset
|
||||||
|
function set(name, value)
|
||||||
|
expect(1, name, "string")
|
||||||
|
expect(2, value, "number", "string", "boolean", "table")
|
||||||
|
|
||||||
|
local opt = details[name]
|
||||||
|
if opt and opt.type then expect(2, value, opt.type) end
|
||||||
|
|
||||||
|
set_value(name, reserialize(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the value of a setting.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the setting to get.
|
||||||
|
-- @param[opt] default The value to use should there be pre-existing value for
|
||||||
|
-- this setting. If not given, it will use the setting's default value if given,
|
||||||
|
-- or `nil` otherwise.
|
||||||
|
-- @return The setting's, or the default if the setting has not been changed.
|
||||||
|
-- @changed 1.87.0 Now respects default value if pre-defined and `default` is unset.
|
||||||
|
function get(name, default)
|
||||||
|
expect(1, name, "string")
|
||||||
|
local result = values[name]
|
||||||
|
if result ~= nil then
|
||||||
|
return copy(result)
|
||||||
|
elseif default ~= nil then
|
||||||
|
return default
|
||||||
|
else
|
||||||
|
local opt = details[name]
|
||||||
|
return opt and copy(opt.default)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get details about a specific setting.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the setting to get.
|
||||||
|
-- @treturn { description? = string, default? = any, type? = string, value? = any }
|
||||||
|
-- Information about this setting. This includes all information from @{settings.define},
|
||||||
|
-- as well as this setting's value.
|
||||||
|
-- @since 1.87.0
|
||||||
|
function getDetails(name)
|
||||||
|
expect(1, name, "string")
|
||||||
|
local deets = copy(details[name]) or {}
|
||||||
|
deets.value = values[name]
|
||||||
|
deets.changed = deets.value ~= nil
|
||||||
|
if deets.value == nil then deets.value = deets.default end
|
||||||
|
return deets
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Remove the value of a setting, setting it to the default.
|
||||||
|
--
|
||||||
|
-- @{settings.get} will return the default value until the setting's value is
|
||||||
|
-- @{settings.set|set}, or the computer is rebooted.
|
||||||
|
--
|
||||||
|
-- @tparam string name The name of the setting to unset.
|
||||||
|
-- @see settings.set
|
||||||
|
-- @see settings.clear
|
||||||
|
function unset(name)
|
||||||
|
expect(1, name, "string")
|
||||||
|
set_value(name, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Resets the value of all settings. Equivalent to calling @{settings.unset}
|
||||||
|
--- on every setting.
|
||||||
|
--
|
||||||
|
-- @see settings.unset
|
||||||
|
function clear()
|
||||||
|
for name in pairs(values) do
|
||||||
|
set_value(name, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the names of all currently defined settings.
|
||||||
|
--
|
||||||
|
-- @treturn { string } An alphabetically sorted list of all currently-defined
|
||||||
|
-- settings.
|
||||||
|
function getNames()
|
||||||
|
local result, n = {}, 1
|
||||||
|
for k in pairs(details) do
|
||||||
|
result[n], n = k, n + 1
|
||||||
|
end
|
||||||
|
for k in pairs(values) do
|
||||||
|
if not details[k] then result[n], n = k, n + 1 end
|
||||||
|
end
|
||||||
|
table.sort(result)
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Load settings from the given file.
|
||||||
|
--
|
||||||
|
-- Existing settings will be merged with any pre-existing ones. Conflicting
|
||||||
|
-- entries will be overwritten, but any others will be preserved.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] string sPath The file to load from, defaulting to `.settings`.
|
||||||
|
-- @treturn boolean Whether settings were successfully read from this
|
||||||
|
-- file. Reasons for failure may include the file not existing or being
|
||||||
|
-- corrupted.
|
||||||
|
--
|
||||||
|
-- @see settings.save
|
||||||
|
-- @changed 1.87.0 `sPath` is now optional.
|
||||||
|
function load(sPath)
|
||||||
|
expect(1, sPath, "string", "nil")
|
||||||
|
local file = fs.open(sPath or ".settings", "r")
|
||||||
|
if not file then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local sText = file.readAll()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
local tFile = textutils.unserialize(sText)
|
||||||
|
if type(tFile) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(tFile) do
|
||||||
|
local ty_v = type(v)
|
||||||
|
if type(k) == "string" and (ty_v == "string" or ty_v == "number" or ty_v == "boolean" or ty_v == "table") then
|
||||||
|
local opt = details[k]
|
||||||
|
if not opt or not opt.type or ty_v == opt.type then
|
||||||
|
-- This may fail if the table is recursive (or otherwise cannot be serialized).
|
||||||
|
local ok, v = pcall(reserialize, v)
|
||||||
|
if ok then set_value(k, v) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Save settings to the given file.
|
||||||
|
--
|
||||||
|
-- This will entirely overwrite the pre-existing file. Settings defined in the
|
||||||
|
-- file, but not currently loaded will be removed.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] string sPath The path to save settings to, defaulting to `.settings`.
|
||||||
|
-- @treturn boolean If the settings were successfully saved.
|
||||||
|
--
|
||||||
|
-- @see settings.load
|
||||||
|
-- @changed 1.87.0 `sPath` is now optional.
|
||||||
|
function save(sPath)
|
||||||
|
expect(1, sPath, "string", "nil")
|
||||||
|
local file = fs.open(sPath or ".settings", "w")
|
||||||
|
if not file then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
file.write(textutils.serialize(values))
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
93
src/main/resources/assets/cctweaked/lua/rom/apis/term.lua
Normal file
93
src/main/resources/assets/cctweaked/lua/rom/apis/term.lua
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- @module term
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local native = term.native and term.native() or term
|
||||||
|
local redirectTarget = native
|
||||||
|
|
||||||
|
local function wrap(_sFunction)
|
||||||
|
return function(...)
|
||||||
|
return redirectTarget[_sFunction](...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local term = _ENV
|
||||||
|
|
||||||
|
--- Redirects terminal output to a monitor, a @{window}, or any other custom
|
||||||
|
-- terminal object. Once the redirect is performed, any calls to a "term"
|
||||||
|
-- function - or to a function that makes use of a term function, as @{print} -
|
||||||
|
-- will instead operate with the new terminal object.
|
||||||
|
--
|
||||||
|
-- A "terminal object" is simply a table that contains functions with the same
|
||||||
|
-- names - and general features - as those found in the term table. For example,
|
||||||
|
-- a wrapped monitor is suitable.
|
||||||
|
--
|
||||||
|
-- The redirect can be undone by pointing back to the previous terminal object
|
||||||
|
-- (which this function returns whenever you switch).
|
||||||
|
--
|
||||||
|
-- @tparam Redirect target The terminal redirect the @{term} API will draw to.
|
||||||
|
-- @treturn Redirect The previous redirect object, as returned by
|
||||||
|
-- @{term.current}.
|
||||||
|
-- @since 1.31
|
||||||
|
-- @usage
|
||||||
|
-- Redirect to a monitor on the right of the computer.
|
||||||
|
-- term.redirect(peripheral.wrap("right"))
|
||||||
|
term.redirect = function(target)
|
||||||
|
expect(1, target, "table")
|
||||||
|
if target == term or target == _G.term then
|
||||||
|
error("term is not a recommended redirect target, try term.current() instead", 2)
|
||||||
|
end
|
||||||
|
for k, v in pairs(native) do
|
||||||
|
if type(k) == "string" and type(v) == "function" then
|
||||||
|
if type(target[k]) ~= "function" then
|
||||||
|
target[k] = function()
|
||||||
|
error("Redirect object is missing method " .. k .. ".", 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local oldRedirectTarget = redirectTarget
|
||||||
|
redirectTarget = target
|
||||||
|
return oldRedirectTarget
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the current terminal object of the computer.
|
||||||
|
--
|
||||||
|
-- @treturn Redirect The current terminal redirect
|
||||||
|
-- @since 1.6
|
||||||
|
-- @usage
|
||||||
|
-- Create a new @{window} which draws to the current redirect target.
|
||||||
|
--
|
||||||
|
-- window.create(term.current(), 1, 1, 10, 10)
|
||||||
|
term.current = function()
|
||||||
|
return redirectTarget
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the native terminal object of the current computer.
|
||||||
|
--
|
||||||
|
-- It is recommended you do not use this function unless you absolutely have
|
||||||
|
-- to. In a multitasked environment, @{term.native} will _not_ be the current
|
||||||
|
-- terminal object, and so drawing may interfere with other programs.
|
||||||
|
--
|
||||||
|
-- @treturn Redirect The native terminal redirect.
|
||||||
|
-- @since 1.6
|
||||||
|
term.native = function()
|
||||||
|
return native
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Some methods shouldn't go through redirects, so we move them to the main
|
||||||
|
-- term API.
|
||||||
|
for _, method in ipairs { "nativePaletteColor", "nativePaletteColour" } do
|
||||||
|
term[method] = native[method]
|
||||||
|
native[method] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(native) do
|
||||||
|
if type(k) == "string" and type(v) == "function" and rawget(term, k) == nil then
|
||||||
|
term[k] = wrap(k)
|
||||||
|
end
|
||||||
|
end
|
969
src/main/resources/assets/cctweaked/lua/rom/apis/textutils.lua
Normal file
969
src/main/resources/assets/cctweaked/lua/rom/apis/textutils.lua
Normal file
@ -0,0 +1,969 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Helpful utilities for formatting and manipulating strings.
|
||||||
|
--
|
||||||
|
-- @module textutils
|
||||||
|
-- @since 1.2
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua")
|
||||||
|
local expect, field = expect.expect, expect.field
|
||||||
|
local wrap = dofile("rom/modules/main/cc/strings.lua").wrap
|
||||||
|
|
||||||
|
--- Slowly writes string text at current cursor position,
|
||||||
|
-- character-by-character.
|
||||||
|
--
|
||||||
|
-- Like @{_G.write}, this does not insert a newline at the end.
|
||||||
|
--
|
||||||
|
-- @tparam string text The the text to write to the screen
|
||||||
|
-- @tparam[opt] number rate The number of characters to write each second,
|
||||||
|
-- Defaults to 20.
|
||||||
|
-- @usage textutils.slowWrite("Hello, world!")
|
||||||
|
-- @usage textutils.slowWrite("Hello, world!", 5)
|
||||||
|
-- @since 1.3
|
||||||
|
function slowWrite(text, rate)
|
||||||
|
expect(2, rate, "number", "nil")
|
||||||
|
rate = rate or 20
|
||||||
|
if rate < 0 then
|
||||||
|
error("Rate must be positive", 2)
|
||||||
|
end
|
||||||
|
local to_sleep = 1 / rate
|
||||||
|
|
||||||
|
local wrapped_lines = wrap(tostring(text), (term.getSize()))
|
||||||
|
local wrapped_str = table.concat(wrapped_lines, "\n")
|
||||||
|
|
||||||
|
for n = 1, #wrapped_str do
|
||||||
|
sleep(to_sleep)
|
||||||
|
write(wrapped_str:sub(n, n))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Slowly prints string text at current cursor position,
|
||||||
|
-- character-by-character.
|
||||||
|
--
|
||||||
|
-- Like @{print}, this inserts a newline after printing.
|
||||||
|
--
|
||||||
|
-- @tparam string sText The the text to write to the screen
|
||||||
|
-- @tparam[opt] number nRate The number of characters to write each second,
|
||||||
|
-- Defaults to 20.
|
||||||
|
-- @usage textutils.slowPrint("Hello, world!")
|
||||||
|
-- @usage textutils.slowPrint("Hello, world!", 5)
|
||||||
|
function slowPrint(sText, nRate)
|
||||||
|
slowWrite(sText, nRate)
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Takes input time and formats it in a more readable format such as `6:30 PM`.
|
||||||
|
--
|
||||||
|
-- @tparam number nTime The time to format, as provided by @{os.time}.
|
||||||
|
-- @tparam[opt] boolean bTwentyFourHour Whether to format this as a 24-hour
|
||||||
|
-- clock (`18:30`) rather than a 12-hour one (`6:30 AM`)
|
||||||
|
-- @treturn string The formatted time
|
||||||
|
-- @usage Print the current in-game time as a 12-hour clock.
|
||||||
|
--
|
||||||
|
-- textutils.formatTime(os.time())
|
||||||
|
-- @usage Print the local time as a 24-hour clock.
|
||||||
|
--
|
||||||
|
-- textutils.formatTime(os.time("local"), true)
|
||||||
|
function formatTime(nTime, bTwentyFourHour)
|
||||||
|
expect(1, nTime, "number")
|
||||||
|
expect(2, bTwentyFourHour, "boolean", "nil")
|
||||||
|
local sTOD = nil
|
||||||
|
if not bTwentyFourHour then
|
||||||
|
if nTime >= 12 then
|
||||||
|
sTOD = "PM"
|
||||||
|
else
|
||||||
|
sTOD = "AM"
|
||||||
|
end
|
||||||
|
if nTime >= 13 then
|
||||||
|
nTime = nTime - 12
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local nHour = math.floor(nTime)
|
||||||
|
local nMinute = math.floor((nTime - nHour) * 60)
|
||||||
|
if sTOD then
|
||||||
|
return string.format("%d:%02d %s", nHour == 0 and 12 or nHour, nMinute, sTOD)
|
||||||
|
else
|
||||||
|
return string.format("%d:%02d", nHour, nMinute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makePagedScroll(_term, _nFreeLines)
|
||||||
|
local nativeScroll = _term.scroll
|
||||||
|
local nFreeLines = _nFreeLines or 0
|
||||||
|
return function(_n)
|
||||||
|
for _ = 1, _n do
|
||||||
|
nativeScroll(1)
|
||||||
|
|
||||||
|
if nFreeLines <= 0 then
|
||||||
|
local _, h = _term.getSize()
|
||||||
|
_term.setCursorPos(1, h)
|
||||||
|
_term.write("Press any key to continue")
|
||||||
|
os.pullEvent("key")
|
||||||
|
_term.clearLine()
|
||||||
|
_term.setCursorPos(1, h)
|
||||||
|
else
|
||||||
|
nFreeLines = nFreeLines - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Prints a given string to the display.
|
||||||
|
|
||||||
|
If the action can be completed without scrolling, it acts much the same as
|
||||||
|
@{print}; otherwise, it will throw up a "Press any key to continue" prompt at
|
||||||
|
the bottom of the display. Each press will cause it to scroll down and write a
|
||||||
|
single line more before prompting again, if need be.
|
||||||
|
|
||||||
|
@tparam string text The text to print to the screen.
|
||||||
|
@tparam[opt] number free_lines The number of lines which will be
|
||||||
|
automatically scrolled before the first prompt appears (meaning free_lines +
|
||||||
|
1 lines will be printed). This can be set to the cursor's y position - 2 to
|
||||||
|
always try to fill the screen. Defaults to 0, meaning only one line is
|
||||||
|
displayed before prompting.
|
||||||
|
@treturn number The number of lines printed.
|
||||||
|
|
||||||
|
@usage Generates several lines of text and then prints it, paging once the
|
||||||
|
bottom of the terminal is reached.
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for i = 1, 30 do lines[i] = ("This is line #%d"):format(i) end
|
||||||
|
local message = table.concat(lines, "\n")
|
||||||
|
|
||||||
|
local width, height = term.getCursorPos()
|
||||||
|
textutils.pagedPrint(message, height - 2)
|
||||||
|
]]
|
||||||
|
function pagedPrint(text, free_lines)
|
||||||
|
expect(2, free_lines, "number", "nil")
|
||||||
|
-- Setup a redirector
|
||||||
|
local oldTerm = term.current()
|
||||||
|
local newTerm = {}
|
||||||
|
for k, v in pairs(oldTerm) do
|
||||||
|
newTerm[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
newTerm.scroll = makePagedScroll(oldTerm, free_lines)
|
||||||
|
term.redirect(newTerm)
|
||||||
|
|
||||||
|
-- Print the text
|
||||||
|
local result
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
if text ~= nil then
|
||||||
|
result = print(text)
|
||||||
|
else
|
||||||
|
result = print()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Removed the redirector
|
||||||
|
term.redirect(oldTerm)
|
||||||
|
|
||||||
|
-- Propagate errors
|
||||||
|
if not ok then
|
||||||
|
error(err, 0)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tabulateCommon(bPaged, ...)
|
||||||
|
local tAll = table.pack(...)
|
||||||
|
for i = 1, tAll.n do
|
||||||
|
expect(i, tAll[i], "number", "table")
|
||||||
|
end
|
||||||
|
|
||||||
|
local w, h = term.getSize()
|
||||||
|
local nMaxLen = w / 8
|
||||||
|
for n, t in ipairs(tAll) do
|
||||||
|
if type(t) == "table" then
|
||||||
|
for nu, sItem in pairs(t) do
|
||||||
|
local ty = type(sItem)
|
||||||
|
if ty ~= "string" and ty ~= "number" then
|
||||||
|
error("bad argument #" .. n .. "." .. nu .. " (string expected, got " .. ty .. ")", 3)
|
||||||
|
end
|
||||||
|
nMaxLen = math.max(#tostring(sItem) + 1, nMaxLen)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local nCols = math.floor(w / nMaxLen)
|
||||||
|
local nLines = 0
|
||||||
|
local function newLine()
|
||||||
|
if bPaged and nLines >= h - 3 then
|
||||||
|
pagedPrint()
|
||||||
|
else
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
nLines = nLines + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawCols(_t)
|
||||||
|
local nCol = 1
|
||||||
|
for _, s in ipairs(_t) do
|
||||||
|
if nCol > nCols then
|
||||||
|
nCol = 1
|
||||||
|
newLine()
|
||||||
|
end
|
||||||
|
|
||||||
|
local cx, cy = term.getCursorPos()
|
||||||
|
cx = 1 + (nCol - 1) * nMaxLen
|
||||||
|
term.setCursorPos(cx, cy)
|
||||||
|
term.write(s)
|
||||||
|
|
||||||
|
nCol = nCol + 1
|
||||||
|
end
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
|
||||||
|
local previous_colour = term.getTextColour()
|
||||||
|
for _, t in ipairs(tAll) do
|
||||||
|
if type(t) == "table" then
|
||||||
|
if #t > 0 then
|
||||||
|
drawCols(t)
|
||||||
|
end
|
||||||
|
elseif type(t) == "number" then
|
||||||
|
term.setTextColor(t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
term.setTextColor(previous_colour)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Prints tables in a structured form.
|
||||||
|
|
||||||
|
This accepts multiple arguments, either a table or a number. When
|
||||||
|
encountering a table, this will be treated as a table row, with each column
|
||||||
|
width being auto-adjusted.
|
||||||
|
|
||||||
|
When encountering a number, this sets the text color of the subsequent rows to it.
|
||||||
|
|
||||||
|
@tparam {string...}|number ... The rows and text colors to display.
|
||||||
|
@since 1.3
|
||||||
|
@usage
|
||||||
|
|
||||||
|
textutils.tabulate(
|
||||||
|
colors.orange, { "1", "2", "3" },
|
||||||
|
colors.lightBlue, { "A", "B", "C" }
|
||||||
|
)
|
||||||
|
]]
|
||||||
|
function tabulate(...)
|
||||||
|
return tabulateCommon(false, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Prints tables in a structured form, stopping and prompting for input should
|
||||||
|
the result not fit on the terminal.
|
||||||
|
|
||||||
|
This functions identically to @{textutils.tabulate}, but will prompt for user
|
||||||
|
input should the whole output not fit on the display.
|
||||||
|
|
||||||
|
@tparam {string...}|number ... The rows and text colors to display.
|
||||||
|
@see textutils.tabulate
|
||||||
|
@see textutils.pagedPrint
|
||||||
|
@since 1.3
|
||||||
|
|
||||||
|
@usage Generates a long table, tabulates it, and prints it to the screen.
|
||||||
|
|
||||||
|
local rows = {}
|
||||||
|
for i = 1, 30 do rows[i] = {("Row #%d"):format(i), math.random(1, 400)} end
|
||||||
|
|
||||||
|
textutils.pagedTabulate(colors.orange, {"Column", "Value"}, colors.lightBlue, table.unpack(rows))
|
||||||
|
]]
|
||||||
|
function pagedTabulate(...)
|
||||||
|
return tabulateCommon(true, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
local g_tLuaKeywords = {
|
||||||
|
["and"] = true,
|
||||||
|
["break"] = true,
|
||||||
|
["do"] = true,
|
||||||
|
["else"] = true,
|
||||||
|
["elseif"] = true,
|
||||||
|
["end"] = true,
|
||||||
|
["false"] = true,
|
||||||
|
["for"] = true,
|
||||||
|
["function"] = true,
|
||||||
|
["if"] = true,
|
||||||
|
["in"] = true,
|
||||||
|
["local"] = true,
|
||||||
|
["nil"] = true,
|
||||||
|
["not"] = true,
|
||||||
|
["or"] = true,
|
||||||
|
["repeat"] = true,
|
||||||
|
["return"] = true,
|
||||||
|
["then"] = true,
|
||||||
|
["true"] = true,
|
||||||
|
["until"] = true,
|
||||||
|
["while"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- A version of the ipairs iterator which ignores metamethods
|
||||||
|
local function inext(tbl, i)
|
||||||
|
i = (i or 0) + 1
|
||||||
|
local v = rawget(tbl, i)
|
||||||
|
if v == nil then return nil else return i, v end
|
||||||
|
end
|
||||||
|
|
||||||
|
local serialize_infinity = math.huge
|
||||||
|
local function serialize_impl(t, tracking, indent, opts)
|
||||||
|
local sType = type(t)
|
||||||
|
if sType == "table" then
|
||||||
|
if tracking[t] ~= nil then
|
||||||
|
if tracking[t] == false then
|
||||||
|
error("Cannot serialize table with repeated entries", 0)
|
||||||
|
else
|
||||||
|
error("Cannot serialize table with recursive entries", 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tracking[t] = true
|
||||||
|
|
||||||
|
local result
|
||||||
|
if next(t) == nil then
|
||||||
|
-- Empty tables are simple
|
||||||
|
result = "{}"
|
||||||
|
else
|
||||||
|
-- Other tables take more work
|
||||||
|
local open, sub_indent, open_key, close_key, equal, comma = "{\n", indent .. " ", "[ ", " ] = ", " = ", ",\n"
|
||||||
|
if opts.compact then
|
||||||
|
open, sub_indent, open_key, close_key, equal, comma = "{", "", "[", "]=", "=", ","
|
||||||
|
end
|
||||||
|
|
||||||
|
result = open
|
||||||
|
local seen_keys = {}
|
||||||
|
for k, v in inext, t do
|
||||||
|
seen_keys[k] = true
|
||||||
|
result = result .. sub_indent .. serialize_impl(v, tracking, sub_indent, opts) .. comma
|
||||||
|
end
|
||||||
|
for k, v in next, t do
|
||||||
|
if not seen_keys[k] then
|
||||||
|
local sEntry
|
||||||
|
if type(k) == "string" and not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then
|
||||||
|
sEntry = k .. equal .. serialize_impl(v, tracking, sub_indent, opts) .. comma
|
||||||
|
else
|
||||||
|
sEntry = open_key .. serialize_impl(k, tracking, sub_indent, opts) .. close_key .. serialize_impl(v, tracking, sub_indent, opts) .. comma
|
||||||
|
end
|
||||||
|
result = result .. sub_indent .. sEntry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result = result .. indent .. "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.allow_repetitions then
|
||||||
|
tracking[t] = nil
|
||||||
|
else
|
||||||
|
tracking[t] = false
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
|
||||||
|
elseif sType == "string" then
|
||||||
|
return string.format("%q", t)
|
||||||
|
|
||||||
|
elseif sType == "number" then
|
||||||
|
if t ~= t then --nan
|
||||||
|
return "0/0"
|
||||||
|
elseif t == serialize_infinity then
|
||||||
|
return "1/0"
|
||||||
|
elseif t == -serialize_infinity then
|
||||||
|
return "-1/0"
|
||||||
|
else
|
||||||
|
return tostring(t)
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif sType == "boolean" or sType == "nil" then
|
||||||
|
return tostring(t)
|
||||||
|
|
||||||
|
else
|
||||||
|
error("Cannot serialize type " .. sType, 0)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mk_tbl(str, name)
|
||||||
|
local msg = "attempt to mutate textutils." .. name
|
||||||
|
return setmetatable({}, {
|
||||||
|
__newindex = function() error(msg, 2) end,
|
||||||
|
__tostring = function() return str end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
--- A table representing an empty JSON array, in order to distinguish it from an
|
||||||
|
-- empty JSON object.
|
||||||
|
--
|
||||||
|
-- The contents of this table should not be modified.
|
||||||
|
--
|
||||||
|
-- @usage textutils.serialiseJSON(textutils.empty_json_array)
|
||||||
|
-- @see textutils.serialiseJSON
|
||||||
|
-- @see textutils.unserialiseJSON
|
||||||
|
empty_json_array = mk_tbl("[]", "empty_json_array")
|
||||||
|
|
||||||
|
--- A table representing the JSON null value.
|
||||||
|
--
|
||||||
|
-- The contents of this table should not be modified.
|
||||||
|
--
|
||||||
|
-- @usage textutils.serialiseJSON(textutils.json_null)
|
||||||
|
-- @see textutils.serialiseJSON
|
||||||
|
-- @see textutils.unserialiseJSON
|
||||||
|
json_null = mk_tbl("null", "json_null")
|
||||||
|
|
||||||
|
local serializeJSONString
|
||||||
|
do
|
||||||
|
local function hexify(c)
|
||||||
|
return ("\\u00%02X"):format(c:byte())
|
||||||
|
end
|
||||||
|
|
||||||
|
local map = {
|
||||||
|
["\""] = "\\\"",
|
||||||
|
["\\"] = "\\\\",
|
||||||
|
["\b"] = "\\b",
|
||||||
|
["\f"] = "\\f",
|
||||||
|
["\n"] = "\\n",
|
||||||
|
["\r"] = "\\r",
|
||||||
|
["\t"] = "\\t",
|
||||||
|
}
|
||||||
|
for i = 0, 0x1f do
|
||||||
|
local c = string.char(i)
|
||||||
|
if map[c] == nil then map[c] = hexify(c) end
|
||||||
|
end
|
||||||
|
|
||||||
|
serializeJSONString = function(s)
|
||||||
|
return ('"%s"'):format(s:gsub("[\0-\x1f\"\\]", map):gsub("[\x7f-\xff]", hexify))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function serializeJSONImpl(t, tTracking, bNBTStyle)
|
||||||
|
local sType = type(t)
|
||||||
|
if t == empty_json_array then return "[]"
|
||||||
|
elseif t == json_null then return "null"
|
||||||
|
|
||||||
|
elseif sType == "table" then
|
||||||
|
if tTracking[t] ~= nil then
|
||||||
|
error("Cannot serialize table with recursive entries", 0)
|
||||||
|
end
|
||||||
|
tTracking[t] = true
|
||||||
|
|
||||||
|
if next(t) == nil then
|
||||||
|
-- Empty tables are simple
|
||||||
|
return "{}"
|
||||||
|
else
|
||||||
|
-- Other tables take more work
|
||||||
|
local sObjectResult = "{"
|
||||||
|
local sArrayResult = "["
|
||||||
|
local nObjectSize = 0
|
||||||
|
local nArraySize = 0
|
||||||
|
local largestArrayIndex = 0
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(k) == "string" then
|
||||||
|
local sEntry
|
||||||
|
if bNBTStyle then
|
||||||
|
sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle)
|
||||||
|
else
|
||||||
|
sEntry = serializeJSONString(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle)
|
||||||
|
end
|
||||||
|
if nObjectSize == 0 then
|
||||||
|
sObjectResult = sObjectResult .. sEntry
|
||||||
|
else
|
||||||
|
sObjectResult = sObjectResult .. "," .. sEntry
|
||||||
|
end
|
||||||
|
nObjectSize = nObjectSize + 1
|
||||||
|
elseif type(k) == "number" and k > largestArrayIndex then --the largest index is kept to avoid losing half the array if there is any single nil in that array
|
||||||
|
largestArrayIndex = k
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for k = 1, largestArrayIndex, 1 do --the array is read up to the very last valid array index, ipairs() would stop at the first nil value and we would lose any data after.
|
||||||
|
local sEntry
|
||||||
|
if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones.
|
||||||
|
sEntry = "null"
|
||||||
|
else -- if the array index does not point to a nil we serialise it's content.
|
||||||
|
sEntry = serializeJSONImpl(t[k], tTracking, bNBTStyle)
|
||||||
|
end
|
||||||
|
if nArraySize == 0 then
|
||||||
|
sArrayResult = sArrayResult .. sEntry
|
||||||
|
else
|
||||||
|
sArrayResult = sArrayResult .. "," .. sEntry
|
||||||
|
end
|
||||||
|
nArraySize = nArraySize + 1
|
||||||
|
end
|
||||||
|
sObjectResult = sObjectResult .. "}"
|
||||||
|
sArrayResult = sArrayResult .. "]"
|
||||||
|
if nObjectSize > 0 or nArraySize == 0 then
|
||||||
|
return sObjectResult
|
||||||
|
else
|
||||||
|
return sArrayResult
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif sType == "string" then
|
||||||
|
return serializeJSONString(t)
|
||||||
|
|
||||||
|
elseif sType == "number" or sType == "boolean" then
|
||||||
|
return tostring(t)
|
||||||
|
|
||||||
|
else
|
||||||
|
error("Cannot serialize type " .. sType, 0)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local unserialise_json
|
||||||
|
do
|
||||||
|
local sub, find, match, concat, tonumber = string.sub, string.find, string.match, table.concat, tonumber
|
||||||
|
|
||||||
|
--- Skip any whitespace
|
||||||
|
local function skip(str, pos)
|
||||||
|
local _, last = find(str, "^[ \n\r\t]+", pos)
|
||||||
|
if last then return last + 1 else return pos end
|
||||||
|
end
|
||||||
|
|
||||||
|
local escapes = {
|
||||||
|
["b"] = '\b', ["f"] = '\f', ["n"] = '\n', ["r"] = '\r', ["t"] = '\t',
|
||||||
|
["\""] = "\"", ["/"] = "/", ["\\"] = "\\",
|
||||||
|
}
|
||||||
|
|
||||||
|
local mt = {}
|
||||||
|
|
||||||
|
local function error_at(pos, msg, ...)
|
||||||
|
if select('#', ...) > 0 then msg = msg:format(...) end
|
||||||
|
error(setmetatable({ pos = pos, msg = msg }, mt))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function expected(pos, actual, exp)
|
||||||
|
if actual == "" then actual = "end of input" else actual = ("%q"):format(actual) end
|
||||||
|
error_at(pos, "Unexpected %s, expected %s.", actual, exp)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_string(str, pos, terminate)
|
||||||
|
local buf, n = {}, 1
|
||||||
|
|
||||||
|
-- We attempt to match all non-special characters at once using Lua patterns, as this
|
||||||
|
-- provides a significant speed boost. This is all characters >= " " except \ and the
|
||||||
|
-- terminator (' or ").
|
||||||
|
local char_pat = "^[ !#-[%]^-\255]+"
|
||||||
|
if terminate == "'" then char_pat = "^[ -&(-[%]^-\255]+" end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local c = sub(str, pos, pos)
|
||||||
|
if c == "" then error_at(pos, "Unexpected end of input, expected '\"'.") end
|
||||||
|
if c == terminate then break end
|
||||||
|
|
||||||
|
if c == "\\" then
|
||||||
|
-- Handle the various escapes
|
||||||
|
c = sub(str, pos + 1, pos + 1)
|
||||||
|
if c == "" then error_at(pos, "Unexpected end of input, expected escape sequence.") end
|
||||||
|
|
||||||
|
if c == "u" then
|
||||||
|
local num_str = match(str, "^%x%x%x%x", pos + 2)
|
||||||
|
if not num_str then error_at(pos, "Malformed unicode escape %q.", sub(str, pos + 2, pos + 5)) end
|
||||||
|
buf[n], n, pos = utf8.char(tonumber(num_str, 16)), n + 1, pos + 6
|
||||||
|
else
|
||||||
|
local unesc = escapes[c]
|
||||||
|
if not unesc then error_at(pos + 1, "Unknown escape character %q.", c) end
|
||||||
|
buf[n], n, pos = unesc, n + 1, pos + 2
|
||||||
|
end
|
||||||
|
elseif c >= " " then
|
||||||
|
local _, finish = find(str, char_pat, pos)
|
||||||
|
buf[n], n = sub(str, pos, finish), n + 1
|
||||||
|
pos = finish + 1
|
||||||
|
else
|
||||||
|
error_at(pos + 1, "Unescaped whitespace %q.", c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return concat(buf, "", 1, n - 1), pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local num_types = { b = true, B = true, s = true, S = true, l = true, L = true, f = true, F = true, d = true, D = true }
|
||||||
|
local function parse_number(str, pos, opts)
|
||||||
|
local _, last, num_str = find(str, '^(-?%d+%.?%d*[eE]?[+-]?%d*)', pos)
|
||||||
|
local val = tonumber(num_str)
|
||||||
|
if not val then error_at(pos, "Malformed number %q.", num_str) end
|
||||||
|
|
||||||
|
if opts.nbt_style and num_types[sub(str, last + 1, last + 1)] then return val, last + 2 end
|
||||||
|
|
||||||
|
return val, last + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_ident(str, pos)
|
||||||
|
local _, last, val = find(str, '^([%a][%w_]*)', pos)
|
||||||
|
return val, last + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local arr_types = { I = true, L = true, B = true }
|
||||||
|
local function decode_impl(str, pos, opts)
|
||||||
|
local c = sub(str, pos, pos)
|
||||||
|
if c == '"' then return parse_string(str, pos + 1, '"')
|
||||||
|
elseif c == "'" and opts.nbt_style then return parse_string(str, pos + 1, "\'")
|
||||||
|
elseif c == "-" or c >= "0" and c <= "9" then return parse_number(str, pos, opts)
|
||||||
|
elseif c == "t" then
|
||||||
|
if sub(str, pos + 1, pos + 3) == "rue" then return true, pos + 4 end
|
||||||
|
elseif c == 'f' then
|
||||||
|
if sub(str, pos + 1, pos + 4) == "alse" then return false, pos + 5 end
|
||||||
|
elseif c == 'n' then
|
||||||
|
if sub(str, pos + 1, pos + 3) == "ull" then
|
||||||
|
if opts.parse_null then
|
||||||
|
return json_null, pos + 4
|
||||||
|
else
|
||||||
|
return nil, pos + 4
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif c == "{" then
|
||||||
|
local obj = {}
|
||||||
|
|
||||||
|
pos = skip(str, pos + 1)
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
|
||||||
|
if c == "" then return error_at(pos, "Unexpected end of input, expected '}'.") end
|
||||||
|
if c == "}" then return obj, pos + 1 end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local key, value
|
||||||
|
if c == "\"" then key, pos = parse_string(str, pos + 1, "\"")
|
||||||
|
elseif opts.nbt_style then key, pos = parse_ident(str, pos)
|
||||||
|
else return expected(pos, c, "object key")
|
||||||
|
end
|
||||||
|
|
||||||
|
pos = skip(str, pos)
|
||||||
|
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
if c ~= ":" then return expected(pos, c, "':'") end
|
||||||
|
|
||||||
|
value, pos = decode_impl(str, skip(str, pos + 1), opts)
|
||||||
|
obj[key] = value
|
||||||
|
|
||||||
|
-- Consume the next delimiter
|
||||||
|
pos = skip(str, pos)
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
if c == "}" then break
|
||||||
|
elseif c == "," then pos = skip(str, pos + 1)
|
||||||
|
else return expected(pos, c, "',' or '}'")
|
||||||
|
end
|
||||||
|
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
end
|
||||||
|
|
||||||
|
return obj, pos + 1
|
||||||
|
|
||||||
|
elseif c == "[" then
|
||||||
|
local arr, n = {}, 1
|
||||||
|
|
||||||
|
pos = skip(str, pos + 1)
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
|
||||||
|
if arr_types[c] and sub(str, pos + 1, pos + 1) == ";" and opts.nbt_style then
|
||||||
|
pos = skip(str, pos + 2)
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
end
|
||||||
|
|
||||||
|
if c == "" then return expected(pos, c, "']'") end
|
||||||
|
if c == "]" then
|
||||||
|
if opts.parse_empty_array ~= false then
|
||||||
|
return empty_json_array, pos + 1
|
||||||
|
else
|
||||||
|
return {}, pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
n, arr[n], pos = n + 1, decode_impl(str, pos, opts)
|
||||||
|
|
||||||
|
-- Consume the next delimiter
|
||||||
|
pos = skip(str, pos)
|
||||||
|
c = sub(str, pos, pos)
|
||||||
|
if c == "]" then break
|
||||||
|
elseif c == "," then pos = skip(str, pos + 1)
|
||||||
|
else return expected(pos, c, "',' or ']'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return arr, pos + 1
|
||||||
|
elseif c == "" then error_at(pos, 'Unexpected end of input.')
|
||||||
|
end
|
||||||
|
|
||||||
|
error_at(pos, "Unexpected character %q.", c)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Converts a serialised JSON string back into a reassembled Lua object.
|
||||||
|
|
||||||
|
This may be used with @{textutils.serializeJSON}, or when communicating
|
||||||
|
with command blocks or web APIs.
|
||||||
|
|
||||||
|
If a `null` value is encountered, it is converted into `nil`. It can be converted
|
||||||
|
into @{textutils.json_null} with the `parse_null` option.
|
||||||
|
|
||||||
|
If an empty array is encountered, it is converted into @{textutils.empty_json_array}.
|
||||||
|
It can be converted into a new empty table with the `parse_empty_array` option.
|
||||||
|
|
||||||
|
@tparam string s The serialised string to deserialise.
|
||||||
|
@tparam[opt] { nbt_style? = boolean, parse_null? = boolean, parse_empty_array? = boolean } options
|
||||||
|
Options which control how this JSON object is parsed.
|
||||||
|
|
||||||
|
- `nbt_style`: When true, this will accept [stringified NBT][nbt] strings,
|
||||||
|
as produced by many commands.
|
||||||
|
- `parse_null`: When true, `null` will be parsed as @{json_null}, rather than
|
||||||
|
`nil`.
|
||||||
|
- `parse_empty_array`: When false, empty arrays will be parsed as a new table.
|
||||||
|
By default (or when this value is true), they are parsed as @{empty_json_array}.
|
||||||
|
|
||||||
|
[nbt]: https://minecraft.gamepedia.com/NBT_format
|
||||||
|
@return[1] The deserialised object
|
||||||
|
@treturn[2] nil If the object could not be deserialised.
|
||||||
|
@treturn string A message describing why the JSON string is invalid.
|
||||||
|
@since 1.87.0
|
||||||
|
@changed 1.100.6 Added `parse_empty_array` option
|
||||||
|
@see textutils.json_null Use to serialize a JSON `null` value.
|
||||||
|
@see textutils.empty_json_array Use to serialize a JSON empty array.
|
||||||
|
@usage Unserialise a basic JSON object
|
||||||
|
|
||||||
|
textutils.unserialiseJSON('{"name": "Steve", "age": null}')
|
||||||
|
|
||||||
|
@usage Unserialise a basic JSON object, returning null values as @{json_null}.
|
||||||
|
|
||||||
|
textutils.unserialiseJSON('{"name": "Steve", "age": null}', { parse_null = true })
|
||||||
|
]]
|
||||||
|
unserialise_json = function(s, options)
|
||||||
|
expect(1, s, "string")
|
||||||
|
expect(2, options, "table", "nil")
|
||||||
|
|
||||||
|
if options then
|
||||||
|
field(options, "nbt_style", "boolean", "nil")
|
||||||
|
field(options, "parse_null", "boolean", "nil")
|
||||||
|
field(options, "parse_empty_array", "boolean", "nil")
|
||||||
|
else
|
||||||
|
options = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, res, pos = pcall(decode_impl, s, skip(s, 1), options)
|
||||||
|
if not ok then
|
||||||
|
if type(res) == "table" and getmetatable(res) == mt then
|
||||||
|
return nil, ("Malformed JSON at position %d: %s"):format(res.pos, res.msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
error(res, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
pos = skip(s, pos)
|
||||||
|
if pos <= #s then
|
||||||
|
return nil, ("Malformed JSON at position %d: Unexpected trailing character %q."):format(pos, sub(s, pos, pos))
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[- Convert a Lua object into a textual representation, suitable for
|
||||||
|
saving in a file or pretty-printing.
|
||||||
|
|
||||||
|
@param t The object to serialise
|
||||||
|
@tparam { compact? = boolean, allow_repetitions? = boolean } opts Options for serialisation.
|
||||||
|
- `compact`: Do not emit indentation and other whitespace between terms.
|
||||||
|
- `allow_repetitions`: Relax the check for recursive tables, allowing them to appear multiple
|
||||||
|
times (as long as tables do not appear inside themselves).
|
||||||
|
|
||||||
|
@treturn string The serialised representation
|
||||||
|
@throws If the object contains a value which cannot be
|
||||||
|
serialised. This includes functions and tables which appear multiple
|
||||||
|
times.
|
||||||
|
@see cc.pretty.pretty_print An alternative way to display a table, often more
|
||||||
|
suitable for pretty printing.
|
||||||
|
@since 1.3
|
||||||
|
@changed 1.97.0 Added `opts` argument.
|
||||||
|
@usage Serialise a basic table.
|
||||||
|
|
||||||
|
textutils.serialise({ 1, 2, 3, a = 1, ["another key"] = { true } })
|
||||||
|
|
||||||
|
@usage Demonstrates some of the other options
|
||||||
|
|
||||||
|
local tbl = { 1, 2, 3 }
|
||||||
|
print(textutils.serialise({ tbl, tbl }, { allow_repetitions = true }))
|
||||||
|
|
||||||
|
print(textutils.serialise(tbl, { compact = true }))
|
||||||
|
]]
|
||||||
|
function serialize(t, opts)
|
||||||
|
local tTracking = {}
|
||||||
|
expect(2, opts, "table", "nil")
|
||||||
|
|
||||||
|
if opts then
|
||||||
|
field(opts, "compact", "boolean", "nil")
|
||||||
|
field(opts, "allow_repetitions", "boolean", "nil")
|
||||||
|
else
|
||||||
|
opts = {}
|
||||||
|
end
|
||||||
|
return serialize_impl(t, tTracking, "", opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
serialise = serialize -- GB version
|
||||||
|
|
||||||
|
--- Converts a serialised string back into a reassembled Lua object.
|
||||||
|
--
|
||||||
|
-- This is mainly used together with @{textutils.serialise}.
|
||||||
|
--
|
||||||
|
-- @tparam string s The serialised string to deserialise.
|
||||||
|
-- @return[1] The deserialised object
|
||||||
|
-- @treturn[2] nil If the object could not be deserialised.
|
||||||
|
-- @since 1.3
|
||||||
|
function unserialize(s)
|
||||||
|
expect(1, s, "string")
|
||||||
|
local func = load("return " .. s, "unserialize", "t", {})
|
||||||
|
if func then
|
||||||
|
local ok, result = pcall(func)
|
||||||
|
if ok then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
unserialise = unserialize -- GB version
|
||||||
|
|
||||||
|
--- Returns a JSON representation of the given data.
|
||||||
|
--
|
||||||
|
-- This function attempts to guess whether a table is a JSON array or
|
||||||
|
-- object. However, empty tables are assumed to be empty objects - use
|
||||||
|
-- @{textutils.empty_json_array} to mark an empty array.
|
||||||
|
--
|
||||||
|
-- This is largely intended for interacting with various functions from the
|
||||||
|
-- @{commands} API, though may also be used in making @{http} requests.
|
||||||
|
--
|
||||||
|
-- @param t The value to serialise. Like @{textutils.serialise}, this should not
|
||||||
|
-- contain recursive tables or functions.
|
||||||
|
-- @tparam[opt] boolean bNBTStyle Whether to produce NBT-style JSON (non-quoted keys)
|
||||||
|
-- instead of standard JSON.
|
||||||
|
-- @treturn string The JSON representation of the input.
|
||||||
|
-- @throws If the object contains a value which cannot be
|
||||||
|
-- serialised. This includes functions and tables which appear multiple
|
||||||
|
-- times.
|
||||||
|
-- @usage textutils.serialiseJSON({ values = { 1, "2", true } })
|
||||||
|
-- @since 1.7
|
||||||
|
-- @see textutils.json_null Use to serialise a JSON `null` value.
|
||||||
|
-- @see textutils.empty_json_array Use to serialise a JSON empty array.
|
||||||
|
function serializeJSON(t, bNBTStyle)
|
||||||
|
expect(1, t, "table", "string", "number", "boolean")
|
||||||
|
expect(2, bNBTStyle, "boolean", "nil")
|
||||||
|
local tTracking = {}
|
||||||
|
return serializeJSONImpl(t, tTracking, bNBTStyle or false)
|
||||||
|
end
|
||||||
|
|
||||||
|
serialiseJSON = serializeJSON -- GB version
|
||||||
|
|
||||||
|
unserializeJSON = unserialise_json
|
||||||
|
unserialiseJSON = unserialise_json
|
||||||
|
|
||||||
|
--- Replaces certain characters in a string to make it safe for use in URLs or POST data.
|
||||||
|
--
|
||||||
|
-- @tparam string str The string to encode
|
||||||
|
-- @treturn string The encoded string.
|
||||||
|
-- @usage print("https://example.com/?view=" .. textutils.urlEncode("some text&things"))
|
||||||
|
-- @since 1.31
|
||||||
|
function urlEncode(str)
|
||||||
|
expect(1, str, "string")
|
||||||
|
if str then
|
||||||
|
str = string.gsub(str, "\n", "\r\n")
|
||||||
|
str = string.gsub(str, "([^A-Za-z0-9 %-%_%.])", function(c)
|
||||||
|
local n = string.byte(c)
|
||||||
|
if n < 128 then
|
||||||
|
-- ASCII
|
||||||
|
return string.format("%%%02X", n)
|
||||||
|
else
|
||||||
|
-- Non-ASCII (encode as UTF-8)
|
||||||
|
return
|
||||||
|
string.format("%%%02X", 192 + bit32.band(bit32.arshift(n, 6), 31)) ..
|
||||||
|
string.format("%%%02X", 128 + bit32.band(n, 63))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
str = string.gsub(str, " ", "+")
|
||||||
|
end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
local tEmpty = {}
|
||||||
|
|
||||||
|
--- Provides a list of possible completions for a partial Lua expression.
|
||||||
|
--
|
||||||
|
-- If the completed element is a table, suggestions will have `.` appended to
|
||||||
|
-- them. Similarly, functions have `(` appended to them.
|
||||||
|
--
|
||||||
|
-- @tparam string sSearchText The partial expression to complete, such as a
|
||||||
|
-- variable name or table index.
|
||||||
|
--
|
||||||
|
-- @tparam[opt] table tSearchTable The table to find variables in, defaulting to
|
||||||
|
-- the global environment (@{_G}). The function also searches the "parent"
|
||||||
|
-- environment via the `__index` metatable field.
|
||||||
|
--
|
||||||
|
-- @treturn { string... } The (possibly empty) list of completions.
|
||||||
|
-- @see shell.setCompletionFunction
|
||||||
|
-- @see _G.read
|
||||||
|
-- @usage textutils.complete( "pa", _ENV )
|
||||||
|
-- @since 1.74
|
||||||
|
function complete(sSearchText, tSearchTable)
|
||||||
|
expect(1, sSearchText, "string")
|
||||||
|
expect(2, tSearchTable, "table", "nil")
|
||||||
|
|
||||||
|
if g_tLuaKeywords[sSearchText] then return tEmpty end
|
||||||
|
local nStart = 1
|
||||||
|
local nDot = string.find(sSearchText, ".", nStart, true)
|
||||||
|
local tTable = tSearchTable or _ENV
|
||||||
|
while nDot do
|
||||||
|
local sPart = string.sub(sSearchText, nStart, nDot - 1)
|
||||||
|
local value = tTable[sPart]
|
||||||
|
if type(value) == "table" then
|
||||||
|
tTable = value
|
||||||
|
nStart = nDot + 1
|
||||||
|
nDot = string.find(sSearchText, ".", nStart, true)
|
||||||
|
else
|
||||||
|
return tEmpty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local nColon = string.find(sSearchText, ":", nStart, true)
|
||||||
|
if nColon then
|
||||||
|
local sPart = string.sub(sSearchText, nStart, nColon - 1)
|
||||||
|
local value = tTable[sPart]
|
||||||
|
if type(value) == "table" then
|
||||||
|
tTable = value
|
||||||
|
nStart = nColon + 1
|
||||||
|
else
|
||||||
|
return tEmpty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local sPart = string.sub(sSearchText, nStart)
|
||||||
|
local nPartLength = #sPart
|
||||||
|
|
||||||
|
local tResults = {}
|
||||||
|
local tSeen = {}
|
||||||
|
while tTable do
|
||||||
|
for k, v in pairs(tTable) do
|
||||||
|
if not tSeen[k] and type(k) == "string" then
|
||||||
|
if string.find(k, sPart, 1, true) == 1 then
|
||||||
|
if not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then
|
||||||
|
local sResult = string.sub(k, nPartLength + 1)
|
||||||
|
if nColon then
|
||||||
|
if type(v) == "function" then
|
||||||
|
table.insert(tResults, sResult .. "(")
|
||||||
|
elseif type(v) == "table" then
|
||||||
|
local tMetatable = getmetatable(v)
|
||||||
|
if tMetatable and (type(tMetatable.__call) == "function" or type(tMetatable.__call) == "table") then
|
||||||
|
table.insert(tResults, sResult .. "(")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if type(v) == "function" then
|
||||||
|
sResult = sResult .. "("
|
||||||
|
elseif type(v) == "table" and next(v) ~= nil then
|
||||||
|
sResult = sResult .. "."
|
||||||
|
end
|
||||||
|
table.insert(tResults, sResult)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tSeen[k] = true
|
||||||
|
end
|
||||||
|
local tMetatable = getmetatable(tTable)
|
||||||
|
if tMetatable and type(tMetatable.__index) == "table" then
|
||||||
|
tTable = tMetatable.__index
|
||||||
|
else
|
||||||
|
tTable = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(tResults)
|
||||||
|
return tResults
|
||||||
|
end
|
@ -0,0 +1,44 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- @module turtle
|
||||||
|
|
||||||
|
if not turtle then
|
||||||
|
error("Cannot load turtle API on computer", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- The builtin turtle API, without any generated helper functions.
|
||||||
|
--
|
||||||
|
-- @deprecated Historically this table behaved differently to the main turtle API, but this is no longer the case. You
|
||||||
|
-- should not need to use it.
|
||||||
|
native = turtle.native or turtle
|
||||||
|
|
||||||
|
local function addCraftMethod(object)
|
||||||
|
if peripheral.getType("left") == "workbench" then
|
||||||
|
object.craft = function(...)
|
||||||
|
return peripheral.call("left", "craft", ...)
|
||||||
|
end
|
||||||
|
elseif peripheral.getType("right") == "workbench" then
|
||||||
|
object.craft = function(...)
|
||||||
|
return peripheral.call("right", "craft", ...)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
object.craft = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put commands into environment table
|
||||||
|
local env = _ENV
|
||||||
|
for k, v in pairs(native) do
|
||||||
|
if k == "equipLeft" or k == "equipRight" then
|
||||||
|
env[k] = function(...)
|
||||||
|
local result, err = v(...)
|
||||||
|
addCraftMethod(turtle)
|
||||||
|
return result, err
|
||||||
|
end
|
||||||
|
else
|
||||||
|
env[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
addCraftMethod(env)
|
195
src/main/resources/assets/cctweaked/lua/rom/apis/vector.lua
Normal file
195
src/main/resources/assets/cctweaked/lua/rom/apis/vector.lua
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- A basic 3D vector type and some common vector operations. This may be useful
|
||||||
|
-- when working with coordinates in Minecraft's world (such as those from the
|
||||||
|
-- @{gps} API).
|
||||||
|
--
|
||||||
|
-- An introduction to vectors can be found on [Wikipedia][wiki].
|
||||||
|
--
|
||||||
|
-- [wiki]: http://en.wikipedia.org/wiki/Euclidean_vector
|
||||||
|
--
|
||||||
|
-- @module vector
|
||||||
|
-- @since 1.31
|
||||||
|
|
||||||
|
--- A 3-dimensional vector, with `x`, `y`, and `z` values.
|
||||||
|
--
|
||||||
|
-- This is suitable for representing both position and directional vectors.
|
||||||
|
--
|
||||||
|
-- @type Vector
|
||||||
|
local vector = {
|
||||||
|
--- Adds two vectors together.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The first vector to add.
|
||||||
|
-- @tparam Vector o The second vector to add.
|
||||||
|
-- @treturn Vector The resulting vector
|
||||||
|
-- @usage v1:add(v2)
|
||||||
|
-- @usage v1 + v2
|
||||||
|
add = function(self, o)
|
||||||
|
return vector.new(
|
||||||
|
self.x + o.x,
|
||||||
|
self.y + o.y,
|
||||||
|
self.z + o.z
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Subtracts one vector from another.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The vector to subtract from.
|
||||||
|
-- @tparam Vector o The vector to subtract.
|
||||||
|
-- @treturn Vector The resulting vector
|
||||||
|
-- @usage v1:sub(v2)
|
||||||
|
-- @usage v1 - v2
|
||||||
|
sub = function(self, o)
|
||||||
|
return vector.new(
|
||||||
|
self.x - o.x,
|
||||||
|
self.y - o.y,
|
||||||
|
self.z - o.z
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Multiplies a vector by a scalar value.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The vector to multiply.
|
||||||
|
-- @tparam number m The scalar value to multiply with.
|
||||||
|
-- @treturn Vector A vector with value `(x * m, y * m, z * m)`.
|
||||||
|
-- @usage v:mul(3)
|
||||||
|
-- @usage v * 3
|
||||||
|
mul = function(self, m)
|
||||||
|
return vector.new(
|
||||||
|
self.x * m,
|
||||||
|
self.y * m,
|
||||||
|
self.z * m
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Divides a vector by a scalar value.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The vector to divide.
|
||||||
|
-- @tparam number m The scalar value to divide by.
|
||||||
|
-- @treturn Vector A vector with value `(x / m, y / m, z / m)`.
|
||||||
|
-- @usage v:div(3)
|
||||||
|
-- @usage v / 3
|
||||||
|
div = function(self, m)
|
||||||
|
return vector.new(
|
||||||
|
self.x / m,
|
||||||
|
self.y / m,
|
||||||
|
self.z / m
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Negate a vector
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The vector to negate.
|
||||||
|
-- @treturn Vector The negated vector.
|
||||||
|
-- @usage -v
|
||||||
|
unm = function(self)
|
||||||
|
return vector.new(
|
||||||
|
-self.x,
|
||||||
|
-self.y,
|
||||||
|
-self.z
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Compute the dot product of two vectors
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The first vector to compute the dot product of.
|
||||||
|
-- @tparam Vector o The second vector to compute the dot product of.
|
||||||
|
-- @treturn Vector The dot product of `self` and `o`.
|
||||||
|
-- @usage v1:dot(v2)
|
||||||
|
dot = function(self, o)
|
||||||
|
return self.x * o.x + self.y * o.y + self.z * o.z
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Compute the cross product of two vectors
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The first vector to compute the cross product of.
|
||||||
|
-- @tparam Vector o The second vector to compute the cross product of.
|
||||||
|
-- @treturn Vector The cross product of `self` and `o`.
|
||||||
|
-- @usage v1:cross(v2)
|
||||||
|
cross = function(self, o)
|
||||||
|
return vector.new(
|
||||||
|
self.y * o.z - self.z * o.y,
|
||||||
|
self.z * o.x - self.x * o.z,
|
||||||
|
self.x * o.y - self.y * o.x
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Get the length (also referred to as magnitude) of this vector.
|
||||||
|
-- @tparam Vector self This vector.
|
||||||
|
-- @treturn number The length of this vector.
|
||||||
|
length = function(self)
|
||||||
|
return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Divide this vector by its length, producing with the same direction, but
|
||||||
|
-- of length 1.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The vector to normalise
|
||||||
|
-- @treturn Vector The normalised vector
|
||||||
|
-- @usage v:normalize()
|
||||||
|
normalize = function(self)
|
||||||
|
return self:mul(1 / self:length())
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Construct a vector with each dimension rounded to the nearest value.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The vector to round
|
||||||
|
-- @tparam[opt] number tolerance The tolerance that we should round to,
|
||||||
|
-- defaulting to 1. For instance, a tolerance of 0.5 will round to the
|
||||||
|
-- nearest 0.5.
|
||||||
|
-- @treturn Vector The rounded vector.
|
||||||
|
round = function(self, tolerance)
|
||||||
|
tolerance = tolerance or 1.0
|
||||||
|
return vector.new(
|
||||||
|
math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance,
|
||||||
|
math.floor((self.y + tolerance * 0.5) / tolerance) * tolerance,
|
||||||
|
math.floor((self.z + tolerance * 0.5) / tolerance) * tolerance
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Convert this vector into a string, for pretty printing.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self This vector.
|
||||||
|
-- @treturn string This vector's string representation.
|
||||||
|
-- @usage v:tostring()
|
||||||
|
-- @usage tostring(v)
|
||||||
|
tostring = function(self)
|
||||||
|
return self.x .. "," .. self.y .. "," .. self.z
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Check for equality between two vectors.
|
||||||
|
--
|
||||||
|
-- @tparam Vector self The first vector to compare.
|
||||||
|
-- @tparam Vector other The second vector to compare to.
|
||||||
|
-- @treturn boolean Whether or not the vectors are equal.
|
||||||
|
equals = function(self, other)
|
||||||
|
return self.x == other.x and self.y == other.y and self.z == other.z
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local vmetatable = {
|
||||||
|
__index = vector,
|
||||||
|
__add = vector.add,
|
||||||
|
__sub = vector.sub,
|
||||||
|
__mul = vector.mul,
|
||||||
|
__div = vector.div,
|
||||||
|
__unm = vector.unm,
|
||||||
|
__tostring = vector.tostring,
|
||||||
|
__eq = vector.equals,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Construct a new @{Vector} with the given coordinates.
|
||||||
|
--
|
||||||
|
-- @tparam number x The X coordinate or direction of the vector.
|
||||||
|
-- @tparam number y The Y coordinate or direction of the vector.
|
||||||
|
-- @tparam number z The Z coordinate or direction of the vector.
|
||||||
|
-- @treturn Vector The constructed vector.
|
||||||
|
function new(x, y, z)
|
||||||
|
return setmetatable({
|
||||||
|
x = tonumber(x) or 0,
|
||||||
|
y = tonumber(y) or 0,
|
||||||
|
z = tonumber(z) or 0,
|
||||||
|
}, vmetatable)
|
||||||
|
end
|
614
src/main/resources/assets/cctweaked/lua/rom/apis/window.lua
Normal file
614
src/main/resources/assets/cctweaked/lua/rom/apis/window.lua
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--[[- A @{term.Redirect|terminal redirect} occupying a smaller area of an
|
||||||
|
existing terminal. This allows for easy definition of spaces within the display
|
||||||
|
that can be written/drawn to, then later redrawn/repositioned/etc as need
|
||||||
|
be. The API itself contains only one function, @{window.create}, which returns
|
||||||
|
the windows themselves.
|
||||||
|
|
||||||
|
Windows are considered terminal objects - as such, they have access to nearly
|
||||||
|
all the commands in the term API (plus a few extras of their own, listed within
|
||||||
|
said API) and are valid targets to redirect to.
|
||||||
|
|
||||||
|
Each window has a "parent" terminal object, which can be the computer's own
|
||||||
|
display, a monitor, another window or even other, user-defined terminal
|
||||||
|
objects. Whenever a window is rendered to, the actual screen-writing is
|
||||||
|
performed via that parent (or, if that has one too, then that parent, and so
|
||||||
|
forth). Bear in mind that the cursor of a window's parent will hence be moved
|
||||||
|
around etc when writing a given child window.
|
||||||
|
|
||||||
|
Windows retain a memory of everything rendered "through" them (hence acting as
|
||||||
|
display buffers), and if the parent's display is wiped, the window's content can
|
||||||
|
be easily redrawn later. A window may also be flagged as invisible, preventing
|
||||||
|
any changes to it from being rendered until it's flagged as visible once more.
|
||||||
|
|
||||||
|
A parent terminal object may have multiple children assigned to it, and windows
|
||||||
|
may overlap. For example, the Multishell system functions by assigning each tab
|
||||||
|
a window covering the screen, each using the starting terminal display as its
|
||||||
|
parent, and only one of which is visible at a time.
|
||||||
|
|
||||||
|
@module window
|
||||||
|
@since 1.6
|
||||||
|
]]
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
local tHex = {
|
||||||
|
[colors.white] = "0",
|
||||||
|
[colors.orange] = "1",
|
||||||
|
[colors.magenta] = "2",
|
||||||
|
[colors.lightBlue] = "3",
|
||||||
|
[colors.yellow] = "4",
|
||||||
|
[colors.lime] = "5",
|
||||||
|
[colors.pink] = "6",
|
||||||
|
[colors.gray] = "7",
|
||||||
|
[colors.lightGray] = "8",
|
||||||
|
[colors.cyan] = "9",
|
||||||
|
[colors.purple] = "a",
|
||||||
|
[colors.blue] = "b",
|
||||||
|
[colors.brown] = "c",
|
||||||
|
[colors.green] = "d",
|
||||||
|
[colors.red] = "e",
|
||||||
|
[colors.black] = "f",
|
||||||
|
}
|
||||||
|
|
||||||
|
local type = type
|
||||||
|
local string_rep = string.rep
|
||||||
|
local string_sub = string.sub
|
||||||
|
|
||||||
|
--[[- Returns a terminal object that is a space within the specified parent
|
||||||
|
terminal object. This can then be used (or even redirected to) in the same
|
||||||
|
manner as eg a wrapped monitor. Refer to @{term|the term API} for a list of
|
||||||
|
functions available to it.
|
||||||
|
|
||||||
|
@{term} itself may not be passed as the parent, though @{term.native} is
|
||||||
|
acceptable. Generally, @{term.current} or a wrapped monitor will be most
|
||||||
|
suitable, though windows may even have other windows assigned as their
|
||||||
|
parents.
|
||||||
|
|
||||||
|
@tparam term.Redirect parent The parent terminal redirect to draw to.
|
||||||
|
@tparam number nX The x coordinate this window is drawn at in the parent terminal
|
||||||
|
@tparam number nY The y coordinate this window is drawn at in the parent terminal
|
||||||
|
@tparam number nWidth The width of this window
|
||||||
|
@tparam number nHeight The height of this window
|
||||||
|
@tparam[opt] boolean bStartVisible Whether this window is visible by
|
||||||
|
default. Defaults to `true`.
|
||||||
|
@treturn Window The constructed window
|
||||||
|
@since 1.6
|
||||||
|
@usage Create a smaller window, fill it red and write some text to it.
|
||||||
|
|
||||||
|
local my_window = window.create(term.current(), 1, 1, 20, 5)
|
||||||
|
my_window.setBackgroundColour(colours.red)
|
||||||
|
my_window.setTextColour(colours.white)
|
||||||
|
my_window.clear()
|
||||||
|
my_window.write("Testing my window!")
|
||||||
|
|
||||||
|
@usage Create a smaller window and redirect to it.
|
||||||
|
|
||||||
|
local my_window = window.create(term.current(), 1, 1, 25, 5)
|
||||||
|
term.redirect(my_window)
|
||||||
|
print("Writing some long text which will wrap around and show the bounds of this window.")
|
||||||
|
|
||||||
|
]]
|
||||||
|
function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
|
||||||
|
expect(1, parent, "table")
|
||||||
|
expect(2, nX, "number")
|
||||||
|
expect(3, nY, "number")
|
||||||
|
expect(4, nWidth, "number")
|
||||||
|
expect(5, nHeight, "number")
|
||||||
|
expect(6, bStartVisible, "boolean", "nil")
|
||||||
|
|
||||||
|
if parent == term then
|
||||||
|
error("term is not a recommended window parent, try term.current() instead", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local sEmptySpaceLine
|
||||||
|
local tEmptyColorLines = {}
|
||||||
|
local function createEmptyLines(nWidth)
|
||||||
|
sEmptySpaceLine = string_rep(" ", nWidth)
|
||||||
|
for n = 0, 15 do
|
||||||
|
local nColor = 2 ^ n
|
||||||
|
local sHex = tHex[nColor]
|
||||||
|
tEmptyColorLines[nColor] = string_rep(sHex, nWidth)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
createEmptyLines(nWidth)
|
||||||
|
|
||||||
|
-- Setup
|
||||||
|
local bVisible = bStartVisible ~= false
|
||||||
|
local nCursorX = 1
|
||||||
|
local nCursorY = 1
|
||||||
|
local bCursorBlink = false
|
||||||
|
local nTextColor = colors.white
|
||||||
|
local nBackgroundColor = colors.black
|
||||||
|
local tLines = {}
|
||||||
|
local tPalette = {}
|
||||||
|
do
|
||||||
|
local sEmptyText = sEmptySpaceLine
|
||||||
|
local sEmptyTextColor = tEmptyColorLines[nTextColor]
|
||||||
|
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
|
||||||
|
for y = 1, nHeight do
|
||||||
|
tLines[y] = {
|
||||||
|
text = sEmptyText,
|
||||||
|
textColor = sEmptyTextColor,
|
||||||
|
backgroundColor = sEmptyBackgroundColor,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 0, 15 do
|
||||||
|
local c = 2 ^ i
|
||||||
|
tPalette[c] = { parent.getPaletteColour(c) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper functions
|
||||||
|
local function updateCursorPos()
|
||||||
|
if nCursorX >= 1 and nCursorY >= 1 and
|
||||||
|
nCursorX <= nWidth and nCursorY <= nHeight then
|
||||||
|
parent.setCursorPos(nX + nCursorX - 1, nY + nCursorY - 1)
|
||||||
|
else
|
||||||
|
parent.setCursorPos(0, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateCursorBlink()
|
||||||
|
parent.setCursorBlink(bCursorBlink)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateCursorColor()
|
||||||
|
parent.setTextColor(nTextColor)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redrawLine(n)
|
||||||
|
local tLine = tLines[n]
|
||||||
|
parent.setCursorPos(nX, nY + n - 1)
|
||||||
|
parent.blit(tLine.text, tLine.textColor, tLine.backgroundColor)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redraw()
|
||||||
|
for n = 1, nHeight do
|
||||||
|
redrawLine(n)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updatePalette()
|
||||||
|
for k, v in pairs(tPalette) do
|
||||||
|
parent.setPaletteColour(k, v[1], v[2], v[3])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function internalBlit(sText, sTextColor, sBackgroundColor)
|
||||||
|
local nStart = nCursorX
|
||||||
|
local nEnd = nStart + #sText - 1
|
||||||
|
if nCursorY >= 1 and nCursorY <= nHeight then
|
||||||
|
if nStart <= nWidth and nEnd >= 1 then
|
||||||
|
-- Modify line
|
||||||
|
local tLine = tLines[nCursorY]
|
||||||
|
if nStart == 1 and nEnd == nWidth then
|
||||||
|
tLine.text = sText
|
||||||
|
tLine.textColor = sTextColor
|
||||||
|
tLine.backgroundColor = sBackgroundColor
|
||||||
|
else
|
||||||
|
local sClippedText, sClippedTextColor, sClippedBackgroundColor
|
||||||
|
if nStart < 1 then
|
||||||
|
local nClipStart = 1 - nStart + 1
|
||||||
|
local nClipEnd = nWidth - nStart + 1
|
||||||
|
sClippedText = string_sub(sText, nClipStart, nClipEnd)
|
||||||
|
sClippedTextColor = string_sub(sTextColor, nClipStart, nClipEnd)
|
||||||
|
sClippedBackgroundColor = string_sub(sBackgroundColor, nClipStart, nClipEnd)
|
||||||
|
elseif nEnd > nWidth then
|
||||||
|
local nClipEnd = nWidth - nStart + 1
|
||||||
|
sClippedText = string_sub(sText, 1, nClipEnd)
|
||||||
|
sClippedTextColor = string_sub(sTextColor, 1, nClipEnd)
|
||||||
|
sClippedBackgroundColor = string_sub(sBackgroundColor, 1, nClipEnd)
|
||||||
|
else
|
||||||
|
sClippedText = sText
|
||||||
|
sClippedTextColor = sTextColor
|
||||||
|
sClippedBackgroundColor = sBackgroundColor
|
||||||
|
end
|
||||||
|
|
||||||
|
local sOldText = tLine.text
|
||||||
|
local sOldTextColor = tLine.textColor
|
||||||
|
local sOldBackgroundColor = tLine.backgroundColor
|
||||||
|
local sNewText, sNewTextColor, sNewBackgroundColor
|
||||||
|
if nStart > 1 then
|
||||||
|
local nOldEnd = nStart - 1
|
||||||
|
sNewText = string_sub(sOldText, 1, nOldEnd) .. sClippedText
|
||||||
|
sNewTextColor = string_sub(sOldTextColor, 1, nOldEnd) .. sClippedTextColor
|
||||||
|
sNewBackgroundColor = string_sub(sOldBackgroundColor, 1, nOldEnd) .. sClippedBackgroundColor
|
||||||
|
else
|
||||||
|
sNewText = sClippedText
|
||||||
|
sNewTextColor = sClippedTextColor
|
||||||
|
sNewBackgroundColor = sClippedBackgroundColor
|
||||||
|
end
|
||||||
|
if nEnd < nWidth then
|
||||||
|
local nOldStart = nEnd + 1
|
||||||
|
sNewText = sNewText .. string_sub(sOldText, nOldStart, nWidth)
|
||||||
|
sNewTextColor = sNewTextColor .. string_sub(sOldTextColor, nOldStart, nWidth)
|
||||||
|
sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth)
|
||||||
|
end
|
||||||
|
|
||||||
|
tLine.text = sNewText
|
||||||
|
tLine.textColor = sNewTextColor
|
||||||
|
tLine.backgroundColor = sNewBackgroundColor
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Redraw line
|
||||||
|
if bVisible then
|
||||||
|
redrawLine(nCursorY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Move and redraw cursor
|
||||||
|
nCursorX = nEnd + 1
|
||||||
|
if bVisible then
|
||||||
|
updateCursorColor()
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- The window object. Refer to the @{window|module's documentation} for
|
||||||
|
-- a full description.
|
||||||
|
--
|
||||||
|
-- @type Window
|
||||||
|
-- @see term.Redirect
|
||||||
|
local window = {}
|
||||||
|
|
||||||
|
function window.write(sText)
|
||||||
|
sText = tostring(sText)
|
||||||
|
internalBlit(sText, string_rep(tHex[nTextColor], #sText), string_rep(tHex[nBackgroundColor], #sText))
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.blit(sText, sTextColor, sBackgroundColor)
|
||||||
|
if type(sText) ~= "string" then expect(1, sText, "string") end
|
||||||
|
if type(sTextColor) ~= "string" then expect(2, sTextColor, "string") end
|
||||||
|
if type(sBackgroundColor) ~= "string" then expect(3, sBackgroundColor, "string") end
|
||||||
|
if #sTextColor ~= #sText or #sBackgroundColor ~= #sText then
|
||||||
|
error("Arguments must be the same length", 2)
|
||||||
|
end
|
||||||
|
sTextColor = sTextColor:lower()
|
||||||
|
sBackgroundColor = sBackgroundColor:lower()
|
||||||
|
internalBlit(sText, sTextColor, sBackgroundColor)
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.clear()
|
||||||
|
local sEmptyText = sEmptySpaceLine
|
||||||
|
local sEmptyTextColor = tEmptyColorLines[nTextColor]
|
||||||
|
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
|
||||||
|
for y = 1, nHeight do
|
||||||
|
tLines[y] = {
|
||||||
|
text = sEmptyText,
|
||||||
|
textColor = sEmptyTextColor,
|
||||||
|
backgroundColor = sEmptyBackgroundColor,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if bVisible then
|
||||||
|
redraw()
|
||||||
|
updateCursorColor()
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.clearLine()
|
||||||
|
if nCursorY >= 1 and nCursorY <= nHeight then
|
||||||
|
local sEmptyText = sEmptySpaceLine
|
||||||
|
local sEmptyTextColor = tEmptyColorLines[nTextColor]
|
||||||
|
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
|
||||||
|
tLines[nCursorY] = {
|
||||||
|
text = sEmptyText,
|
||||||
|
textColor = sEmptyTextColor,
|
||||||
|
backgroundColor = sEmptyBackgroundColor,
|
||||||
|
}
|
||||||
|
if bVisible then
|
||||||
|
redrawLine(nCursorY)
|
||||||
|
updateCursorColor()
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.getCursorPos()
|
||||||
|
return nCursorX, nCursorY
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.setCursorPos(x, y)
|
||||||
|
if type(x) ~= "number" then expect(1, x, "number") end
|
||||||
|
if type(y) ~= "number" then expect(2, y, "number") end
|
||||||
|
nCursorX = math.floor(x)
|
||||||
|
nCursorY = math.floor(y)
|
||||||
|
if bVisible then
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.setCursorBlink(blink)
|
||||||
|
if type(blink) ~= "boolean" then expect(1, blink, "boolean") end
|
||||||
|
bCursorBlink = blink
|
||||||
|
if bVisible then
|
||||||
|
updateCursorBlink()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.getCursorBlink()
|
||||||
|
return bCursorBlink
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isColor()
|
||||||
|
return parent.isColor()
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.isColor()
|
||||||
|
return isColor()
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.isColour()
|
||||||
|
return isColor()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setTextColor(color)
|
||||||
|
if type(color) ~= "number" then expect(1, color, "number") end
|
||||||
|
if tHex[color] == nil then
|
||||||
|
error("Invalid color (got " .. color .. ")" , 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
nTextColor = color
|
||||||
|
if bVisible then
|
||||||
|
updateCursorColor()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
window.setTextColor = setTextColor
|
||||||
|
window.setTextColour = setTextColor
|
||||||
|
|
||||||
|
function window.setPaletteColour(colour, r, g, b)
|
||||||
|
if type(colour) ~= "number" then expect(1, colour, "number") end
|
||||||
|
|
||||||
|
if tHex[colour] == nil then
|
||||||
|
error("Invalid color (got " .. colour .. ")" , 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tCol
|
||||||
|
if type(r) == "number" and g == nil and b == nil then
|
||||||
|
tCol = { colours.unpackRGB(r) }
|
||||||
|
tPalette[colour] = tCol
|
||||||
|
else
|
||||||
|
if type(r) ~= "number" then expect(2, r, "number") end
|
||||||
|
if type(g) ~= "number" then expect(3, g, "number") end
|
||||||
|
if type(b) ~= "number" then expect(4, b, "number") end
|
||||||
|
|
||||||
|
tCol = tPalette[colour]
|
||||||
|
tCol[1] = r
|
||||||
|
tCol[2] = g
|
||||||
|
tCol[3] = b
|
||||||
|
end
|
||||||
|
|
||||||
|
if bVisible then
|
||||||
|
return parent.setPaletteColour(colour, tCol[1], tCol[2], tCol[3])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
window.setPaletteColor = window.setPaletteColour
|
||||||
|
|
||||||
|
function window.getPaletteColour(colour)
|
||||||
|
if type(colour) ~= "number" then expect(1, colour, "number") end
|
||||||
|
if tHex[colour] == nil then
|
||||||
|
error("Invalid color (got " .. colour .. ")" , 2)
|
||||||
|
end
|
||||||
|
local tCol = tPalette[colour]
|
||||||
|
return tCol[1], tCol[2], tCol[3]
|
||||||
|
end
|
||||||
|
|
||||||
|
window.getPaletteColor = window.getPaletteColour
|
||||||
|
|
||||||
|
local function setBackgroundColor(color)
|
||||||
|
if type(color) ~= "number" then expect(1, color, "number") end
|
||||||
|
if tHex[color] == nil then
|
||||||
|
error("Invalid color (got " .. color .. ")", 2)
|
||||||
|
end
|
||||||
|
nBackgroundColor = color
|
||||||
|
end
|
||||||
|
|
||||||
|
window.setBackgroundColor = setBackgroundColor
|
||||||
|
window.setBackgroundColour = setBackgroundColor
|
||||||
|
|
||||||
|
function window.getSize()
|
||||||
|
return nWidth, nHeight
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.scroll(n)
|
||||||
|
if type(n) ~= "number" then expect(1, n, "number") end
|
||||||
|
if n ~= 0 then
|
||||||
|
local tNewLines = {}
|
||||||
|
local sEmptyText = sEmptySpaceLine
|
||||||
|
local sEmptyTextColor = tEmptyColorLines[nTextColor]
|
||||||
|
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
|
||||||
|
for newY = 1, nHeight do
|
||||||
|
local y = newY + n
|
||||||
|
if y >= 1 and y <= nHeight then
|
||||||
|
tNewLines[newY] = tLines[y]
|
||||||
|
else
|
||||||
|
tNewLines[newY] = {
|
||||||
|
text = sEmptyText,
|
||||||
|
textColor = sEmptyTextColor,
|
||||||
|
backgroundColor = sEmptyBackgroundColor,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tLines = tNewLines
|
||||||
|
if bVisible then
|
||||||
|
redraw()
|
||||||
|
updateCursorColor()
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.getTextColor()
|
||||||
|
return nTextColor
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.getTextColour()
|
||||||
|
return nTextColor
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.getBackgroundColor()
|
||||||
|
return nBackgroundColor
|
||||||
|
end
|
||||||
|
|
||||||
|
function window.getBackgroundColour()
|
||||||
|
return nBackgroundColor
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the buffered contents of a line in this window.
|
||||||
|
--
|
||||||
|
-- @tparam number y The y position of the line to get.
|
||||||
|
-- @treturn string The textual content of this line.
|
||||||
|
-- @treturn string The text colours of this line, suitable for use with @{term.blit}.
|
||||||
|
-- @treturn string The background colours of this line, suitable for use with @{term.blit}.
|
||||||
|
-- @throws If `y` is not between 1 and this window's height.
|
||||||
|
-- @since 1.84.0
|
||||||
|
function window.getLine(y)
|
||||||
|
if type(y) ~= "number" then expect(1, y, "number") end
|
||||||
|
|
||||||
|
if y < 1 or y > nHeight then
|
||||||
|
error("Line is out of range.", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return tLines[y].text, tLines[y].textColor, tLines[y].backgroundColor
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Other functions
|
||||||
|
|
||||||
|
--- Set whether this window is visible. Invisible windows will not be drawn
|
||||||
|
-- to the screen until they are made visible again.
|
||||||
|
--
|
||||||
|
-- Making an invisible window visible will immediately draw it.
|
||||||
|
--
|
||||||
|
-- @tparam boolean visible Whether this window is visible.
|
||||||
|
function window.setVisible(visible)
|
||||||
|
if type(visible) ~= "boolean" then expect(1, visible, "boolean") end
|
||||||
|
if bVisible ~= visible then
|
||||||
|
bVisible = visible
|
||||||
|
if bVisible then
|
||||||
|
window.redraw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get whether this window is visible. Invisible windows will not be
|
||||||
|
-- drawn to the screen until they are made visible again.
|
||||||
|
--
|
||||||
|
-- @treturn boolean Whether this window is visible.
|
||||||
|
-- @see Window:setVisible
|
||||||
|
-- @since 1.94.0
|
||||||
|
function window.isVisible()
|
||||||
|
return bVisible
|
||||||
|
end
|
||||||
|
--- Draw this window. This does nothing if the window is not visible.
|
||||||
|
--
|
||||||
|
-- @see Window:setVisible
|
||||||
|
function window.redraw()
|
||||||
|
if bVisible then
|
||||||
|
redraw()
|
||||||
|
updatePalette()
|
||||||
|
updateCursorBlink()
|
||||||
|
updateCursorColor()
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the current terminal's cursor to where this window's cursor is. This
|
||||||
|
-- does nothing if the window is not visible.
|
||||||
|
function window.restoreCursor()
|
||||||
|
if bVisible then
|
||||||
|
updateCursorBlink()
|
||||||
|
updateCursorColor()
|
||||||
|
updateCursorPos()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the position of the top left corner of this window.
|
||||||
|
--
|
||||||
|
-- @treturn number The x position of this window.
|
||||||
|
-- @treturn number The y position of this window.
|
||||||
|
function window.getPosition()
|
||||||
|
return nX, nY
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Reposition or resize the given window.
|
||||||
|
--
|
||||||
|
-- This function also accepts arguments to change the size of this window.
|
||||||
|
-- It is recommended that you fire a `term_resize` event after changing a
|
||||||
|
-- window's, to allow programs to adjust their sizing.
|
||||||
|
--
|
||||||
|
-- @tparam number new_x The new x position of this window.
|
||||||
|
-- @tparam number new_y The new y position of this window.
|
||||||
|
-- @tparam[opt] number new_width The new width of this window.
|
||||||
|
-- @tparam number new_height The new height of this window.
|
||||||
|
-- @tparam[opt] term.Redirect new_parent The new redirect object this
|
||||||
|
-- window should draw to.
|
||||||
|
-- @changed 1.85.0 Add `new_parent` parameter.
|
||||||
|
function window.reposition(new_x, new_y, new_width, new_height, new_parent)
|
||||||
|
if type(new_x) ~= "number" then expect(1, new_x, "number") end
|
||||||
|
if type(new_y) ~= "number" then expect(2, new_y, "number") end
|
||||||
|
if new_width ~= nil or new_height ~= nil then
|
||||||
|
expect(3, new_width, "number")
|
||||||
|
expect(4, new_height, "number")
|
||||||
|
end
|
||||||
|
if new_parent ~= nil and type(new_parent) ~= "table" then expect(5, new_parent, "table") end
|
||||||
|
|
||||||
|
nX = new_x
|
||||||
|
nY = new_y
|
||||||
|
|
||||||
|
if new_parent then parent = new_parent end
|
||||||
|
|
||||||
|
if new_width and new_height then
|
||||||
|
local tNewLines = {}
|
||||||
|
createEmptyLines(new_width)
|
||||||
|
local sEmptyText = sEmptySpaceLine
|
||||||
|
local sEmptyTextColor = tEmptyColorLines[nTextColor]
|
||||||
|
local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
|
||||||
|
for y = 1, new_height do
|
||||||
|
if y > nHeight then
|
||||||
|
tNewLines[y] = {
|
||||||
|
text = sEmptyText,
|
||||||
|
textColor = sEmptyTextColor,
|
||||||
|
backgroundColor = sEmptyBackgroundColor,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
local tOldLine = tLines[y]
|
||||||
|
if new_width == nWidth then
|
||||||
|
tNewLines[y] = tOldLine
|
||||||
|
elseif new_width < nWidth then
|
||||||
|
tNewLines[y] = {
|
||||||
|
text = string_sub(tOldLine.text, 1, new_width),
|
||||||
|
textColor = string_sub(tOldLine.textColor, 1, new_width),
|
||||||
|
backgroundColor = string_sub(tOldLine.backgroundColor, 1, new_width),
|
||||||
|
}
|
||||||
|
else
|
||||||
|
tNewLines[y] = {
|
||||||
|
text = tOldLine.text .. string_sub(sEmptyText, nWidth + 1, new_width),
|
||||||
|
textColor = tOldLine.textColor .. string_sub(sEmptyTextColor, nWidth + 1, new_width),
|
||||||
|
backgroundColor = tOldLine.backgroundColor .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nWidth = new_width
|
||||||
|
nHeight = new_height
|
||||||
|
tLines = tNewLines
|
||||||
|
end
|
||||||
|
if bVisible then
|
||||||
|
window.redraw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if bVisible then
|
||||||
|
window.redraw()
|
||||||
|
end
|
||||||
|
return window
|
||||||
|
end
|
@ -0,0 +1,4 @@
|
|||||||
|
--[[
|
||||||
|
Alright then, don't ignore me. This file is to ensure the existence of the "autorun" folder, files placed in this folder
|
||||||
|
using resource packs will always run when computers startup.
|
||||||
|
]]
|
@ -0,0 +1 @@
|
|||||||
|
adventure is a text adventure game for CraftOS. To navigate around the world of adventure, type simple instructions to the interpreter, for example: "go north", "punch tree", "craft planks", "mine coal with pickaxe", "hit creeper with sword"
|
@ -0,0 +1,6 @@
|
|||||||
|
alias assigns shell commands to run other programs.
|
||||||
|
|
||||||
|
ex:
|
||||||
|
"alias dir ls" will make the "dir" command run the "ls" program
|
||||||
|
"alias dir" will remove the alias set on "dir"
|
||||||
|
"alias" will list all current aliases.
|
@ -0,0 +1,4 @@
|
|||||||
|
apis lists the currently loaded APIs available to programs in CraftOS.
|
||||||
|
|
||||||
|
Type "help <api>" to see help for a specific api.
|
||||||
|
Call os.loadAPI( path ) to load extra apis.
|
5
src/main/resources/assets/cctweaked/lua/rom/help/bg.txt
Normal file
5
src/main/resources/assets/cctweaked/lua/rom/help/bg.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
bg is a program for Advanced Computers which opens a new tab in the background.
|
||||||
|
|
||||||
|
ex:
|
||||||
|
"bg" will open a background tab running the shell
|
||||||
|
"bg worm" will open a background tab running the "worm" program
|
7
src/main/resources/assets/cctweaked/lua/rom/help/bit.txt
Normal file
7
src/main/resources/assets/cctweaked/lua/rom/help/bit.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Functions in the bit manipulation API (NOTE: This API will be removed in a future version. Use bit32 instead):
|
||||||
|
bit.bnot(n) -- bitwise not (~n)
|
||||||
|
bit.band(m, n) -- bitwise and (m & n)
|
||||||
|
bit.bor(m, n) -- bitwise or (m | n)
|
||||||
|
bit.bxor(m, n) -- bitwise xor (m ^ n)
|
||||||
|
bit.brshift(n, bits) -- right shift (n >> bits)
|
||||||
|
bit.blshift(n, bits) -- left shift (n << bits)
|
15
src/main/resources/assets/cctweaked/lua/rom/help/bundled.txt
Normal file
15
src/main/resources/assets/cctweaked/lua/rom/help/bundled.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
To set bundled outputs:
|
||||||
|
c = colors.combine( colors.red, colors.blue )
|
||||||
|
rs.setBundledOutput( "left", c )
|
||||||
|
|
||||||
|
c = colors.combine( c, colors.green )
|
||||||
|
rs.setBundledOutput( "left", c )
|
||||||
|
|
||||||
|
c = colors.subtract( c, colors.blue )
|
||||||
|
rs.setBundledOutput( "left", c )
|
||||||
|
|
||||||
|
To get bundled inputs:
|
||||||
|
c = rs.getBundledInput( "right" )
|
||||||
|
red = colors.test( c, colors.red )
|
||||||
|
|
||||||
|
Type "help colors" for the list of wire colors.
|
6
src/main/resources/assets/cctweaked/lua/rom/help/cd.txt
Normal file
6
src/main/resources/assets/cctweaked/lua/rom/help/cd.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
cd changes the directory you're in.
|
||||||
|
|
||||||
|
ex:
|
||||||
|
"cd rom" will move to "rom" folder.
|
||||||
|
"cd .." will move up one folder.
|
||||||
|
"cd /" will move to the root.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user