1
0
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:
Jonathan Coates 2023-06-13 17:30:09 +01:00
commit 073df30e53
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
288 changed files with 29793 additions and 0 deletions

33
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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.

View 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
View 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
View 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"

View 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")
}

View 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"
}
}

View 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) }
}
}
}

View 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
View 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
View 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
View 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

Binary file not shown.

View 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
View 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
View 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
View 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")

View 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;
}
}

View 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;
}
}

View 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) {
}
}

View 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");
}
};
}
};
}
}

View 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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 {
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
};
}
}

View File

@ -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);
}

View File

@ -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");
}
}

View 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";
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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 + ")");
}
}

View 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.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();
}
}

View File

@ -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();
}
}

View 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/&#42;/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());
}
}
}

View File

@ -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);
}
}

View 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);
}
};
}
}

View 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);
}
}

View 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;
}
}

View 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 ' ';
}
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View 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;
}
};
}
}

View 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;
}

View 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);
}
}
}
}

View 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;
}
}

View File

@ -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();
}

View 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);
}
}
}

View 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 };
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View 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);
}
}
}

View 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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View 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];
}
}

View 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()

View 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

View 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

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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)

View 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

View 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

View File

@ -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.
]]

View File

@ -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"

View File

@ -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.

View File

@ -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.

View 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

View 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)

View 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.

View 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