commit 073df30e530c0c0ecbcf752a3ee07ce28622496c Author: Jonathan Coates Date: Tue Jun 13 17:30:09 2023 +0100 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..92cfba65e --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..bcb471c43 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..09a09bef6 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..ca95b2e01 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..0d2c95e7f --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 000000000..d47c75e6c --- /dev/null +++ b/.reuse/dep5 @@ -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 + +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 diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 000000000..137069b82 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -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. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 000000000..0e259d42c --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -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. diff --git a/LICENSES/LicenseRef-CCPL.txt b/LICENSES/LicenseRef-CCPL.txt new file mode 100644 index 000000000..cf30d77a6 --- /dev/null +++ b/LICENSES/LicenseRef-CCPL.txt @@ -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. diff --git a/LICENSES/MPL-2.0.txt b/LICENSES/MPL-2.0.txt new file mode 100644 index 000000000..d0a1fa148 --- /dev/null +++ b/LICENSES/MPL-2.0.txt @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 000000000..3771efae7 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ + + +# ![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" diff --git a/build-tools/build.gradle.kts b/build-tools/build.gradle.kts new file mode 100644 index 000000000..daf7a7f31 --- /dev/null +++ b/build-tools/build.gradle.kts @@ -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") +} diff --git a/build-tools/src/main/kotlin/cc/tweaked/build/ClassEmitter.kt b/build-tools/src/main/kotlin/cc/tweaked/build/ClassEmitter.kt new file mode 100644 index 000000000..970378e3b --- /dev/null +++ b/build-tools/src/main/kotlin/cc/tweaked/build/ClassEmitter.kt @@ -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() + 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(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, 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" + } +} diff --git a/build-tools/src/main/kotlin/cc/tweaked/build/Main.kt b/build-tools/src/main/kotlin/cc/tweaked/build/Main.kt new file mode 100644 index 000000000..dd380d857 --- /dev/null +++ b/build-tools/src/main/kotlin/cc/tweaked/build/Main.kt @@ -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) { + 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) } + } + } +} diff --git a/build-tools/src/main/kotlin/cc/tweaked/build/Unlambda.kt b/build-tools/src/main/kotlin/cc/tweaked/build/Unlambda.kt new file mode 100644 index 000000000..003804784 --- /dev/null +++ b/build-tools/src/main/kotlin/cc/tweaked/build/Unlambda.kt @@ -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?) { + 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?): 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 != "") { + 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() + + 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, "", Type.getMethodDescriptor(Type.VOID_TYPE, *fields), false) + mw.visitInsn(ARETURN) + mw.visitMaxs(0, 0) + mw.visitEnd() + } + + cw.visitMethod(0, "", Type.getMethodDescriptor(Type.VOID_TYPE, *fields), null, null).let { mw -> + mw.visitCode() + mw.visitVarInsn(ALOAD, 0) + mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()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() + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..074ff7dea --- /dev/null +++ b/build.gradle.kts @@ -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) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..63501a90e --- /dev/null +++ b/gradle.properties @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..bb3a1de86 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..943f0cbfa Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..f398c33c4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..79a61d421 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..5bcbd464f --- /dev/null +++ b/settings.gradle.kts @@ -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") diff --git a/src/main/java/cc/tweaked/CCTweaked.java b/src/main/java/cc/tweaked/CCTweaked.java new file mode 100644 index 000000000..def84a2f7 --- /dev/null +++ b/src/main/java/cc/tweaked/CCTweaked.java @@ -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; + } +} diff --git a/src/main/java/cc/tweaked/patch/ClassTransformer.java b/src/main/java/cc/tweaked/patch/ClassTransformer.java new file mode 100644 index 000000000..d38b14ef2 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/ClassTransformer.java @@ -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 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; + } +} diff --git a/src/main/java/cc/tweaked/patch/CorePlugin.java b/src/main/java/cc/tweaked/patch/CorePlugin.java new file mode 100644 index 000000000..0ecf95c3b --- /dev/null +++ b/src/main/java/cc/tweaked/patch/CorePlugin.java @@ -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 map) { + } +} diff --git a/src/main/java/cc/tweaked/patch/RedirectDrawString.java b/src/main/java/cc/tweaked/patch/RedirectDrawString.java new file mode 100644 index 000000000..93ef0cd38 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/RedirectDrawString.java @@ -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 { + private final Consumer getTerminal; + + RedirectDrawString(Consumer 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"); + } + }; + } + }; + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/AnnotationHelper.java b/src/main/java/cc/tweaked/patch/framework/AnnotationHelper.java new file mode 100644 index 000000000..685089e0c --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/AnnotationHelper.java @@ -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 getAnnotation(List nodes, String name) { + if (nodes == null) return null; + for (AnnotationNode node : nodes) { + if (node.desc.equals(name)) { + Map result = new HashMap(); + 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 getAnnotation(ClassNode node, String name) { + Map annotation = getAnnotation(node.invisibleAnnotations, name); + if (annotation != null) return annotation; + + for (FieldNode field : (List) 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 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 Type of the return value + * @return The found value or null. + */ + @SuppressWarnings("unchecked") + public static T getAnnotationValue(Map annotation, String key) { + if (annotation == null) return null; + + Object value = annotation.get(key); + if (value == null) return null; + + return (T) value; + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/TransformationChain.java b/src/main/java/cc/tweaked/patch/framework/TransformationChain.java new file mode 100644 index 000000000..a350d32ec --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/TransformationChain.java @@ -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 transforms = new HashMap<>(); + + public TransformationChain atClass(String name, Transform transform) { + transforms.computeIfAbsent(name, x -> new ClassTransformer()).transforms.add(transform); + return this; + } + + public TransformationChain atMethod(String owner, String name, String desc, Transform 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>> methodTransforms = new HashMap<>(); + final List> transforms = new ArrayList<>(); + + ClassVisitor transform(ClassVisitor visitor) { + for (Transform 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>> methodTransforms; + + MethodTransformClassVisitor(ClassVisitor visitor, Map>> 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> transforms = methodTransforms.get(new MethodDesc(name, desc)); + if (transforms != null) { + for (Transform transform : transforms) visitor = transform.chain(visitor); + } + + return visitor; + } + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/BasicRemapper.java b/src/main/java/cc/tweaked/patch/framework/transform/BasicRemapper.java new file mode 100644 index 000000000..6dbe812e8 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/BasicRemapper.java @@ -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 types; + + private BasicRemapper(Map 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 toMethodTransform() { + return mv -> new RemapMethod(mv, this); + } + + public Transform toClassTransform() { + return cw -> new RemapClass(cw, this); + } + + @Override + public String toString() { + return "Remap[" + types + "]"; + } + + public static class Builder { + private final Map types = new HashMap<>(); + + public Builder remapType(String from, String to) { + types.put(from, to); + return this; + } + + public BasicRemapper build() { + return new BasicRemapper(types); + } + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/ClassMerger.java b/src/main/java/cc/tweaked/patch/framework/transform/ClassMerger.java new file mode 100644 index 000000000..48234bac1 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/ClassMerger.java @@ -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 { + 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)); + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/MergeVisitor.java b/src/main/java/cc/tweaked/patch/framework/transform/MergeVisitor.java new file mode 100644 index 000000000..53425d8da --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/MergeVisitor.java @@ -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 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 overrideInterfaces = new HashSet<>(); + for (String inter : (List) 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) node.fields) { + if (!AnnotationHelper.hasAnnotation(field.invisibleAnnotations, SHADOW)) field.accept(this); + } + + // Visit methods + for (MethodNode method : (List) node.methods) { + if (!method.name.equals("") && !method.name.equals("")) { + 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("") && 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 { + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/RemapClass.java b/src/main/java/cc/tweaked/patch/framework/transform/RemapClass.java new file mode 100644 index 000000000..1e12479a5 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/RemapClass.java @@ -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); + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/RemapMethod.java b/src/main/java/cc/tweaked/patch/framework/transform/RemapMethod.java new file mode 100644 index 000000000..4826d3823 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/RemapMethod.java @@ -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)); + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/ReplaceConstant.java b/src/main/java/cc/tweaked/patch/framework/transform/ReplaceConstant.java new file mode 100644 index 000000000..3d0cd63d8 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/ReplaceConstant.java @@ -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 { + private final Map replace; + + private ReplaceConstant(Map replace) { + this.replace = replace; + } + + public static Transform 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 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); + } + } + }; + } +} diff --git a/src/main/java/cc/tweaked/patch/framework/transform/Transform.java b/src/main/java/cc/tweaked/patch/framework/transform/Transform.java new file mode 100644 index 000000000..851c166ab --- /dev/null +++ b/src/main/java/cc/tweaked/patch/framework/transform/Transform.java @@ -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 The type of visitor, either {@link ClassVisitor} or {@link MethodVisitor}. + */ +public interface Transform { + /** + * 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); +} diff --git a/src/main/java/cc/tweaked/patch/mixins/TileEntityMonitorMixin.java b/src/main/java/cc/tweaked/patch/mixins/TileEntityMonitorMixin.java new file mode 100644 index 000000000..845897cb8 --- /dev/null +++ b/src/main/java/cc/tweaked/patch/mixins/TileEntityMonitorMixin.java @@ -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> 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"); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java b/src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java new file mode 100644 index 000000000..b6ee65cd7 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java @@ -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[])}. + *

+ * 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. + * + *

Example usage:

+ *
+ * {@code
+ * int slot = getInt( args, 0 );
+ * int amount = optInt( args, 1, 64 );
+ * }
+ * 
+ */ +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 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"; + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/Coerced.java b/src/main/java/dan200/computercraft/api/lua/Coerced.java new file mode 100644 index 000000000..1befa340e --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/Coerced.java @@ -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. + *

+ * 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. + * + *

Example:

+ *
{@code
+ * @LuaFunction
+ * public final void doSomething(Coerced myString) {
+ *   var value = myString.value();
+ * }
+ * }
+ * + * @param The type of the underlying value. + * @see IArguments#getStringCoerced(int) + */ +public final class Coerced { + 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); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/IArguments.java b/src/main/java/dan200/computercraft/api/lua/IArguments.java new file mode 100644 index 000000000..a998a61c0 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -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: + * + *
    + *
  • Lua values of type "string" will be represented by a {@link String}.
  • + *
  • Lua values of type "number" will be represented by a {@link Number}.
  • + *
  • Lua values of type "boolean" will be represented by a {@link Boolean}.
  • + *
  • Lua values of type "table" will be represented by a {@link Map}.
  • + *
  • Lua values of any other type will be represented by a {@code null} value.
  • + *
+ * + * @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 + * not 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. + *

+ * 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. + *

+ * 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 + * not 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 read only 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 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 getEnum(int index, Class 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 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 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 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 optFiniteDouble(int index) throws LuaException { + Optional 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 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 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 read only buffer. + * @throws LuaException If the value is not a string. + */ + public Optional 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 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 > Optional optEnum(int index, Class klass) throws LuaException { + Optional 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> 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 def) throws LuaException { + return optTable(index).orElse(def); + } + + /** + * Create a version of these arguments which escapes the scope of the current function call. + *

+ * 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. + *

+ * 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. + *

+ * 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; + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaException.java b/src/main/java/dan200/computercraft/api/lua/LuaException.java new file mode 100644 index 000000000..c992b34a1 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaException.java @@ -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; + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaFunction.java b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java new file mode 100644 index 000000000..964effce1 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java @@ -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. + *

+ * 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: + * + *

    + *
  • {@link IArguments}: The arguments supplied to this function.
  • + *
  • + * Alternatively, one may specify the desired arguments as normal parameters and the argument parsing code will + * be generated automatically. + *

    + * 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}. + *

  • + *
+ *

+ * 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)}. + *

+ * This is incompatible with {@link #mainThread()}. + * + * @return Whether this function supports unsafe arguments. + */ + boolean unsafe() default false; +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaValues.java b/src/main/java/dan200/computercraft/api/lua/LuaValues.java new file mode 100644 index 000000000..4c0b377c4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaValues.java @@ -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 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 checkEnum(int index, Class 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 + ")"); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java new file mode 100644 index 000000000..e336595c4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java @@ -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 args; + + @Deprecated + @SuppressWarnings("unused") + public ObjectArguments(IArguments arguments) { + throw new IllegalStateException(); + } + + public ObjectArguments(Object... args) { + this.args = Arrays.asList(args); + } + + public ObjectArguments(List 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(); + } +} diff --git a/src/main/java/dan200/computercraft/client/FixedWidthFontRenderer.java b/src/main/java/dan200/computercraft/client/FixedWidthFontRenderer.java new file mode 100644 index 000000000..73d5c7c4e --- /dev/null +++ b/src/main/java/dan200/computercraft/client/FixedWidthFontRenderer.java @@ -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(); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java new file mode 100644 index 000000000..74d8c2977 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -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: + * + *
    + *
  • **Reading and writing files:** Call {@link #open} to obtain a file "handle", which can be used to read from or + * write to a file.
  • + *
  • **Path manipulation:** {@link #combine}, {@link #getName} and {@link #getDir} allow you to manipulate file + * paths, joining them together or extracting components.
  • + *
  • **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}.
  • + *
  • **File and directory manipulation:** For instance, moving or copying files. See {@link #makeDir}, {@link #move}, + * {@link #copy} and {@link #delete}.
  • + *
+ *

+ * :::note + * All functions in the API work on absolute paths, and do not take the @{shell.dir|current directory} into account. + * You can use @{shell.resolve} to convert a relative path into an absolute one. + * ::: + *

+ * ## Mounts + * While a computer can only have one hard drive and filesystem, other filesystems may be "mounted" inside it. For + * instance, the {@link dan200.computer.shared.TileEntityDiskDrive drive peripheral} mounts + * its disk's contents at {@code "disk/"}, {@code "disk1/"}, etc... + *

+ * You can see which mount a path belongs to with the {@link #getDrive} function. This returns {@code "hdd"} for the + * computer's main filesystem ({@code "/"}), {@code "rom"} for the rom ({@code "rom/"}). + *

+ * Most filesystems have a limited capacity, operations which would cause that capacity to be reached (such as writing + * an incredibly large file) will fail. You can see a mount's capacity with {@link #getCapacity} and the remaining + * space with {@link #getFreeSpace}. + * + * @cc.module fs + */ +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/} + *

{@code
+     * local files = fs.list("/rom/")
+     * for i = 1, #files do
+     *   print(files[i])
+     * end
+     * }
+ */ + @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 + *
{@code
+     * fs.combine("/rom/programs", "../apis", "parallel.lua")
+     * -- => rom/apis/parallel.lua
+     * }
+ */ + @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} + *
{@code
+     * fs.getName("rom/startup.lua")
+     * -- => startup.lua
+     * }
+ */ + @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} + *
{@code
+     * fs.getDir("rom/startup.lua")
+     * -- => rom
+     * }
+ */ + @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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * The {@code 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. + * + * @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. + *

{@code
+     * local file = fs.open("/rom/help/intro.txt", "r")
+     * local contents = file.readAll()
+     * file.close()
+     *
+     * print(contents)
+     * }
+ * @cc.usage Open a file and read all lines into a table. @{io.lines} offers an alternative way to do this. + *
{@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.
+     * }
+ * @cc.usage Open a file and write some text to it. You can run {@code edit out.txt} to see the written text. + *
{@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!
+     * }
+ */ + @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: + * + *
{@code
+     * print("/: " .. fs.getDrive("/"))
+     * print("/rom/: " .. fs.getDrive("rom"))
+     * }
+ */ + @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. + *

+ * 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, rom/*/command* 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. + *

+ * 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. + *

+ * 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 attributes(String path) throws LuaException { + try { + boolean isDir = getFileSystem().isDir(path); + Map 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()); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/FileSystemExtensions.java b/src/main/java/dan200/computercraft/core/apis/FileSystemExtensions.java new file mode 100644 index 000000000..77f87f77f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/FileSystemExtensions.java @@ -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 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 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 outputParts = new ArrayDeque(); + 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); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java new file mode 100644 index 000000000..d659e4032 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java @@ -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 toTable(TemporalAccessor date, ZoneId offset, Instant instant) { + Map 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 adjustInto(R temporal, long newValue) { + return (R) temporal.with(field, newValue); + } + }; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java new file mode 100644 index 000000000..72773d874 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -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 alarms = new HashMap<>(); + private final Map 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 { + 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> it = timers.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry 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> it = alarms.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry 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. + *

+ * 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 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). + *

+ * * 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. + *

+ * 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. + *

{@code
+     * textutils.formatTime(os.time())
+     * }
+ * @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. + *

+ * * 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 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. + *

+ * * 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. + *

+ * :::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. + *

{@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))
+     * }
+ */ + @LuaFunction + public final long epoch(Optional 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. + *

+ * 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. + *

+ * 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. + *

{@code
+     * os.date("%A %d %B %Y") -- See the reference above!
+     * }
+ */ + @LuaFunction + public final Object date(Optional formatA, Optional 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); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/TermAPI.java b/src/main/java/dan200/computercraft/core/apis/TermAPI.java new file mode 100644 index 000000000..37baa0b29 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TermAPI.java @@ -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; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/TermMethods.java b/src/main/java/dan200/computercraft/core/apis/TermMethods.java new file mode 100644 index 000000000..e5e10f29f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TermMethods.java @@ -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. + *

+ * 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 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * {@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. + *

{@code
+     * term.blit("Hello, world!","01234456789ab","0000000000000")
+     * }
+ */ + @LuaFunction + public final void blit(Coerced text, Coerced textColour, Coerced 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. + *

+ * ComputerCraft's palette system allows you to change how a specific colour should be displayed. For instance, you + * can make @{colors.red} more red 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 which 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. + *

{@code
+     * term.setPaletteColour(colors.red, 0xFF0000)
+     * term.setTextColour(colors.red)
+     * print("Hello, world!")
+     * }
+ * @cc.usage As above, but specifying each colour channel separately. + *
{@code
+     * term.setPaletteColour(colors.red, 1, 0, 0)
+     * term.setTextColour(colors.red)
+     * print("Hello, world!")
+     * }
+ * @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 ' '; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java new file mode 100644 index 000000000..d6b2b6f7c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -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 as a number. 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 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 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; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java new file mode 100644 index 000000000..542e71a02 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -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()); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java new file mode 100644 index 000000000..271cece25 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -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 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; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java new file mode 100644 index 000000000..c362e6211 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java @@ -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 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 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()); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java new file mode 100644 index 000000000..0366b5197 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -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. + *

+ * 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(); + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java b/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java new file mode 100644 index 000000000..070845c62 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java @@ -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); + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java new file mode 100644 index 000000000..810b6846f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -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 { + 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 base; + private final List> context; + + private final String[] interfaces; + private final String methodDesc; + + private final Function wrap; + + private final LoadingCache, List>> classCache = CacheBuilder + .newBuilder() + .build(CacheLoader.from(catching(this::build, Collections.emptyList()))); + + private final LoadingCache> methodCache = CacheBuilder + .newBuilder() + .build(CacheLoader.from(catching(this::build, Optional.empty()))); + + Generator(Class base, List> context, Function 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> 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> build(Class klass) { + ArrayList> 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> 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 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, "", "()V", null, null); + mw.visitCode(); + mw.visitVarInsn(ALOAD, 0); + mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()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, "", "(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 com.google.common.base.Function catching(Function 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; + } + }; + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/LuaMethod.java b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java new file mode 100644 index 000000000..0d4ccb5aa --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java @@ -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; +} diff --git a/src/main/java/dan200/computercraft/core/asm/Methods.java b/src/main/java/dan200/computercraft/core/asm/Methods.java new file mode 100644 index 000000000..4abca6754 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Methods.java @@ -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 LUA_METHOD = new Generator<>(LuaMethod.class, Collections.emptyList(), m -> { + throw new IllegalStateException("Impossible"); + }); + + public static void forEachMethod(Generator generator, Object object, BiConsumer> accept) { + for (NamedMethod method : generator.getMethods(object.getClass())) accept.accept(object, method); + + if (object instanceof ObjectSource) { + for (Object extra : ((ObjectSource) object).getExtra()) { + for (NamedMethod method : generator.getMethods(extra.getClass())) accept.accept(extra, method); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java new file mode 100644 index 000000000..050edeff3 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java @@ -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 { + 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; + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/ObjectSource.java b/src/main/java/dan200/computercraft/core/asm/ObjectSource.java new file mode 100644 index 000000000..a0be197a5 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/ObjectSource.java @@ -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. + *

+ * 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 getExtra(); +} diff --git a/src/main/java/dan200/computercraft/core/asm/Reflect.java b/src/main/java/dan200/computercraft/core/asm/Reflect.java new file mode 100644 index 000000000..1c5776cc3 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Reflect.java @@ -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); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Support.java b/src/main/java/dan200/computercraft/core/asm/Support.java new file mode 100644 index 000000000..fbfffe7ef --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Support.java @@ -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 }; + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/BasicFunction.java b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java new file mode 100644 index 000000000..56e6cbc2b --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java @@ -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. + *

+ * 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()); + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java new file mode 100644 index 000000000..ff5475cf7 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -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 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 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 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 table = new HashMap(); + 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; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java new file mode 100644 index 000000000..0d74212ef --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java @@ -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 cache; + private ArraySlice 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 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 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 cache = this.cache; + ArraySlice 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 { + 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 drop(int count) { + return new ArraySlice<>(array, offset + count); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/util/StringUtil.java b/src/main/java/dan200/computercraft/core/util/StringUtil.java new file mode 100644 index 000000000..892bd269f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/util/StringUtil.java @@ -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(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java new file mode 100644 index 000000000..95bbb6f0f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java @@ -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. + *

+ * Monitors act as @{term.Redirect|terminal redirects} and so expose the same methods, as well as several additional + * ones, which are documented below. + *

+ * Like computers, monitors come in both normal (no colour) and advanced (colour) varieties. + *

+ * ## Recipes + *

+ * + * + *
+ * + * @cc.module monitor + * @cc.usage Write "Hello, world!" to an adjacent monitor: + * + *
{@code
+ * local monitor = peripheral.find("monitor")
+ * monitor.setCursorPos(1, 1)
+ * monitor.write("Hello, world!")
+ * }
+ */ +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; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileEntityMonitorAccessor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileEntityMonitorAccessor.java new file mode 100644 index 000000000..de4758e6a --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileEntityMonitorAccessor.java @@ -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(); +} diff --git a/src/main/java/dan200/computercraft/util/Colour.java b/src/main/java/dan200/computercraft/util/Colour.java new file mode 100644 index 000000000..c284ca78f --- /dev/null +++ b/src/main/java/dan200/computercraft/util/Colour.java @@ -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]; + } +} diff --git a/src/main/resources/assets/cctweaked/lua/bios.lua b/src/main/resources/assets/cctweaked/lua/bios.lua new file mode 100644 index 000000000..917b69862 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/bios.lua @@ -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() diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/colors.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/colors.lua new file mode 100644 index 000000000..5bdbf9fd2 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/colors.lua @@ -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`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Default Colors
ColorValueDefault Palette Color
DecHexPaint/BlitPreviewHexRGBGrayscale
colors.white10x10#F0F0F0240, 240, 240
colors.orange20x21#F2B233242, 178, 51
colors.magenta40x42#E57FD8229, 127, 216
colors.lightBlue80x83#99B2F2153, 178, 242
colors.yellow160x104#DEDE6C222, 222, 108
colors.lime320x205#7FCC19127, 204, 25
colors.pink640x406#F2B2CC242, 178, 204
colors.gray1280x807#4C4C4C76, 76, 76
colors.lightGray2560x1008#999999153, 153, 153
colors.cyan5120x2009#4C99B276, 153, 178
colors.purple10240x400a#B266E5178, 102, 229
colors.blue20480x800b#3366CC51, 102, 204
colors.brown40960x1000c#7F664C127, 102, 76
colors.green81920x2000d#57A64E87, 166, 78
colors.red163840x4000e#CC4C4C204, 76, 76
colors.black327680x8000f#11111117, 17, 17
+ +@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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/colours.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/colours.lua new file mode 100644 index 000000000..7181cb85c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/colours.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/command/commands.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/command/commands.lua new file mode 100644 index 000000000..315e17358 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/command/commands.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/disk.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/disk.lua new file mode 100644 index 000000000..cf518bfba --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/disk.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/fs.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/fs.lua new file mode 100644 index 000000000..1a0fb4b53 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/fs.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/gps.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/gps.lua new file mode 100644 index 000000000..d95715fe0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/gps.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/help.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/help.lua new file mode 100644 index 000000000..1836dc3b5 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/help.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/http/http.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/http/http.lua new file mode 100644 index 000000000..087898b91 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/http/http.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/io.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/io.lua new file mode 100644 index 000000000..125397900 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/io.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/keys.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/keys.lua new file mode 100644 index 000000000..26b1d828a --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/keys.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/paintutils.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/paintutils.lua new file mode 100644 index 000000000..d52373ce7 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/paintutils.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/parallel.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/parallel.lua new file mode 100644 index 000000000..20b2b1e8f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/parallel.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/peripheral.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/peripheral.lua new file mode 100644 index 000000000..7515a3578 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/peripheral.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/rednet.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/rednet.lua new file mode 100644 index 000000000..a2d168cc0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/rednet.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/settings.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/settings.lua new file mode 100644 index 000000000..e4ebd7cff --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/settings.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/term.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/term.lua new file mode 100644 index 000000000..2bfa72b8b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/term.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/textutils.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/textutils.lua new file mode 100644 index 000000000..353dbb799 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/textutils.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/turtle/turtle.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/turtle/turtle.lua new file mode 100644 index 000000000..5a19e15a7 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/turtle/turtle.lua @@ -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) diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/vector.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/vector.lua new file mode 100644 index 000000000..e4d204062 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/vector.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/apis/window.lua b/src/main/resources/assets/cctweaked/lua/rom/apis/window.lua new file mode 100644 index 000000000..302372a39 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/apis/window.lua @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/autorun/.ignoreme b/src/main/resources/assets/cctweaked/lua/rom/autorun/.ignoreme new file mode 100644 index 000000000..daa5f8acc --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/autorun/.ignoreme @@ -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. +]] diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/adventure.txt b/src/main/resources/assets/cctweaked/lua/rom/help/adventure.txt new file mode 100644 index 000000000..6a9880eba --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/adventure.txt @@ -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" diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/alias.txt b/src/main/resources/assets/cctweaked/lua/rom/help/alias.txt new file mode 100644 index 000000000..8f742bd3f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/alias.txt @@ -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. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/apis.txt b/src/main/resources/assets/cctweaked/lua/rom/help/apis.txt new file mode 100644 index 000000000..20fc893fb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/apis.txt @@ -0,0 +1,4 @@ +apis lists the currently loaded APIs available to programs in CraftOS. + +Type "help " to see help for a specific api. +Call os.loadAPI( path ) to load extra apis. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/bg.txt b/src/main/resources/assets/cctweaked/lua/rom/help/bg.txt new file mode 100644 index 000000000..e674e2184 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/bg.txt @@ -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 diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/bit.txt b/src/main/resources/assets/cctweaked/lua/rom/help/bit.txt new file mode 100644 index 000000000..eeee09e05 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/bit.txt @@ -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) diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/bundled.txt b/src/main/resources/assets/cctweaked/lua/rom/help/bundled.txt new file mode 100644 index 000000000..9b0a94187 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/bundled.txt @@ -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. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/cd.txt b/src/main/resources/assets/cctweaked/lua/rom/help/cd.txt new file mode 100644 index 000000000..6acd66ad5 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/cd.txt @@ -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. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/changelog.md b/src/main/resources/assets/cctweaked/lua/rom/help/changelog.md new file mode 100644 index 000000000..098267351 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/changelog.md @@ -0,0 +1,1272 @@ +# New features in CC: Tweaked 1.105.0 + +* Optimise JSON string parsing. +* Add `colors.fromBlit` (Erb3). +* Upload file size limit is now configurable (khankul). +* Wired cables no longer have a distance limit. +* Java methods now coerce values to strings consistently with Lua. +* Add custom timeout support to the HTTP API. +* Support custom proxies for HTTP requests (Lemmmy). +* The `speaker` program now errors when playing HTML files. +* `edit` now shows an error message when editing read-only files. +* Update Ukranian translation (SirEdvin). + +Several bug fixes: +* Allow GPS hosts to only be 1 block apart. +* Fix "Turn On"/"Turn Off" buttons being inverted in the computer GUI (Erb3). +* Fix arrow keys not working in the printout UI. +* Several documentation fixes (zyxkad, Lupus590, Commandcracker). +* Fix monitor renderer debug text always being visible on Forge. +* Fix crash when another mod changes the LoggerContext. +* Fix the `monitor_renderer` option not being present in Fabric config files. +* Pasting on MacOS/OSX now uses Cmd+V rather than Ctrl+V. +* Fix turtles placing blocks upside down when at y<0. + +# New features in CC: Tweaked 1.104.0 + +* Update to Minecraft 1.19.4. +* Turtles can now right click items "into" certain blocks (cauldrons and hives by default, configurable with the `computercraft:turtle_can_use` block tag). +* Update Cobalt to 0.7: + * `table` methods and `ipairs` now use metamethods. + * Type errors now use the `__name` metatag. + * Coroutines no longer run on multiple threads. + * Timeout errors should be thrown more reliably. +* `speaker` program now reports an error on common unsupported audio formats. +* `multishell` now hides the implementation details of its terminal redirect from programs. +* Use VBO monitor renderer by default. +* Improve syntax errors when missing commas in tables, and on trailing commas in parameter lists. +* Turtles can now hold flags. +* Update several translations (Alessandro, chesiren, Erlend, RomanPlayer22). + +Several bug fixes: +* `settings.load` now ignores malformed values created by editing the `.settings` file by hand. +* Fix introduction dates on `os.cancelAlarm` and `os.cancelTimer` (MCJack123). +* Fix the REPL syntax reporting crashing on valid parses. +* Make writes to the ID file atomic. +* Obey stack limits when transferring items with Fabric's APIs. +* Ignore metatables in `textutils.serialize`. +* Correctly recurse into NBT lists when computing the NBT hash (Lemmmy). +* Fix advanced pocket computers rendering as greyscale. +* Fix stack overflow when using `shell` as a hashbang program. +* Fix websocket messages being empty when using a non-default compression settings. +* Fix `gps.locate` returning `nan` when receiving a duplicate location (Wojbie). +* Remove several thread safety issues inside Java-side argument parsing code. + +# New features in CC: Tweaked 1.103.1 + +Several bug fixes: +* Fix values not being printed in the REPL. +* Fix `function f()` providing suboptimal parse errors in the REPL. + +# New features in CC: Tweaked 1.103.0 + +* The shell now supports hashbangs (`#!`) (emmachase). +* Error messages in `edit` are now displayed in red on advanced computers. +* `turtle.getItemDetail` now always includes the `nbt` hash. +* Improvements to the display of errors in the shell and REPL. +* Turtles, pocket computers, and disks can be undyed by careful application (i.e. crafting) of a sponge. +* Turtles can no longer be dyed/undyed by right clicking. + +Several bug fixes: +* Several documentation improvements and fixes (ouroborus, LelouBil). +* Fix rednet queueing the wrong message when sending a message to the current computer. +* Fix the Lua VM crashing when a `__len` metamethod yields. +* `pocket.{un,}equipBack` now correctly copies the stack when unequipping an upgrade. +* Fix `key` events not being queued while pressing computer shortcuts. + +# New features in CC: Tweaked 1.102.2 + +Several bug fixes: +* Fix printouts crashing in item frames. +* Fix disks not being assigned an ID when placed in a disk drive. + +# New features in CC: Tweaked 1.102.1 + +Several bug fixes: +* Fix crash on Fabric when refuelling with a non-fuel item (emmachase). +* Fix crash when calling `pocket.equipBack()` with a wireless modem. +* Fix turtles dropping their inventory when moving (emmachase). +* Fix crash when inserting items into a full inventory (emmachase). +* Simplify wired cable breaking code, fixing items sometimes not dropping. +* Correctly handle double chests being treated as single chests under Fabric. +* Fix `mouse_up` not being fired under Fabric. +* Fix full-block Wired modems not connecting to adjacent cables when placed. +* Hide the search tab from the `itemGroups` item details. +* Fix speakers playing too loudly. +* Change where turtles drop items from, reducing the chance that items clip through blocks. +* Fix the `computer_threads` config option not applying under Fabric. +* Fix stack overflow in logging code. + +# New features in CC: Tweaked 1.102.0 + +* `fs.isReadOnly` now reads filesystem attributes (Lemmmy). +* `IComputerAccess.executeMainThreadTask` no longer roundtrips values through Lua. +* The turtle label now animates when the turtle moves. + +Several bug fixes: +* Trim spaces from filesystem paths. +* Correctly format 12AM/PM with `%I`. +* Fix `import.lua` failing to upload a file. +* Fix duplicated swing animations on high-ping servers (emmachase). +* Fix several issues with sparse Lua tables (Shiranuit). + +# New features in CC: Tweaked 1.101.1 + +Several bug fixes: +* Improve validation of rednet messages (Ale32bit). +* Fix `turtle.refuel()` always failing. + +# New features in CC: Tweaked 1.101.0 + +* Improve Dutch translation (Quezler) +* Better reporting of fatal computer timeouts in the server log. +* Convert detail providers into a registry, allowing peripheral mods to read item/block details. +* Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time. +* File drag-and-drop now queues a `file_transfer` event on the computer. The built-in shell or the `import` program must now be running to upload files. +* The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems. +* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72). +* Add `shell.autocomplete_hidden` setting. (IvoLeal72) + +Several bug fixes: +* Prevent `edit`'s "Run" command scrolling the terminal output on smaller screens. +* Remove some non-determinism in computing item's `nbt` hash. +* Don't set the `Origin` header on outgoing websocket requests. + +# New features in CC: Tweaked 1.100.10 + +* Mention WAV support in speaker help (MCJack123). +* Add http programs to the path, even when http is not enabled. + +Several bug fixes: +* Fix example in `textutils.pagedTabulate` docs (IvoLeal72). +* Fix help program treating the terminal one line longer than it was. +* Send block updates to client when turtle moves (roland-a). +* Resolve several monitor issues when running Occulus shaders. + +# New features in CC: Tweaked 1.100.9 + +* Add documentation for setting up GPS (Lupus590). +* Add WAV support to the `speaker` program (MCJack123). +* Expose item groups in `getItemDetail` (itisluiz). +* Other fixes to documentation (Erb3, JohnnyIrvin). +* Add Norwegian translation (Erb3). + +Several bug fixes: +* Fix z-fighting on bold printout borders (toad-dev). +* Fix `term.blit` failing on certain strings. +* Fix `getItemLimit()` using the wrong slot (heap-underflow). +* Increase size of monitor depth blocker. + +# New features in CC: Tweaked 1.100.8 + +Several bug fixes: +* Fix NPE within disk drive and printer code. + +# New features in CC: Tweaked 1.100.7 + +* Fix failing to launch outside of a dev environment. + + +# New features in CC: Tweaked 1.100.6 + +* Various documentation improvements (MCJack123, FayneAldan). +* Allow CC's blocks to be rotated when used in structure blocks (Seniorendi). +* Several performance improvements to computer execution. +* Add parse_empty_array option to textutils.unserialiseJSON (@ChickChicky). +* Add an API to allow other mods to provide extra item/block details (Lemmmy). +* All blocks with GUIs can now be "locked" (via a command or NBT editing tools) like vanilla inventories. Players can only interact with them with a specific named item. + +Several bug fixes: +* Fix printouts being rendered with an offset in item frames (coolsa). +* Reduce position latency when playing audio with a noisy pocket computer. +* Fix total counts in /computercraft turn-on/shutdown commands. +* Fix "Run" command not working in the editor when run from a subdirectory (Wojbie). +* Pocket computers correctly preserve their on state. + +# New features in CC: Tweaked 1.100.5 + +* Generic peripherals now use capabilities on the given side if one isn't provided on the internal side. +* Improve performance of monitor rendering. + +Several bug fixes: +* Various documentation fixes (bclindner, Hasaabitt) +* Speaker sounds are now correctly positioned on the centre of the speaker block. + +# New features in CC: Tweaked 1.100.4 + +Several bug fixes: +* Fix the monitor watching blocking the main thread when chunks are slow to load. + +# New features in CC: Tweaked 1.100.3 + +Several bug fixes: +* Fix client disconnect when uploading large files. +* Correctly handling empty computer ID file. +* Fix the normal turtle recipe not being unlocked. +* Remove turtle fake EntityType. + +# New features in CC: Tweaked 1.100.2 + +Several bug fixes: +* Fix wired modems swapping the modem/peripheral block state. +* Remove debugging logging line from `turtle.attack`. + +# New features in CC: Tweaked 1.100.1 + +Several bug fixes: +* Fix `peripheral.hasType` not working with wired modems (Toad-Dev). +* Fix crashes when noisy pocket computer are shutdown. + +# New features in CC: Tweaked 1.100.0 + +* Speakers can now play arbitrary PCM audio. +* Add support for encoding and decoding DFPWM streams, with the `cc.audio.dfpwm` module. +* Wired modems now only render breaking progress for the part which is being broken. +* Various documentation improvements. + +Several bug fixes: +* Fix the "repeat" program not repeating broadcast rednet messages. +* Fix the drag-and-drop upload functionality writing empty files. +* Prevent turtles from pushing non-pushable entities. + +# New features in CC: Tweaked 1.99.1 + +* Add package.searchpath to the cc.require API. (MCJack123) +* Provide a more efficient way for the Java API to consume Lua tables in certain restricted cases. + +Several bug fixes: +* Fix keys being "sticky" when opening the off-hand pocket computer GUI. +* Correctly handle broken coroutine managers resuming Java code with a `nil` event. +* Prevent computer buttons stealing focus from the terminal. +* Fix a class cast exception when a monitor is malformed in ways I do not quite understand. + +# New features in CC: Tweaked 1.99.0 + +* Pocket computers in their offhand will open without showing a terminal. You can look around and interact with the world, but your keyboard will be forwarded to the computer. (Wojbie, MagGen-hub). +* Peripherals can now have multiple types. `peripheral.getType` now returns multiple values, and `peripheral.hasType` checks if a peripheral has a specific type. +* Add several missing keys to the `keys` table. (ralphgod3) +* Add feature introduction/changed version information to the documentation. (MCJack123) +* Increase the file upload limit to 512KiB. +* Rednet can now handle computer IDs larger than 65535. (Ale32bit) +* Optimise deduplication of rednet messages (MCJack123) +* Make `term.blit` colours case insensitive. (Ocawesome101) +* Add a new `about` program for easier version identification. (MCJack123) +* Optimise peripheral calls in `rednet.run`. (xAnavrins) +* Add dimension parameter to `commands.getBlockInfo`. +* Add `cc.pretty.pretty_print` helper function (Lupus590). +* Add back JEI integration. +* Turtle and pocket computer upgrades can now be added and modified with data packs. +* Various translation updates (MORIMORI3017, Ale2Bit, mindy15963) + +And several bug fixes: +* Fix various computer commands failing when OP level was 4. +* Various documentation fixes. (xXTurnerLP, MCJack123) +* Fix `textutils.serialize` not serialising infinity and nan values. (Wojbie) +* Wired modems now correctly clean up mounts when a peripheral is detached. +* Fix incorrect turtle and pocket computer upgrade recipes in the recipe book. +* Fix speakers not playing sounds added via resource packs which are not registered in-game. +* Fix speaker upgrades sending packets after the server has stopped. +* Monitor sizing has been rewritten, hopefully making it more stable. +* Peripherals are now invalidated when the computer ticks, rather than when the peripheral changes. +* Fix printouts and pocket computers rendering at fullbright when in item frames. +* All mod blocks now have an effective tool (pickaxe). + +# New features in CC: Tweaked 1.98.2 + +* Add JP translation (MORIMORI0317) +* Migrate several recipes to data generators. + +Several bug fixes: +* Fix volume speaker sounds are played at. +* Fix several rendering issues when holding pocket computers and printouts in hand. +* Ensure wired modems and cables join the wired network on chunk load. +* Fix stack overflow when using wired networks. + +# New features in CC: Tweaked 1.98.1 + +Several bug fixes: +* Fix monitors not correctly resizing when placed. +* Update Russian translation (DrHesperus). + +# New features in CC: Tweaked 1.98.0 +* Add motd for file uploading. +* Add config options to limit total bandwidth used by the HTTP API. + +And several bug fixes: +* Fix `settings.define` not accepting a nil second argument (SkyTheCodeMaster). +* Various documentation fixes (Angalexik, emiliskiskis, SkyTheCodeMaster). +* Fix selected slot indicator not appearing in turtle interface. +* Fix crash when printers are placed as part of world generation. +* Fix crash when breaking a speaker on a multiplayer world. +* Add a missing type check for `http.checkURL`. +* Prevent `parallel.*` from hanging when no arguments are given. +* Prevent issue in rednet when the message ID is NaN. +* Fix `help` program crashing when terminal changes width. +* Ensure monitors are well-formed when placed, preventing graphical glitches when using Carry On or Quark. +* Accept several more extensions in the websocket client. +* Prevent `wget` crashing when given an invalid URL and no filename. +* Correctly wrap string within `textutils.slowWrite`. + +# New features in CC: Tweaked 1.97.0 + +* Update several translations (Anavrins, Jummit, Naheulf). +* Add button to view a computer's folder to `/computercraft dump`. +* Allow cleaning dyed turtles in a cauldron. +* Add scale subcommand to `monitor` program (MCJack123). +* Add option to make `textutils.serialize` not write an indent (magiczocker10). +* Allow comparing vectors using `==` (fatboychummy). +* Improve HTTP error messages for SSL failures. +* Allow `craft` program to craft unlimited items (fatboychummy). +* Impose some limits on various command queues. +* Add buttons to shutdown and terminate to computer GUIs. +* Add program subcompletion to several programs (Wojbie). +* Update the `help` program to accept and (partially) highlight markdown files. +* Remove config option for the debug API. +* Allow setting the subprotocol header for websockets. +* Add basic JMX monitoring on dedicated servers. +* Add support for MoreRed bundled. +* Allow uploading files by dropping them onto a computer. + +And several bug fixes: +* Fix NPE when using a treasure disk when no treasure disks are available. +* Prevent command computers discarding command output when certain game rules are off. +* Fix turtles not updating peripherals when upgrades are unequipped (Ronan-H). +* Fix computers not shutting down on fatal errors within the Lua VM. +* Speakers now correctly stop playing when broken, and sound follows noisy turtles and pocket computers. +* Update the `wget` to be more resiliant in the face of user-errors. +* Fix exiting `paint` typing "e" in the shell. +* Fix coloured pocket computers using the wrong texture. +* Correctly render the transparent background on pocket/normal computers. +* Don't apply CraftTweaker actions twice on single-player worlds. + +# New features in CC: Tweaked 1.96.0 + +* Use lightGrey for folders within the "list" program. +* Add `getItemLimit` to inventory peripherals. +* Expose the generic peripheral system to the public API. +* Add cc.expect.range (Lupus590). +* Allow calling cc.expect directly (MCJack123). +* Numerous improvements to documentation. + +And several bug fixes: +* Fix paintutils.drawLine incorrectly sorting coordinates (lilyzeiset). +* Improve JEI's handling of turtle/pocket upgrade recipes. +* Correctly handle sparse arrays in cc.pretty. +* Fix crashes when a turtle places a monitor (baeuric). +* Fix very large resource files being considered empty. +* Allow turtles to use compostors. +* Fix dupe bug when colouring turtles. + +# New features in CC: Tweaked 1.95.3 + +Several bug fixes: +* Correctly serialise sparse arrays into JSON (livegamer999) +* Fix hasAudio/playAudio failing on record discs. +* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster) +* Programs run via edit are now a little better behaved (Wojbie) +* Add User-Agent to a websocket's headers. + +# New features in CC: Tweaked 1.95.2 + +* Add `isReadOnly` to `fs.attributes` (Lupus590) +* Many more programs now support numpad enter (Wojbie) + +Several bug fixes: +* Fix some commands failing to parse on dedicated servers. +* Fix all disk recipes appearing to produce a white disk in JEI/recipe book. +* Hopefully improve edit's behaviour with AltGr on some European keyboards. +* Prevent files being usable after their mount was removed. +* Fix the `id` program crashing on non-disk items (Wojbie). +* Preserve registration order of turtle/pocket upgrades when displaying in JEI. + +# New features in CC: Tweaked 1.95.1 + +Several bug fixes: +* Command computers now drop items again. +* Restore crafting of disks with dyes. +* Fix CraftTweaker integrations for damageable items. +* Catch reflection errors in the generic peripheral system, resolving crashes with Botania. + +# New features in CC: Tweaked 1.95.0 + +* Optimise the paint program's initial render. +* Several documentation improvements (Gibbo3771, MCJack123). +* `fs.combine` now accepts multiple arguments. +* Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590). +* Add an improved help viewer which allows scrolling up and down (MCJack123). +* Add `cc.strings` module, with utilities for wrapping text (Lupus590). +* The `clear` program now allows resetting the palette too (Luca0208). + +And several bug fixes: +* Fix memory leak in generic peripherals. +* Fix crash when a turtle is broken while being ticked. +* `textutils.*tabulate` now accepts strings _or_ numbers. +* We now deny _all_ local IPs, using the magic `$private` host. Previously the IPv6 loopback interface was not blocked. +* Fix crash when rendering monitors if the block has not yet been synced. You will need to regenerate the config file to apply this change. +* `read` now supports numpad enter (TheWireLord) +* Correctly handle HTTP redirects to URLs containing escape characters. +* Fix integer overflow in `os.epoch`. +* Allow using pickaxes (and other items) for turtle upgrades which have mod-specific NBT. +* Fix duplicate turtle/pocket upgrade recipes appearing in JEI. + +# New features in CC: Tweaked 1.94.0 + +* Add getter for window visibility (devomaa) +* Generic peripherals are no longer experimental, and on by default. +* Use term.blit to draw boxes in paintutils (Lemmmy). + +And several bug fixes: +* Fix turtles not getting advancements when turtles are on. +* Draw in-hand pocket computers with the correct transparent flags enabled. +* Several bug fixes to SNBT parsing. +* Fix several programs using their original name instead of aliases in usage hints (Lupus590). + +# New features in CC: Tweaked 1.93.1 + +* Various documentation improvements (Lemmmy). +* Fix TBO monitor renderer on some older graphics cards (Lemmmy). + +# New features in CC: Tweaked 1.93.0 + +* Update Swedish translations (Granddave). +* Printers use item tags to check dyes. +* HTTP rules may now be targeted for a specific port. +* Don't propagate adjacent redstone signals through computers. + +And several bug fixes: +* Fix NPEs when turtles interact with containers. + +# New features in CC: Tweaked 1.92.0 + +* Bump Cobalt version: + * Add support for the __pairs metamethod. + * string.format now uses the __tostring metamethod. +* Add date-specific MOTDs (MCJack123). + +And several bug fixes: +* Correctly handle tabs within textutils.unserailizeJSON. +* Fix sheep not dropping items when sheared by turtles. + +# New features in CC: Tweaked 1.91.1 + +* Fix crash when turtles interact with an entity. + +# New features in CC: Tweaked 1.91.0 + +* [Generic peripherals] Expose NBT hashes of items to inventory methods. +* Bump Cobalt version: + * Optimise handling of string concatenation. + * Add string.{pack,unpack,packsize} (MCJack123) +* Update to 1.16.2 + +And several bug fixes: +* Escape non-ASCII characters in JSON strings (neumond) +* Make field names in fs.attributes more consistent (abby) +* Fix textutils.formatTime correctly handle 12 AM (R93950X) +* Fix turtles placing buckets multiple times. + +# New features in CC: Tweaked 1.90.3 + +* Fix the selected slot indicator missing from the turtle GUI. +* Ensure we load/save computer data from the world directory, rather than a global one. + +# New features in CC: Tweaked 1.90.2 + +* Fix generic peripherals not being registered outside a dev environment. +* Fix `turtle.attack()` failing. +* Correctly set styles for the output of `/computercraft` commands. + +# New features in CC: Tweaked 1.90.1 + +* Update to Forge 32.0.69 + +# New features in CC: Tweaked 1.90.0 + +* Add cc.image.nft module, for working with nft files. (JakobDev) +* [experimental] Provide a generic peripheral for any tile entity without an existing one. We currently provide methods for working with inventories, fluid tanks and energy storage. This is disabled by default, and must be turned on in the config. +* Add configuration to control the sizes of monitors and terminals. +* Add configuration to control maximum render distance of monitors. +* Allow getting "detailed" information about an item, using `turtle.getItemDetail(slot, true)`. This will contain the same information that the generic peripheral supplies. + +And several bug fixes: +* Add back config for allowing interacting with command computers. +* Fix write method missing from printers. +* Fix dupe bug when killing an entity with a turtle. +* Correctly supply port in the Host header (neumond). +* Fix `turtle.craft` failing when missing an argument. +* Fix deadlock when mistakenly "watching" an unloaded chunk. +* Fix full path of files being leaked in some errors. + +# New features in CC: Tweaked 1.89.1 + +* Fix crashes when rendering monitors of varying sizes. + +# New features in CC: Tweaked 1.89.0 + +* Compress monitor data, reducing network traffic by a significant amount. +* Allow limiting the bandwidth monitor updates use. +* Several optimisations to monitor rendering (@Lignum). +* Expose block and item tags to turtle.inspect and turtle.getItemDetail. + +And several bug fixes: +* Fix settings.load failing on defined settings. +* Fix name of the `ejectDisk` peripheral method. + +# New features in CC: Tweaked 1.88.1 + +* Fix error on objects with too many methods. + +# New features in CC: Tweaked 1.88.0 + +* Computers and turtles now preserve their ID when broken. +* Add `peripheral.getName` - returns the name of a wrapped peripheral. +* Reduce network overhead of monitors and terminals. +* Add a TBO backend for monitors, with a significant performance boost. +* The Lua REPL warns when declaring locals (lupus590, exerro) +* Add config to allow using command computers in survival. +* Add fs.isDriveRoot - checks if a path is the root of a drive. +* `cc.pretty` can now display a function's arguments and where it was defined. The Lua REPL will show arguments by default. +* Move the shell's `require`/`package` implementation to a separate `cc.require` module. +* Move treasure programs into a separate external data pack. + +And several bug fixes: +* Fix io.lines not accepting arguments. +* Fix settings.load using an unknown global (MCJack123). +* Prevent computers scanning peripherals twice. + +# New features in CC: Tweaked 1.87.1 + +* Fix blocks not dropping items in survival. + +# New features in CC: Tweaked 1.87.0 + +* Add documentation to many Lua functions. This is published online at https://tweaked.cc/. +* Replace to pretty-printer in the Lua REPL. It now supports displaying functions and recursive tables. This printer is may be used within your own code through the `cc.pretty` module. +* Add `fs.getCapacity`. A complement to `fs.getFreeSpace`, this returns the capacity of the supplied drive. +* Add `fs.getAttributes`. This provides file size and type, as well as creation and modification time. +* Update Cobalt version. This backports several features from Lua 5.2 and 5.3: + - The `__len` metamethod may now be used by tables. + - Add `\z`, hexadecimal (`\x00`) and unicode (`\u0000`) string escape codes. + - Add `utf8` lib. + - Mirror Lua's behaviour of tail calls more closely. Native functions are no longer tail called, and tail calls are displayed in the stack trace. + - `table.unpack` now uses `__len` and `__index` metamethods. + - Parser errors now include the token where the error occurred. +* Add `textutils.unserializeJSON`. This can be used to decode standard JSON and stringified-NBT. +* The `settings` API now allows "defining" settings. This allows settings to specify a default value and description. +* Enable the motd on non-pocket computers. +* Allow using the menu with the mouse in edit and paint (JakobDev). +* Add Danish and Korean translations (ChristianLW, mindy15963) +* Fire `mouse_up` events in the monitor program. +* Allow specifying a timeout to `websocket.receive`. +* Increase the maximum limit for websocket messages. +* Optimise capacity checking of computer/disk folders. + +And several bug fixes: +* Fix turtle texture being incorrectly oriented (magiczocker10). +* Prevent copying folders into themselves. +* Normalise file paths within shell.setDir (JakobDev) +* Fix turtles treating waterlogged blocks as water. +* Register an entity renderer for the turtle's fake player. + +# New features in CC: Tweaked 1.86.2 + +* Fix peripheral.getMethods returning an empty table. +* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing missing features and may be unstable. + +# New features in CC: Tweaked 1.86.1 + +* Add a help message to the Lua REPL's exit function. +* Add more MOTD messages. (osmarks) +* GPS requests are now made anonymously (osmarks) +* Minor memory usage improvements to Cobalt VM. + +And several bug fixes: +* Fix error when calling `write` with a number. +* Add missing assertion to `io.write`. +* Fix incorrect coordinates in `mouse_scroll` events. + +# New features in CC: Tweaked 1.86.0 + +* Add PATCH and TRACE HTTP methods. (jaredallard) +* Add more MOTD messages. (JakobDev) +* Allow removing and adding turtle upgrades via CraftTweaker. + +And several bug fixes: +* Fix crash when interacting with Wearable Backpacks. + +# New features in CC: Tweaked 1.85.2 + +* Fix crashes when using the mouse with advanced computers. + +# New features in CC: Tweaked 1.85.1 + +* Add basic mouse support to `read` + +And several bug fixes: +* Fix turtles not having breaking particles. +* Correct rendering of monitors when underwater. +* Adjust the position from where turtle performs actions, correcting the behaviour of some interactions. +* Fix several crashes when the turtle performs some action. + +# New features in CC: Tweaked 1.85.0 + +* Window.reposition now allows changing the redirect buffer +* Add cc.completion and cc.shell.completion modules +* command.exec also returns the number of affected objects, when exposed by the game. + +And several bug fixes: +* Change how turtle mining drops are handled, improving compatibility with some mods. +* Fix several GUI desyncs after a turtle moves. +* Fix os.day/os.time using the incorrect world time. +* Prevent wired modems dropping incorrectly. +* Fix mouse events not firing within the computer GUI. + +# New features in CC: Tweaked 1.84.1 + +* Update to latest Forge + +# New features in CC: Tweaked 1.84.0 + +* Improve validation in rename, copy and delete programs +* Add window.getLine - the inverse of blit +* turtle.refuel no longer consumes more fuel than needed +* Add "cc.expect" module, for improved argument type checks +* Mount the ROM from all mod jars, not just CC's + +And several bug fixes: +* Ensure file error messages use the absolute correct path +* Fix NPE when closing a file multiple times. +* Do not load chunks when calling writeDescription. +* Fix the signature of loadfile +* Fix turtles harvesting blocks multiple times +* Improve thread-safety of various peripherals +* Prevent printed pages having massive/malformed titles + +# New features in CC: Tweaked 1.83.1 + +* Add several new MOTD messages (JakobDev) + +And several bug fixes: +* Fix type check in `rednet.lookup` +* Error if turtle and pocket computer programs are run on the wrong system (JakobDev) +* Do not discard varargs after a nil. + +# New features in CC: Tweaked 1.83.0 + +* Add Chinese translation (XuyuEre) +* Small performance optimisations for packet sending. +* Provide an `arg` table to programs fun from the shell, similar to PUC Lua. +* Add `os.date`, and handle passing datetime tables to `os.time`, making them largely compatible with PUC Lua. +* `rm` and `mkdir` accept multiple arguments (hydraz, JakobDev). +* Rework rendering of in-hand pocket computers. +* Prevent rendering of a bounding box on a monitor's screen. +* Refactor Lua-side type checking code into a single method. Also include the function name in error messages. + +And several bug fixes: +* Fix incorrect computation of server-tick budget. +* Fix list-based config options not reloading. +* Ensure `require` is usable within the Lua REPL. + +# New features in CC: Tweaked 1.82.3 + +* Make computers' redstone input handling consistent with repeaters. Redstone inputs parallel to the computer will now be picked up. + +And several bug fixes: +* Fix `turtle.compare*()` crashing the server. +* Fix Cobalt leaking threads when coroutines blocked on Java methods are discarded. +* Fix `rawset` allowing nan keys +* Fix several out-of-bounds exceptions when handling malformed patterns. + +# New features in CC: Tweaked 1.82.2 + +* Don't tie `turtle.refuel`/the `refuel` script's limits to item stack sizes + +And several bug fixes: +* Fix changes to Project:Red inputs not being detected. +* Convert non-modem peripherals to multiparts too, fixing crash with Proportional Destruction Particles +* Remove a couple of over-eager error messages +* Fix wired modems not correctly saving their attached peripherals + +# New features in CC: Tweaked 1.82.1 + +* Make redstone updates identical to vanilla behaviour +* Update German translation + +# New features in CC: Tweaked 1.82.0 + +* Warn when `pastebin put` potentially triggers spam protection (Lemmmy) +* Display HTTP errors on pastebin requests (Lemmmy) +* Attach peripherals on the main thread, rather than deferring to the computer thread. +* Computers may now be preemptively interrupted if they run for too long. This reduces the risk of malicious or badly written programs making other computers unusable. +* Reduce overhead of running with a higher number of computer threads. +* Set the initial multishell tab when starting the computer. This fixes the issue where you would not see any content until the first yield. +* Allow running `pastebin get|url` with the URL instead (e.g. `pastebin run https://pastebin.com/LYAxmSby`). +* Make `os.time`/`os.day` case insensitive. +* Add translations for several languages: Brazilian Portuguese (zardyh), Swedish (nothjarnan), Italian (Ale32bit), French(absolument), German (Wilma456), Spanish (daelvn) +* Improve JEI integration for turtle/pocket computer upgrades. You can now see recipes and usages of any upgrade or upgrade combination. +* Associate turtle/pocket computer upgrades with the mod which registered them. For instance, a "Sensing Turtle" will now be labelled as belonging to Plethora. +* Fire `key_up` and `mouse_up` events when closing the GUI. +* Allow limiting the amount of server time computers can consume. +* Add several new events for turtle refuelling and item inspection. Should allow for greater flexibility in add on mods in the future. +* `rednet.send` returns if the message was sent. Restores behaviour present before CC 1.6 (Luca0208) +* Add MCMP integration for wireless and ender modems. +* Make turtle crafting more consistent with vanilla behaviour. +* `commands.getBlockInfo(s)` now also includes NBT. +* Turtles will no longer reset their label when clicked with an unnamed name tag. + +And several bug fixes: +* Update Cobalt (fixes `load` not unwind the stack) +* Fix `commands.collapseArgs` appending a trailing space. +* Fix leaking file descriptors when closing (see [this JVM bug!](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8220477)) +* Fix NPE on some invalid URLs +* Fix pocket computer API working outside of the player inventory +* Fix printing not updating the output display state. + +# New features in CC: Tweaked 1.81.1 + +* Fix colour.*RGB using 8-bit values, rather than 0-1 floats. + +# New features in CC: Tweaked 1.81.0 + +* Handle connection errors on websockets (Devilholk) +* Make `require` a little more consistent with PUC Lua, passing the required name to modules and improving error messages. +* Track how long each turtle action takes within the profiling tools +* Bump Cobalt version + * Coroutines are no longer backed by threads, reducing overhead of coroutines. + * Maximum stack depth is much larger (2^16 rather than 2^8) + * Stack is no longer unwound when an unhandled error occurs, meaning `debug.traceback` can be used on dead coroutines. +* Reduce jar size by reducing how many extra files we bundle. +* Add `term.nativePaletteColo(u)r` (Lignum) +* Split `colours.rgb8` into `colours.packRGB` and `colours.unpackRGB` (Lignum) +* Printers now only accept paper and ink, rather than any item +* Allow scrolling through the multishell tab bar, when lots of programs are running. (Wilma456) + +And several bug fixes: +* Fix modems not being advanced when they should be +* Fix direction of some peripheral blocks not being set +* Strip `\r` from `.readLine` on binary handles. +* Websockets handle pings correctly +* Fix turtle peripherals becoming desynced on chunk unload. +* `/computercraft` table are now truncated correctly. + +# New features in CC: Tweaked 1.80pr1.14 + +* Allow seeking within ROM files. +* Fix not being able to craft upgraded turtles or pocket computers when Astral Sorcery was installed. +* Make several tile entities (modems, cables, and monitors) non-ticking, substantially reducing their overhead, + +And several bug fixes: +* Fix cables not rendering the breaking steps +* Try to prevent `/computercraft_copy` showing up in auto-complete. +* Fix several memory leaks and other issues with ROM mounts. + +# New features in CC: Tweaked 1.80pr1.13 + +* `websocket_message` and `.receive` now return whether a message was binary or not. +* `websocket_close` events may contain a status code and reason the socket was closed. +* Enable the `debug` library by default. +* Clean up configuration files, moving various properties into sub-categories. +* Rewrite the HTTP API to use Netty. +* HTTP requests may now redirect from http to https if the server requests it. +* Add config options to limit various parts of the HTTP API: + * Restrict the number of active http requests and websockets. + * Limit the size of HTTP requests and responses. + * Introduce a configurable timeout +* `.getResponseCode` also returns the status text associated with that status code. + +And several bug fixes: +* Fix being unable to create resource mounts from individual files. +* Sync computer state using TE data instead. +* Fix `.read` always consuming a multiple of 8192 bytes for binary handles. + +# New features in CC: Tweaked 1.80pr1.12 + +* Using longs inside `.seek` rather than 32 bit integers. This allows you to seek in very large files. +* Move the `/computer` command into the main `/computercraft` command +* Allow copying peripheral names from a wired modem's attach/detach chat message. + +And several bug fixes +* Fix `InventoryUtil` ignoring the stack limit when extracting items +* Fix computers not receiving redstone inputs sent through another block. +* Fix JEI responding to key-presses when within a computer or turtle's inventory. + +# New features in CC: Tweaked 1.80pr1.11 + +* Rename all tile entities to have the correct `computercraft:` prefix. +* Fix files not being truncated when opened for a write. +* `.read*` methods no longer fail on malformed unicode. Malformed input is replaced with a fake character. +* Fix numerous issues with wireless modems being attached to wired ones. +* Prevent deadlocks within the wireless modem code. +* Create coroutines using a thread pool, rather than creating a new thread each time. Should make short-lived coroutines (such as iterators) much more performance friendly. +* Create all CC threads under appropriately named thread groups. + +# New features in CC: Tweaked 1.80pr1.10 + +This is just a minor bugfix release to solve some issues with the filesystem rewrite +* Fix computers not loading if resource packs are enabled +* Fix stdin not being recognised as a usable input +* Return an unsigned byte rather than a signed one for no-args `.read()` + +# New features in CC: Tweaked 1.80pr1.9 + +* Add German translation (Vexatos) +* Add `.getCursorBlink` to monitors and terminals. +* Allow sending binary messages with websockets. +* Extend `fs` and `io` APIs + * `io` should now be largely compatible with PUC Lua's implementation (`:read("n")` is not currently supported). + * Binary readable file handles now support `.readLine` + * Binary file handles now support `.seek(whence: string[, position:number])`, taking the same arguments as PUC Lua's method. + +And several bug fixes: +* Fix `repeat` program crashing when malformed rednet packets are received (gollark/osmarks) +* Reduce risk of deadlock when calling peripheral methods. +* Fix speakers being unable to play sounds. + +# New features in CC: Tweaked 1.80pr1.8 + +* Bump Cobalt version + * Default to using little endian in string.dump + * Remove propagation of debug hooks to child coroutines + * Allow passing functions to `debug.getlocal`, al-la Lua 5.2 +* Add Charset support for bundled cables +* `/computercraft` commands are more generous in allowing computer selectors to fail. +* Remove bytecode loading disabling from bios.lua. + +And several bug fixes: +* Fix stack overflow when using `turtle.place` with a full inventory +* Fix in-hand printout rendering causing visual glitches. + +# New features in CC: Tweaked 1.80pr1.7 + + * Add `.getNameLocal` to wired modems: provides the name that computer is exposed as on the network. This is mostly useful for working with Plethora's transfer locations, though could have other purposes. + * Change turtle block breaking to closer conform to how players break blocks. + * Rewrite rendering of printed pages, allowing them to be held in hand, and placed in item frames. + +And several bug fixes: + * Improve formatting of `/computercraft` when run by a non-player. + * Fix pocket computer terminals not updating when being held. + * Fix a couple of minor blemishes in the GUI textures. + * Fix sign text not always being set when placed. + * Cache turtle fakeplayer, hopefully proving some minor performance improvements. + +# New features in CC: Tweaked 1.80pr1.6 + +* Allow network cables to work with compact machines. +* A large number of improvements to the `/computercraft` command, including: + * Ensure the tables are correctly aligned + * Remove the output of the previous invocation of that command when posting to chat. + * `/computercraft track` is now per-user, instead of global. + * We now track additional fields, such as the number of peripheral calls, http requests, etc... You can specify these as an optional argument to `/computercraft track dump` to see them. +* `wget` automatically determines the filename (Luca0208) +* Allow using alternative HTTP request methods (`DELETE`, `PUT`, etc...) +* Enable Gzip compression for websockets. +* Fix monitors not rendering when optifine shaders are enabled. There are still issues (they are tinted orange during the night), but it is an improvement. + +And several bug fixes: +* Fix `.isDiskPresent()` always returning true. +* Fix peripherals showing up on wired networks when they shouldn't be. +* Fix `turtle.place()` crashing the server in some esoteric conditions. +* Remove upper bound on the number of characters than can be read with `.read(n: number)`. +* Fix various typos in `keys.lua` (hugeblank) + +# New features in CC: Tweaked 1.80pr1.5 + +* Several additional fixes to monitors, solving several crashes and graphical glitches. +* Add recipes to upgrade computers, turtles and pocket computers. + +# New features in CC: Tweaked 1.80pr1.4 + +* Verify the action can be completed in `copy`, `rename` and `mkdir` commands. +* Add `/rom/modules` so the package path. +* Add `read` to normal file handles - allowing reading a given number of characters. +* Various minor bug fixes. +* Ensure ComputerCraft peripherals are thread-safe. This fixes multiple Lua errors and crashes with modems monitors. +* Add `/computercraft track` command, for monitoring how long computers execute for. +* Add ore dictionary support for recipes. +* Track which player owns a turtle. This allows turtles to play nicely with various claim/grief prevention systems. +* Add config option to disable various turtle actions. +* Add an API for extending wired networks. +* Add full-block wired modems. +* Several minor bug fixes. + +# New features in CC: Tweaked 1.80pr1.3 + +* Add `/computercraft` command, providing various diagnostic tools. +* Make `http.websocket` synchronous and add `http.websocketAsync`. +* Restore binary compatibility for `ILuaAPI`. + +# New features in CC: Tweaked 1.80pr1.2 + +* Fix `term.getTextScale()` not working across multiple monitors. +* Fix computer state not being synced to client when turning on/off. +* Provide an API for registering custom APIs. +* Render turtles called "Dinnerbone" or "Grumm" upside*down. +* Fix `getCollisionBoundingBox` not using all AABBs. +* **Experimental:** Add map-like rendering for pocket computers. + +# New features in CC: Tweaked 1.80pr1.1 + +* Large numbers of bug fixes, stabilisation and hardening. +* Replace LuaJ with Cobalt. +* Allow running multiple computers at the same time. +* Add config option to enable Lua's debug API. +* Add websocket support to HTTP library. +* Add `/computer` command, allowing one to queue events on command computers. +* Fix JEI's handling of various ComputerCraft items. +* Make wired cables act more like multiparts. +* Add turtle and pocket recipes to recipe book. +* Flash pocket computer's light when playing a note. + +# New Features in ComputerCraft 1.80pr1: + +* Update to Minecraft 1.12.2 +* Large number of bug fixes and stabilisation. +* Allow loading bios.lua files from resource packs. +* Fix texture artefacts when rendering monitors. +* Improve HTTP whitelist functionality and add an optional blacklist. +* Add support for completing Lua's self calls (`foo:bar()`). +* Add binary mode to HTTP. +* Use file extensions for ROM files. +* Automatically add `.lua` when editing files, and handle running them in the shell. +* Add require to the shell environment. +* Allow startup to be a directory. +* Add speaker peripheral and corresponding turtle and pocket upgrades. +* Add pocket computer upgrades. +* Allow turtles and pocket computers to be dyed any colour. +* Allow computer and monitors to configure their palette. Also allow normal computer/monitors to use any colour converting it to greyscale. +* Add extensible pocket computer upgrade system, including ender modem upgrade. +* Add config option to limit the number of open files on a computer. +* Monitors glow in the dark. +* http_failure event includes the HTTP handle if available. +* HTTP responses include the response headers. + +# New Features in ComputerCraft 1.79: + +* Ported ComputerCraftEdu to Minecraft 1.8.9 +* Fixed a handful of bugs in ComputerCraft + +# New Features in ComputerCraft 1.77: + +* Ported to Minecraft 1.8.9 +* Added `settings` API +* Added `set` and `wget` programs +* Added settings to disable multishell, startup scripts, and tab completion on a per-computer basis. The default values for these settings can be customised in ComputerCraft.cfg +* All Computer and Turtle items except Command Computers can now be mounted in Disk Drives + +# New Features in ComputerCraft 1.76: + +* Ported to Minecraft 1.8 +* Added Ender Modems for cross-dimensional communication +* Fixed handling of 8-bit characters. All the characters in the ISO 8859-1 codepage can now be displayed +* Added some extra graphical characters in the unused character positions, including a suite of characters for Teletext style drawing +* Added support for the new commands in Minecraft 1.8 to the Command Computer +* The return values of `turtle.inspect()` and `commands.getBlockInfo()` now include blockstate information +* Added `commands.getBlockInfos()` function for Command Computers +* Added new `peripherals` program +* Replaced the `_CC_VERSION` and `_MC_VERSION` constants with a new `_HOST` constant +* Shortened the length of time that "Ctrl+T", "Ctrl+S" and "Ctrl+R" must be held down for to terminate, shutdown and reboot the computer +* `textutils.serialiseJSON()` now takes an optional parameter allowing it to produce JSON text with unquoted object keys. This is used by all autogenerated methods in the `commands` api except for "title" and "tellraw" +* Fixed many bugs + +# New Features in ComputerCraft 1.75: + +* Fixed monitors sometimes rendering without part of their text. +* Fixed a regression in the `bit` API. + +# New Features in ComputerCraft 1.74: + +* Added tab completion to `edit`, `lua` and the shell. +* Added `textutils.complete()`, `fs.complete()`, `shell.complete()`, `shell.setCompletionFunction()` and `help.complete()`. +* Added tab completion options to `read()`. +* Added `key_up` and `mouse_up` events. +* Non-advanced terminals now accept both grey colours. +* Added `term.getTextColour()`, `term.getBackgroundColour()` and `term.blit()`. +* Improved the performance of text rendering on Advanced Computers. +* Added a "Run" button to the edit program on Advanced Computers. +* Turtles can now push players and entities (configurable). +* Turtles now respect server spawn protection (configurable). +* Added a turtle permissions API for mod authors. +* Implemented a subset of the Lua 5.2 API so programs can be written against it now, ahead of a future Lua version upgrade. +* Added a config option to disable parts of the Lua 5.1 API which will be removed when a future Lua version upgrade happens. +* Command Computers can no longer be broken by survival players. +* Fixed the "pick block" key not working on ComputerCraft items in creative mode. +* Fixed the `edit` program being hard to use on certain European keyboards. +* Added `_CC_VERSION` and `_MC_VERSION` constants. + +# New Features in ComputerCraft 1.73: + +* The `exec` program, `commands.exec()` and all related Command Computer functions now return the console output of the command. +* Fixed two multiplayer crash bugs. + +# New Features in ComputerCraft 1.7: + +* Added Command Computers +* Added new API: `commands` +* Added new programs: `commands`, `exec` +* Added `textutils.serializeJSON()` +* Added `ILuaContext.executeMainThreadTask()` for peripheral developers +* Disk Drives and Printers can now be renamed with Anvils +* Fixed various bugs, crashes and exploits +* Fixed problems with HD texture packs +* Documented the new features in the in-game help + +# New Features in ComputerCraft 1.65: + +* Fixed a multiplayer-only crash with `turtle.place()` +* Fixed some problems with `http.post()` +* Fixed `fs.getDrive()` returning incorrect results on remote peripherals + +# New Features in ComputerCraft 1.64: + +* Ported to Minecraft 1.7.10 +* New turtle functions: `turtle.inspect()`, `turtle.inspectUp()`, `turtle.inspectDown()`, `turtle.getItemDetail()` +* Lots of bug and crash fixes, a huge stability improvement over previous versions + +# New Features in ComputerCraft 1.63: + +* Turtles can now be painted with dyes, and cleaned with water buckets +* Added a new game: Redirection - ComputerCraft Edition +* Turtle label nameplates now only show when the Turtle is moused-over +* The HTTP API is now enabled by default, and can be configured with a whitelist of permitted domains +* `http.get()` and `http.post()` now accept parameters to control the request headers +* New fs function: `fs.getDir( path )` +* Fixed some bugs + +# New Features in ComputerCraft 1.62: + +* Added IRC-style commands to the `chat` program +* Fixed some bugs and crashes + +# New Features in ComputerCraft 1.6: + +* Added Pocket Computers +* Added a multi-tasking system for Advanced Computers and Turtles +* Turtles can now swap out their tools and peripherals at runtime +* Turtles can now carry two tools or peripherals at once in any combination +* Turtles and Computers can now be labelled using Name Tags and Anvils +* Added a configurable fuel limit for Turtles +* Added hostnames, protocols and long distance routing to the rednet API +* Added a peer-to-peer chat program to demonstrate new rednet capabilities +* Added a new game, only on Pocket Computers: "falling" by GopherATL +* File system commands in the shell now accept wildcard arguments +* The shell now accepts long arguments in quotes +* Terminal redirection now no longer uses a stack-based system. Instead: `term.current()` gets the current terminal object and `term.redirect()` replaces it. `term.restore()` has been removed. +* Added a new Windowing API for addressing sub-areas of the terminal +* New programs: `fg`, `bg`, `multishell`, `chat`, `repeat`, `redstone`, `equip`, `unequip` +* Improved programs: `copy`, `move`, `delete`, `rename`, `paint`, `shell` +* Removed programs: `redset`, `redprobe`, `redpulse` +* New APIs: `window`, `multishell` +* New turtle functions: `turtle.equipLeft()` and `turtle.equipRight()` +* New peripheral functions: `peripheral.find( [type] )` +* New rednet functions: `rednet.host( protocol, hostname )`, `rednet.unhost( protocol )`, `rednet.locate( protocol, [hostname] )` +* New fs function: `fs.find( wildcard )` +* New shell functions: `shell.openTab()`, `shell.switchTab( [number] )` +* New event `term_resize` fired when the size of a terminal changes +* Improved rednet functions: `rednet.send()`, `rednet.broadcast()` and `rednet.receive()`now take optional protocol parameters +* `turtle.craft(0)` and `turtle.refuel(0)` now return true if there is a valid recipe or fuel item, but do not craft of refuel anything +* `turtle.suck( [limit] )` can now be used to limit the number of items picked up +* Users of `turtle.dig()` and `turtle.attack()` can now specify which side of the turtle to look for a tool to use (by default, both will be considered) +* `textutils.serialise( text )` now produces human-readable output +* Refactored most of the codebase and fixed many old bugs and instabilities, turtles should never ever lose their content now +* Fixed the `turtle_inventory` event firing when it shouldn't have +* Added error messages to many more turtle functions after they return false +* Documented all new programs and API changes in the `help` system + +# New Features in ComputerCraft 1.58: + +* Fixed a long standing bug where turtles could lose their identify if they travel too far away +* Fixed use of deprecated code, ensuring mod compatibility with the latest versions of Minecraft Forge, and world compatibility with future versions of Minecraft + +# New Features in ComputerCraft 1.57: + +* Ported to Minecraft 1.6.4 +* Added two new Treasure Disks: Conway's Game of Life by vilsol and Protector by fredthead +* Fixed a very nasty item duplication bug + +# New Features in ComputerCraft 1.56: + +* Added Treasure Disks: Floppy Disks in dungeons which contain interesting community made programs. Find them all! +* All turtle functions now return additional error messages when they fail. +* Resource Packs with Lua Programs can now be edited when extracted to a folder, for easier editing. + +# New Features in ComputerCraft 1.55: + +* Ported to Minecraft 1.6.2 +* Added Advanced Turtles +* Added `turtle_inventory` event. Fires when any change is made to the inventory of a turtle +* Added missing functions `io.close`, `io.flush`, `io.input`, `io.lines`, `io.output` +* Tweaked the screen colours used by Advanced Computers, Monitors and Turtles +* Added new features for Peripheral authors +* Lua programs can now be included in Resource Packs + +# New Features in ComputerCraft 1.52: + +* Ported to Minecraft 1.5.1 + +# New Features in ComputerCraft 1.51: + +* Ported to Minecraft 1.5 +* Added Wired Modems +* Added Networking Cables +* Made Wireless Modems more expensive to craft +* New redstone API functions: `getAnalogInput()`, `setAnalogOutput()`, `getAnalogOutput()` +* Peripherals can now be controlled remotely over wired networks. New peripheral API function: `getNames()` +* New event: `monitor_resize` when the size of a monitor changes +* Except for labelled computers and turtles, ComputerCraft blocks no longer drop items in creative mode +* The pick block function works in creative mode now works for all ComputerCraft blocks +* All blocks and items now use the IDs numbers assigned by FTB by default +* Fixed turtles sometimes placing blocks with incorrect orientations +* Fixed Wireless modems being able to send messages to themselves +* Fixed `turtle.attack()` having a very short range +* Various bugfixes + +# New Features in ComputerCraft 1.5: + +* Redesigned Wireless Modems; they can now send and receive on multiple channels, independent of the computer ID. To use these features, interface with modem peripherals directly. The rednet API still functions as before +* Floppy Disks can now be dyed with multiple dyes, just like armour +* The `excavate` program now retains fuel in it's inventory, so can run unattended +* `turtle.place()` now tries all possible block orientations before failing +* `turtle.refuel(0)` returns true if a fuel item is selected +* `turtle.craft(0)` returns true if the inventory is a valid recipe +* The in-game help system now has documentation for all the peripherals and their methods, including the new modem functionality +* A romantic surprise + +# New Features in ComputerCraft 1.48: + +* Ported to Minecraft 1.4.6 +* Advanced Monitors now emit a `monitor_touch` event when right clicked +* Advanced Monitors are now cheaper to craft +* Turtles now get slightly less fuel from items +* Computers can now interact with Command Blocks (if enabled in ComputerCraft.cfg) +* New API function: `os.day()` +* A christmas surprise + +# New Features in ComputerCraft 1.45: + +* Added Advanced Computers +* Added Advanced Monitors +* New program: paint by nitrogenfingers +* New API: `paintutils` +* New term functions: `term.setBackgroundColor`, `term.setTextColor`, `term.isColor` +* New turtle function: `turtle.transferTo` + +# New Features in ComputerCraft 1.43: + +* Added Printed Pages +* Added Printed Books +* Fixed incompatibility with Forge 275 and above +* Labelled Turtles now keep their fuel when broken + +# New Features in ComputerCraft 1.42: + +* Ported to Minecraft 1.3.2 +* Added Printers +* Floppy Disks can be dyed different colours +* Wireless Crafty Turtles can now be crafted +* New textures +* New forge config file +* Bug fixes + +# New Features in ComputerCraft 1.4: + +* Ported to Forge Mod Loader. ComputerCraft can now be ran directly from the .zip without extraction +* Added Farming Turtles +* Added Felling Turtles +* Added Digging Turtles +* Added Melee Turtles +* Added Crafty Turtles +* Added 14 new Turtle Combinations accessible by combining the turtle upgrades above +* Labelled computers and turtles can now be crafted into turtles or other turtle types without losing their ID, label and data +* Added a "Turtle Upgrade API" for mod developers to create their own tools and peripherals for turtles +* Turtles can now attack entities with `turtle.attack()`, and collect their dropped items +* Turtles can now use `turtle.place()` with any item the player can, and can interact with entities +* Turtles can now craft items with `turtle.craft()` +* Turtles can now place items into inventories with `turtle.drop()` +* Changed the behaviour of `turtle.place()` and `turtle.drop()` to only consider the currently selected slot +* Turtles can now pick up items from the ground, or from inventories, with `turtle.suck()` +* Turtles can now compare items in their inventories +* Turtles can place signs with text on them with `turtle.place( [signText] )` +* Turtles now optionally require fuel items to move, and can refuel themselves +* The size of the the turtle inventory has been increased to 16 +* The size of the turtle screen has been increased +* New turtle functions: `turtle.compareTo( [slotNum] )`, `turtle.craft()`, `turtle.attack()`, `turtle.attackUp()`, `turtle.attackDown()`, `turtle.dropUp()`, `turtle.dropDown()`, `turtle.getFuelLevel()`, `turtle.refuel()` +* New disk function: disk.getID() +* New turtle programs: `craft`, `refuel` +* `excavate` program now much smarter: Will return items to a chest when full, attack mobs, and refuel itself automatically +* New API: `keys` +* Added optional Floppy Disk and Hard Drive space limits for computers and turtles +* New `fs` function: `fs.getFreeSpace( path )`, also `fs.getDrive()` works again +* The send and receive range of wireless modems now increases with altitude, allowing long range networking from high-altitude computers (great for GPS networks) +* `http.request()` now supports https:// URLs +* Right clicking a Disk Drive with a Floppy Disk or a Record when sneaking will insert the item into the Disk Drive automatically +* The default size of the computer screen has been increased +* Several stability and security fixes. LuaJ can now no longer leave dangling threads when a computer is unloaded, turtles can no longer be destroyed by tree leaves or walking off the edge of the loaded map. Computers no longer crash when used with RedPower frames. + +# New Features in ComputerCraft 1.31: + +* Ported to Minecraft 1.2.3 +* Added Monitors (thanks to Cloudy) +* Updated LuaJ to a newer, less memory hungry version +* `rednet_message` event now has a third parameter, "distance", to support position triangulation. +* New programs: `gps`, `monitor`, `pastebin`. +* Added a secret program. Use with large monitors! +* New apis: `gps`, `vector` +* New turtle functions: `turtle.compare()`, `turtle.compareUp()`, `turtle.compareDown()`, `turtle.drop( quantity )` +* New `http` functions: `http.post()`. +* New `term` functions: `term.redirect()`, `term.restore()` +* New `textutils` functions: `textutils.urlEncode()` +* New `rednet` functions: `rednet.isOpen()` +* New config options: modem_range, modem_rangeDuringStorm +* Bug fixes, program tweaks, and help updates + +# New Features in ComputerCraft 1.3: + +* Ported to Minecraft Forge +* Added Turtles +* Added Wireless Modems +* Added Mining Turtles +* Added Wireless Turtles +* Added Wireless Mining Turtles +* Computers and Disk Drives no longer get destroyed by water. +* Computers and Turtles can now be labelled with the label program, and labelled devices keep their state when destroyed. +* Computers/Turtles can connect to adjacent devices, and turn them on and off +* User programs now give line numbers in their error messages +* New APIs: `turtle`, `peripheral` +* New programs for turtles: tunnel, excavate, go, turn, dance +* New os functions: `os.getComputerLabel()`, `os.setComputerLabel()` +* Added "filter" parameter to `os.pullEvent()` +* New shell function: `shell.getCurrentProgram()` +* New textutils functions: `textutils.serialize()`, `textutils.unserialize()`, `textutils.tabulate()`, `textutils.pagedTabulate()`, `textutils.slowWrite()` +* New io file function: `file:lines()` +* New fs function: `fs.getSize()` +* Disk Drives can now play records from other mods +* Bug fixes, program tweaks, and help updates + +# New Features in ComputerCraft 1.2: + +* Added Disk Drives and Floppy Disks +* Added Ctrl+T shortcut to terminate the current program (hold) +* Added Ctrl+S shortcut to shutdown the computer (hold) +* Added Ctrl+R shortcut to reboot the computer (hold) +* New Programs: `alias`, `apis`, `copy`, `delete`, `dj`, `drive`, `eject`, `id`, `label`, `list`, `move`, `reboot`, `redset`, `rename`, `time`, `worm`. +* New APIs: `bit`, `colours`, `disk`, `help`, `rednet`, `parallel`, `textutils`. +* New color functions: `colors.combine()`, `colors.subtract()`, `colors.test()` +* New fs functions: `fs.getName()`, new modes for `fs.open()` +* New os functions: `os.loadAPI()`, `os.unloadAPI()`, `os.clock()`, `os.time()`, `os.setAlarm()`, `os.reboot()`, `os.queueEvent()` +* New redstone function: `redstone.getSides()` +* New shell functions: `shell.setPath()`, `shell.programs()`, `shell.resolveProgram()`, `shell.setAlias()` +* Lots of updates to the help pages +* Bug fixes + +# New Features in ComputerCraft 1.1: + +* Added Multiplayer support throughout. +* Added connectivity with RedPower bundled cables +* Added HTTP api, enabled via the mod config, to allow computers to access the real world internet +* Added command history to the shell. +* Programs which spin in an infinite loop without yielding will no longer freeze minecraft +* Help updates and bug fixes + +# New Features in ComputerCraft 1.0: + +* First Release! diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/chat.txt b/src/main/resources/assets/cctweaked/lua/rom/help/chat.txt new file mode 100644 index 000000000..035cb463d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/chat.txt @@ -0,0 +1,5 @@ +Surf the rednet superhighway with "chat", the networked chat program for CraftOS! Host chatrooms and invite your friends! Requires a Wired or Wireless Modem on each computer. When running chat, type "/help" to see a list of available commands. + +ex: +"chat host forgecraft" will create a chatroom with the name "forgecraft" +"chat join forgecraft direwolf20" will connect to the chatroom with the name "forgecraft", using the nickname "direwolf20" diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/clear.txt b/src/main/resources/assets/cctweaked/lua/rom/help/clear.txt new file mode 100644 index 000000000..037d3c8a5 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/clear.txt @@ -0,0 +1,6 @@ +clear clears the screen and/or resets the palette. +ex: +"clear" clears the screen, but keeps the palette. +"clear screen" does the same as "clear" +"clear palette" resets the palette, but doesn't clear the screen +"clear all" clears the screen and resets the palette diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/colors.txt b/src/main/resources/assets/cctweaked/lua/rom/help/colors.txt new file mode 100644 index 000000000..18580a31f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/colors.txt @@ -0,0 +1,9 @@ +Functions in the colors api +(used for redstone.setBundledOutput): +colors.combine( color1, color2, color3, ... ) +colors.subtract( colors, color1, color2, ... ) +colors.test( colors, color ) +colors.rgb8( r, g, b ) + +Color constants in the colors api, in ascending bit order: +colors.white, colors.orange, colors.magenta, colors.lightBlue, colors.yellow, colors.lime, colors.pink, colors.gray, colors.lightGray, colors.cyan, colors.purple, colors.blue, colors.brown, colors.green, colors.red, colors.black. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/colours.txt b/src/main/resources/assets/cctweaked/lua/rom/help/colours.txt new file mode 100644 index 000000000..ff7d88a92 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/colours.txt @@ -0,0 +1,9 @@ +Functions in the colours api +(used for redstone.setBundledOutput): +colours.combine( colour1, colour2, colour3, ...) +colours.subtract( colours, colour1, colour2, ...) +colours.test( colours, colour ) +colours.rgb8( r, g, b ) + +Colour constants in the colours api, in ascending bit order: +colours.white, colours.orange, colours.magenta, colours.lightBlue, colours.yellow, colours.lime, colours.pink, colours.grey, colours.lightGrey, colours.cyan, colours.purple, colours.blue, colours.brown, colours.green, colours.red, colours.black. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/commands.txt b/src/main/resources/assets/cctweaked/lua/rom/help/commands.txt new file mode 100644 index 000000000..6026a6ee2 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/commands.txt @@ -0,0 +1,2 @@ +On a Command Computer, "commands" will list all the commands available for use. Use "exec" to execute them. +Type "help commandsapi" for help using commands in lua programs. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/commandsapi.txt b/src/main/resources/assets/cctweaked/lua/rom/help/commandsapi.txt new file mode 100644 index 000000000..de8cffd51 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/commandsapi.txt @@ -0,0 +1,15 @@ +Functions in the commands API: +commands.exec( command ) +commands.execAsync( command ) +commands.list() +commands.getBlockPosition() +commands.getBlockInfo( x, y, z ) +commands.getBlockInfos( minx, miny, minz, maxx, maxy, maxz ) + +The commands API can also be used to invoke commands directly, like so: +commands.say( "Hello World" ) +commands.give( "dan200", "minecraft:diamond", 64 ) +This works with any command. Use "commands.async" instead of "commands" to execute asynchronously. + +The commands API is only available on Command Computers. +Visit http://minecraft.gamepedia.com/Commands for documentation on all commands. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/copy.txt b/src/main/resources/assets/cctweaked/lua/rom/help/copy.txt new file mode 100644 index 000000000..57b3187cb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/copy.txt @@ -0,0 +1,6 @@ +cp copies a file or directory from one location to another. + +ex: +"cp rom myrom" copies "rom" to "myrom". +"cp rom mystuff/rom" copies "rom" to "mystuff/rom". +"cp disk/* disk2" copies the contents of one disk to another diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/coroutine.txt b/src/main/resources/assets/cctweaked/lua/rom/help/coroutine.txt new file mode 100644 index 000000000..93d8f7b04 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/coroutine.txt @@ -0,0 +1,2 @@ +coroutine is a standard Lua5.1 API. +Refer to http://www.lua.org/manual/5.1/ for more information. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/craft.txt b/src/main/resources/assets/cctweaked/lua/rom/help/craft.txt new file mode 100644 index 000000000..9f0bd6cf5 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/craft.txt @@ -0,0 +1,5 @@ +craft is a program for Crafty Turtles. Craft will craft a stack of items using the current inventory. + +ex: +"craft all" will craft as many items as possible +"craft 5" will craft at most 5 times diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/credits.md b/src/main/resources/assets/cctweaked/lua/rom/help/credits.md new file mode 100644 index 000000000..515ac7490 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/credits.md @@ -0,0 +1,390 @@ +ComputerCraft was created by Daniel "dan200" Ratcliffe, with additional code by Aaron "Cloudy" Mills. +Thanks to nitrogenfingers, GopherATL and RamiLego for program contributions. +Thanks to Mojang, the Forge team, and the MCP team. +Uses LuaJ from http://luaj.sourceforge.net/ + +The ComputerCraft 1.76 update was sponsored by MinecraftU and Deep Space. +Visit http://www.minecraftu.org and http://www.deepspace.me/space-cadets to find out more. + +Join the ComputerCraft community online at https://computercraft.cc +Follow @DanTwoHundred on Twitter! + +To help contribute to CC: Tweaked, browse the source code at https://github.com/cc-tweaked/cc-tweaked. + +# GitHub +Numerous people have contributed to CC: Tweaked over the years: + +${gitContributors} + +Thank you to everyone who has contributed + +# Software Licenses +CC: Tweaked would not be possible without the work of other open source libraries. Their licenses are included below: + +## Apache 2.0 +CC: Tweaked contains code from the following Apache 2.0 licensed libraries: + + - Netty (https://github.com/netty/netty) + +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 + +## GNU Lesser General Public License 3.0 +CC: Tweaked contains code from the following LGPL 3.0 licensed libraries: + + - NightConfig (https://github.com/TheElectronWill/night-config/tree/master) + +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. + +"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + + a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license document. + +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + + a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license document. + + c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. + + e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) + +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## Cobalt (https://github.com/SquidDev/Cobalt) +The MIT License (MIT) + +Original Source: Copyright (c) 2009-2011 Luaj.org. All rights reserved. +Modifications: Copyright (c) 2015-2020 SquidDev + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## double-conversion (https://github.com/google/double-conversion/) +Copyright 2006-2012 the V8 project authors. All rights reserved. +Java Port Copyright 2021 sir-maniac. All Rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/dance.txt b/src/main/resources/assets/cctweaked/lua/rom/help/dance.txt new file mode 100644 index 000000000..cef419ab9 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/dance.txt @@ -0,0 +1 @@ +dance is a program for Turtles. Turtles love to get funky. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/delete.txt b/src/main/resources/assets/cctweaked/lua/rom/help/delete.txt new file mode 100644 index 000000000..f1c092749 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/delete.txt @@ -0,0 +1,5 @@ +rm deletes a file or a directory and its contents. + +ex: +"rm foo" will delete the file foo. +"rm disk/*" will delete the contents of a disk. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/disk.txt b/src/main/resources/assets/cctweaked/lua/rom/help/disk.txt new file mode 100644 index 000000000..3c1348e6b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/disk.txt @@ -0,0 +1,17 @@ +Functions in the disk API. These functions are for interacting with disk drives: +disk.isPresent( drive ) +disk.setLabel( drive, label ) +disk.getLabel( drive ) +disk.hasData( drive ) +disk.getMountPath( drive ) +disk.hasAudio( drive ) +disk.getAudioTitle( drive ) +disk.playAudio( drive ) +disk.stopAudio( ) +disk.eject( drive ) +disk.getID( drive ) + +Events fired by the disk API: +"disk" when a disk or other item is inserted into a disk drive. Argument is the name of the drive +"disk_eject" when a disk is removed from a disk drive. Argument is the name of the drive +Type "help events" to learn about the event system. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/dj.txt b/src/main/resources/assets/cctweaked/lua/rom/help/dj.txt new file mode 100644 index 000000000..c41dccc6e --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/dj.txt @@ -0,0 +1,6 @@ +dj plays Music Discs from disk drives attached to the computer. + +ex: +"dj" or "dj play" plays a random disc. +"dj play left" plays the disc in the drive on the left of the computer. +"dj stop" stops the current disc. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/drive.txt b/src/main/resources/assets/cctweaked/lua/rom/help/drive.txt new file mode 100644 index 000000000..eb6472a8e --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/drive.txt @@ -0,0 +1,5 @@ +drive tells you which disk drive the current or specified directory is located in. + +ex: +"drive" tell you the disk drive of the current directory. +"drive foo" tells you the disk drive of the subdirectory "foo" diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/drives.txt b/src/main/resources/assets/cctweaked/lua/rom/help/drives.txt new file mode 100644 index 000000000..b793f8e7d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/drives.txt @@ -0,0 +1,19 @@ +The Disk Drive is a peripheral device available for CraftOS. Type "help peripheral" to learn about using the Peripheral API to connect with peripherals. When a Disk Drive is connected, peripheral.getType() will return "drive". + +Methods exposed by the Disk Drive: +isDiskPresent() +getDiskLabel() +setDiskLabel( label ) +hasData() +getMountPath() +hasAudio() +getAudioTitle() +playAudio() +stopAudio() +ejectDisk() +getDiskID() + +Events fired by the Disk Drive: +"disk" when a disk or other item is inserted into the drive. Argument is the name of the drive. +"disk_eject" when a disk is removed from a drive. Argument is the name of the drive. +Type "help events" to learn about the event system. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/earth.txt b/src/main/resources/assets/cctweaked/lua/rom/help/earth.txt new file mode 100644 index 000000000..b9842d064 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/earth.txt @@ -0,0 +1 @@ +Mostly harmless. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/edit.txt b/src/main/resources/assets/cctweaked/lua/rom/help/edit.txt new file mode 100644 index 000000000..6e89a7e08 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/edit.txt @@ -0,0 +1,4 @@ +edit is a text editor for creating or modifying programs or text files. After creating a program with edit, type its filename in the shell to run it. You can open any of the builtin programs with edit to learn how to program. + +ex: +"edit hello" opens a file called "hello" for editing. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/eject.txt b/src/main/resources/assets/cctweaked/lua/rom/help/eject.txt new file mode 100644 index 000000000..ba6ad1b6c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/eject.txt @@ -0,0 +1,4 @@ +eject ejects the contents of an attached disk drive. + +ex: +"eject left" ejects the contents of the disk drive to the left of the computer. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/equip.txt b/src/main/resources/assets/cctweaked/lua/rom/help/equip.txt new file mode 100644 index 000000000..684aad125 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/equip.txt @@ -0,0 +1,5 @@ +equip is a program for Turtles and Pocket Computer. equip will equip an item from the Turtle's inventory for use as a tool of peripheral. On a Pocket Computer you don't need to write a side. + +ex: +"equip 5 left" will equip the item from slot 5 of the turtle onto the left side of the turtle +"equip" on a Pocket Computer will equip the first item from your inventory. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/events.txt b/src/main/resources/assets/cctweaked/lua/rom/help/events.txt new file mode 100644 index 000000000..2fa405881 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/events.txt @@ -0,0 +1,15 @@ +The function os.pullEvent() will yield the program until a system event occurs. The first return value is the event name, followed by any arguments. + +Some events which can occur are: +"char" when text is typed on the keyboard. Argument is the character typed. +"key" when a key is pressed on the keyboard. Arguments are the keycode and whether the key is a repeat. Compare the keycode to the values in keys API to see which key was pressed. +"key_up" when a key is released on the keyboard. Argument is the numerical keycode. Compare to the values in keys API to see which key was released. +"paste" when text is pasted from the users keyboard. Argument is the line of text pasted. + +Events only on advanced computers: +"mouse_click" when a user clicks the mouse. Arguments are button, xPos, yPos. +"mouse_drag" when a user moves the mouse when held. Arguments are button, xPos, yPos. +"mouse_up" when a user releases the mouse button. Arguments are button, xPos, yPos. +"mouse_scroll" when a user uses the scrollwheel on the mouse. Arguments are direction, xPos, yPos. + +Other APIs and peripherals will emit their own events. See their respective help pages for details. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/excavate.txt b/src/main/resources/assets/cctweaked/lua/rom/help/excavate.txt new file mode 100644 index 000000000..fc868d378 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/excavate.txt @@ -0,0 +1,4 @@ +excavate is a program for Mining Turtles. When excavate is run, the turtle will mine a rectangular shaft into the ground, collecting blocks as it goes, and return to the surface once bedrock is hit. + +ex: +"excavate 3" will mine a 3x3 shaft. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/exec.txt b/src/main/resources/assets/cctweaked/lua/rom/help/exec.txt new file mode 100644 index 000000000..3e2e0429d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/exec.txt @@ -0,0 +1,7 @@ +On a Command Computer, "exec" will execute a command as if entered on a command block. Use "commands" to list all the available commands. + +ex: +"exec say Hello World" +"exec setblock ~0 ~1 ~0 minecraft:dirt" + +Type "help commandsapi" for help using commands in lua programs. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/exit.txt b/src/main/resources/assets/cctweaked/lua/rom/help/exit.txt new file mode 100644 index 000000000..891745139 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/exit.txt @@ -0,0 +1 @@ +exit will exit the current shell. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/falling.txt b/src/main/resources/assets/cctweaked/lua/rom/help/falling.txt new file mode 100644 index 000000000..ac2517244 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/falling.txt @@ -0,0 +1 @@ +"From Russia with Fun" comes a fun, new, suspiciously-familiar falling block game for CraftOS. Only on Pocket Computers! diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/fg.txt b/src/main/resources/assets/cctweaked/lua/rom/help/fg.txt new file mode 100644 index 000000000..989efde67 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/fg.txt @@ -0,0 +1,5 @@ +fg is a program for Advanced Computers which opens a new tab in the foreground. + +ex: +"fg" will open a foreground tab running the shell +"fg worm" will open a foreground tab running the "worm" program diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/fs.txt b/src/main/resources/assets/cctweaked/lua/rom/help/fs.txt new file mode 100644 index 000000000..363273b12 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/fs.txt @@ -0,0 +1,40 @@ +Functions in the Filesystem API: +fs.list( path ) +fs.find( wildcard ) +fs.exists( path ) +fs.isDir( path ) +fs.isReadOnly( path ) +fs.getDir( path ) +fs.getName( path ) +fs.getSize( path ) +fs.getDrive( path ) +fs.getFreeSpace( path ) +fs.makeDir( path ) +fs.move( path, path ) +fs.copy( path, path ) +fs.delete( path ) +fs.combine( path, localpath ) +fs.open( path, mode ) +fs.complete( path, location ) +Available fs.open() modes are "r", "w", "a", "rb", "wb" and "ab". + +Functions on files opened with mode "r": +readLine() +readAll() +close() +read( number ) + +Functions on files opened with mode "w" or "a": +write( string ) +writeLine( string ) +flush() +close() + +Functions on files opened with mode "rb": +read() +close() + +Functions on files opened with mode "wb" or "ab": +write( byte ) +flush() +close() diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/go.txt b/src/main/resources/assets/cctweaked/lua/rom/help/go.txt new file mode 100644 index 000000000..c005a9a59 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/go.txt @@ -0,0 +1,6 @@ +go is a program for Turtles, used to control the turtle without programming. It accepts one or more commands as a direction followed by a distance. + +ex: +"go forward" moves the turtle 1 space forward. +"go forward 3" moves the turtle 3 spaces forward. +"go forward 3 up left 2" moves the turtle 3 spaces forward, 1 spaces up, then left 180 degrees. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/gps.txt b/src/main/resources/assets/cctweaked/lua/rom/help/gps.txt new file mode 100644 index 000000000..35b833580 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/gps.txt @@ -0,0 +1,10 @@ +gps can be used to host a GPS server, or to determine a position using trilateration. +Type "help gpsapi" for help using GPS functions in lua programs. + +ex: +"gps locate" will connect to nearby GPS servers, and try to determine the position of the computer or turtle. +"gps host" will try to determine the position, and host a GPS server if successful. +"gps host 10 20 30" will host a GPS server, using the manually entered position 10,20,30. + +Take care when manually entering host positions. If the positions entered into multiple GPS hosts +are not consistent, the results of locate calls will be incorrect. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/gpsapi.txt b/src/main/resources/assets/cctweaked/lua/rom/help/gpsapi.txt new file mode 100644 index 000000000..83116f669 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/gpsapi.txt @@ -0,0 +1,4 @@ +Functions in the GPS API: +gps.locate( timeout ) + +The locate function will send a signal to nearby gps servers, and wait for responses before the timeout. If it receives enough responses to determine this computers position then x, y and z co-ordinates will be returned, otherwise it will return nil. If GPS hosts do not have their positions configured correctly, results will be inaccurate. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/hello.txt b/src/main/resources/assets/cctweaked/lua/rom/help/hello.txt new file mode 100644 index 000000000..0f2017aad --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/hello.txt @@ -0,0 +1 @@ +hello prints the text "Hello World!" to the screen. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/help.txt b/src/main/resources/assets/cctweaked/lua/rom/help/help.txt new file mode 100644 index 000000000..29c0b3a32 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/help.txt @@ -0,0 +1,4 @@ +help is the help tool you're currently using. +Type "help index" to see all help topics. +Type "help" to see the help intro. +Type "help helpapi" for information on the help Lua API. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/helpapi.txt b/src/main/resources/assets/cctweaked/lua/rom/help/helpapi.txt new file mode 100644 index 000000000..322ce0345 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/helpapi.txt @@ -0,0 +1,5 @@ +Functions in the help API: +help.setPath( path ) +help.lookup( topic ) +help.topics() +help.completeTopic( topic ) diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/http.txt b/src/main/resources/assets/cctweaked/lua/rom/help/http.txt new file mode 100644 index 000000000..27d2be100 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/http.txt @@ -0,0 +1,9 @@ +Functions in the HTTP API: +http.checkURL( url ) +http.checkURLAsync( url ) +http.request( url, [postData], [headers] ) +http.get( url, [headers] ) +http.post( url, postData, [headers] ) + +The HTTP API may be disabled in ComputerCraft.cfg +A period of time after a http.request() call is made, a "http_success" or "http_failure" event will be raised. Arguments are the url and a file handle if successful. Arguments are nil, an error message, and (optionally) a file handle if the request failed. http.get() and http.post() block until this event fires instead. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/id.txt b/src/main/resources/assets/cctweaked/lua/rom/help/id.txt new file mode 100644 index 000000000..e23f1290a --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/id.txt @@ -0,0 +1,5 @@ +id prints the unique identifier of this computer, or a Disk in an attached Disk Drive. + +ex: +"id" will print this Computers ID and label +"id left" will print the ID and label of the disk in the Disk Drive on the left diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/intro.txt b/src/main/resources/assets/cctweaked/lua/rom/help/intro.txt new file mode 100644 index 000000000..0165fbe66 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/intro.txt @@ -0,0 +1,7 @@ +Welcome to CraftOS! +Type "programs" to see the programs you can run. +Type "help " to see help for a specific program. +Type "help programming" to learn about programming. +Type "help whatsnew" to find out about new features. +Type "help credits" to learn who made all this. +Type "help index" to see all help topics. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/io.txt b/src/main/resources/assets/cctweaked/lua/rom/help/io.txt new file mode 100644 index 000000000..ed8e2cc18 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/io.txt @@ -0,0 +1,2 @@ +io is a standard Lua5.1 API, reimplemented for CraftOS. Not all the features are available. +Refer to http://www.lua.org/manual/5.1/ for more information. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/keys.txt b/src/main/resources/assets/cctweaked/lua/rom/help/keys.txt new file mode 100644 index 000000000..a46f10731 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/keys.txt @@ -0,0 +1,9 @@ +The keys API contains constants for all the key codes that can be returned by the "key" event: + +Example usage: +local sEvent, nKey = os.pullEvent() +if sEvent == "key" and nKey == keys.enter then + -- Do something +end + +See http://www.minecraftwiki.net/wiki/Key_codes, or the source code, for a complete reference. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/label.txt b/src/main/resources/assets/cctweaked/lua/rom/help/label.txt new file mode 100644 index 000000000..7bacfbec1 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/label.txt @@ -0,0 +1,9 @@ +label gets or sets the label of the Computer, or of Floppy Disks in attached disk drives. + +ex: +"label get" prints the label of the computer. +"label get left" prints the label of the disk in the left drive. +"label set "My Computer"" set the label of the computer to "My Computer". +"label set left "My Programs"" - sets the label of the disk in the left drive to "My Programs". +"label clear" clears the label of the computer. +"label clear left" clears the label of the disk in the left drive. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/list.txt b/src/main/resources/assets/cctweaked/lua/rom/help/list.txt new file mode 100644 index 000000000..16c0fdf52 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/list.txt @@ -0,0 +1 @@ +ls will list all the directories and files in the current location. Use "type" to find out if an item is a file or a directory. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/lua.txt b/src/main/resources/assets/cctweaked/lua/rom/help/lua.txt new file mode 100644 index 000000000..4d164c8b0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/lua.txt @@ -0,0 +1 @@ +lua is an interactive prompt for the lua programming language. It's a useful tool for learning the language. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/math.txt b/src/main/resources/assets/cctweaked/lua/rom/help/math.txt new file mode 100644 index 000000000..35ed34827 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/math.txt @@ -0,0 +1,2 @@ +math is a standard Lua5.1 API. +Refer to http://www.lua.org/manual/5.1/ for more information. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/mkdir.txt b/src/main/resources/assets/cctweaked/lua/rom/help/mkdir.txt new file mode 100644 index 000000000..e5e8fdab3 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/mkdir.txt @@ -0,0 +1,5 @@ +mkdir creates a directory in the current location. + +ex: +"mkdir foo" creates a directory named "foo". +"mkdir ../foo" creates a directory named "foo" in the directory above the current directory. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/modems.txt b/src/main/resources/assets/cctweaked/lua/rom/help/modems.txt new file mode 100644 index 000000000..281a87582 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/modems.txt @@ -0,0 +1,12 @@ +Wired and Wireless Modems are peripheral devices available for CraftOS. Type "help peripheral" to learn about using the Peripheral API to connect with peripherals. When a Modem is connected, peripheral.getType() will return "modem". + +Methods exposed by Modems: +open( channel ) +isOpen( channel ) +close( channel ) +closeAll() +transmit( channel, replyChannel, message ) +isWireless() + +Events fired by Modems: +"modem_message" when a message is received on an open channel. Arguments are name, channel, replyChannel, message, distance diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/monitor.txt b/src/main/resources/assets/cctweaked/lua/rom/help/monitor.txt new file mode 100644 index 000000000..028a8bc80 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/monitor.txt @@ -0,0 +1,6 @@ +monitor will connect to an attached Monitor peripheral, and run a program on its display. +Type "help monitors" for help using monitors as peripherals in lua programs. + +ex: +"monitor left hello" will run the "hello" program on the monitor to the left of the computer. +"monitor top edit foo" will run the edit program on the top monitor, editing the file "foo". diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/monitors.txt b/src/main/resources/assets/cctweaked/lua/rom/help/monitors.txt new file mode 100644 index 000000000..2add3096b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/monitors.txt @@ -0,0 +1,23 @@ +The Monitor is a peripheral device available for CraftOS. Type "help peripheral" to learn about using the Peripheral API to connect with peripherals. When a Monitor is connected, peripheral.getType() will return "monitor". A wrapped monitor can be used with term.redirect() to send all terminal output to the monitor. + +Methods exposed by the Monitor: +write( text ) +blit( text, textColor, backgroundColor ) +clear() +clearLine() +getCursorPos() +setCursorPos( x, y ) +setCursorBlink( blink ) +isColor() +setTextColor( color ) +setBackgroundColor( color ) +getTextColor() +getBackgroundColor() +getSize() +scroll( n ) +setPaletteColor( color, r, g, b ) +getPaletteColor( color ) + +Events fired by the Monitor: +"monitor_touch" when an Advanced Monitor is touched by the player. Arguments are name, x, y +"monitor_resize" when the size of a Monitor changes. Argument is the name of the monitor. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/move.txt b/src/main/resources/assets/cctweaked/lua/rom/help/move.txt new file mode 100644 index 000000000..bc16f732a --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/move.txt @@ -0,0 +1,6 @@ +mv moves a file or directory from one location to another. + +ex: +"mv foo bar" renames the file "foo" to "bar". +"mv foo bar/foo" moves the file "foo" to a folder called "bar". +"mv disk/* disk2" moves the contents of one disk to another diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/multishell.txt b/src/main/resources/assets/cctweaked/lua/rom/help/multishell.txt new file mode 100644 index 000000000..8fbf8d75b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/multishell.txt @@ -0,0 +1,2 @@ +multishell is the toplevel program on Advanced Computers which manages background tabs. +Type "help shellapi" for information about the shell lua api. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/os.txt b/src/main/resources/assets/cctweaked/lua/rom/help/os.txt new file mode 100644 index 000000000..b57bef8da --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/os.txt @@ -0,0 +1,26 @@ +Functions in the os (Operating System) API: +os.version() +os.getComputerID() +os.getComputerLabel() +os.setComputerLabel() +os.run( environment, programpath, arguments ) +os.loadAPI( path ) +os.unloadAPI( name ) +os.pullEvent( [filter] ) +os.queueEvent( event, arguments ) +os.clock() +os.startTimer( timeout ) +os.cancelTimer( token ) +os.sleep( timeout ) +os.time( [source] ) +os.day( [source] ) +os.epoch( [source] ) +os.setAlarm( time ) +os.cancelAlarm( token ) +os.shutdown() +os.reboot() + +Events emitted by the os API: +"timer" when a timeout started by os.startTimer() completes. Argument is the token returned by os.startTimer(). +"alarm" when a time passed to os.setAlarm() is reached. Argument is the token returned by os.setAlarm(). +Type "help events" to learn about the event system. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/paint.txt b/src/main/resources/assets/cctweaked/lua/rom/help/paint.txt new file mode 100644 index 000000000..d845b664d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/paint.txt @@ -0,0 +1,4 @@ +paint is a program for creating images on Advanced Computers. Select colors from the color pallette on the right, and click on the canvas to draw. Press Ctrl to access the menu and save your pictures. + +ex: +"edit mario" opens an image called "mario" for editing. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/paintutils.txt b/src/main/resources/assets/cctweaked/lua/rom/help/paintutils.txt new file mode 100644 index 000000000..d7e6d2042 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/paintutils.txt @@ -0,0 +1,7 @@ +Functions in the Paint Utilities API: +paintutils.drawPixel( x, y, colour ) +paintutils.drawLine( startX, startY, endX, endY, colour ) +paintutils.drawBox( startX, startY, endX, endY, colour ) +paintutils.drawFilledBox( startX, startY, endX, endY, colour ) +paintutils.loadImage( path ) +paintutils.drawImage( image, x, y ) diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/parallel.txt b/src/main/resources/assets/cctweaked/lua/rom/help/parallel.txt new file mode 100644 index 000000000..4b958c442 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/parallel.txt @@ -0,0 +1,4 @@ +Functions in the Parallel API: +parallel.waitForAny( function1, function2, ... ) +parallel.waitForAll( function1, function2, ... ) +These methods provide an easy way to run multiple lua functions simultaneously. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/pastebin.txt b/src/main/resources/assets/cctweaked/lua/rom/help/pastebin.txt new file mode 100644 index 000000000..8f5589828 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/pastebin.txt @@ -0,0 +1,7 @@ +pastebin is a program for uploading files to and downloading files from pastebin.com. This is useful for sharing programs with other players. +The HTTP API must be enabled in ComputerCraft.cfg to use this program. + +ex: +"pastebin put foo" will upload the file "foo" to pastebin.com, and print the URL. +"pastebin get xq5gc7LB foo" will download the file from the URL http://pastebin.com/xq5gc7LB, and save it as "foo". +"pastebin run CxaWmPrX" will download the file from the URL http://pastebin.com/CxaWmPrX, and immediately run it. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/peripheral.txt b/src/main/resources/assets/cctweaked/lua/rom/help/peripheral.txt new file mode 100644 index 000000000..333f52f51 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/peripheral.txt @@ -0,0 +1,16 @@ +The peripheral API is for interacting with external peripheral devices. Type "help peripherals" to learn about the peripherals available. + +Functions in the peripheral API: +peripheral.getNames() +peripheral.isPresent( name ) +peripheral.getName( peripheral ) +peripheral.getType( name ) +peripheral.getMethods( name ) +peripheral.call( name, methodName, param1, param2, etc ) +peripheral.wrap( name ) +peripheral.find( type, [fnFilter] ) + +Events fired by the peripheral API: +"peripheral" when a new peripheral is attached. Argument is the name. +"peripheral_detach" when a peripheral is removed. Argument is the name. +Type "help events" to learn about the event system. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/peripherals.txt b/src/main/resources/assets/cctweaked/lua/rom/help/peripherals.txt new file mode 100644 index 000000000..7cea884e2 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/peripherals.txt @@ -0,0 +1,7 @@ +The "peripherals" program will list all of the peripheral devices accessible from this computer. +Peripherals are external devices which CraftOS Computers and Turtles can interact with using the peripheral API. +Type "help peripheral" to learn about using the peripheral API. +Type "help drives" to learn about using Disk Drives. +Type "help modems" to learn about using Modems. +Type "help monitors" to learn about using Monitors. +Type "help printers" to learn about using Printers. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/pocket.txt b/src/main/resources/assets/cctweaked/lua/rom/help/pocket.txt new file mode 100644 index 000000000..72925bdea --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/pocket.txt @@ -0,0 +1,6 @@ +pocket is an API available on pocket computers, which allows modifying its upgrades. +Functions in the pocket API: +pocket.equipBack() +pocket.unequipBack() + +When equipping upgrades, it will search your inventory for a suitable upgrade, starting in the selected slot. If one cannot be found then it will check your offhand. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/printers.txt b/src/main/resources/assets/cctweaked/lua/rom/help/printers.txt new file mode 100644 index 000000000..73295344e --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/printers.txt @@ -0,0 +1,12 @@ +The Printer is a peripheral device available for CraftOS. Type "help peripheral" to learn about using the Peripheral API to connect with peripherals. When a Printer is connected, peripheral.getType() will return "printer". + +Methods exposed by the Printer: +getInkLevel() +getPaperLevel() +newPage() +setPageTitle( title ) +getPageSize() +setCursorPos( x, y ) +getCursorPos() +write( text ) +endPage() diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/programming.txt b/src/main/resources/assets/cctweaked/lua/rom/help/programming.txt new file mode 100644 index 000000000..c82b2e36a --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/programming.txt @@ -0,0 +1,11 @@ +To learn the lua programming language, visit http://lua-users.org/wiki/TutorialDirectory. + +To experiment with lua in CraftOS, run the "lua" program and start typing code. +To create programs, use "edit" to create files, then type their names in the shell to run them. If you name a program "startup" and place it in the root or on a disk drive, it will run automatically when the computer starts. + +To terminate a program stuck in a loop, hold Ctrl+T for 1 second. +To quickly shutdown a computer, hold Ctrl+S for 1 second. +To quickly reboot a computer, hold Ctrl+R for 1 second. + +To learn about the programming APIs available, type "apis" or "help apis". +If you get stuck, visit the forums at http://www.computercraft.info/ for advice and tutorials. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/programs.txt b/src/main/resources/assets/cctweaked/lua/rom/help/programs.txt new file mode 100644 index 000000000..e1bc3c575 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/programs.txt @@ -0,0 +1 @@ +programs lists all the programs on the rom of the computer. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/reboot.txt b/src/main/resources/assets/cctweaked/lua/rom/help/reboot.txt new file mode 100644 index 000000000..525c9c979 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/reboot.txt @@ -0,0 +1,2 @@ +reboot will turn the computer off and on again. +You can also hold Ctrl+R at any time to quickly reboot. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/redirection.txt b/src/main/resources/assets/cctweaked/lua/rom/help/redirection.txt new file mode 100644 index 000000000..01ec7dfb3 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/redirection.txt @@ -0,0 +1,2 @@ +Redirection ComputerCraft Edition is the CraftOS version of a fun new puzzle game by Dan200, the author of ComputerCraft. +Play it on any Advanced Computer, then visit http://www.redirectiongame.com to play the full game! diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/rednet.txt b/src/main/resources/assets/cctweaked/lua/rom/help/rednet.txt new file mode 100644 index 000000000..ddff18b2f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/rednet.txt @@ -0,0 +1,18 @@ +The rednet API provides a simple computer networking model using modems. + +Functions in the rednet API: +rednet.open( side ) +rednet.close( [side] ) +rednet.isOpen( [side] ) +rednet.send( receiverID, message, [protocol] ) -- Send to a specific computer +rednet.broadcast( message, [protocol] ) -- Send to all computers +rednet.receive( [protocol], [timeout] ) -- Returns: senderID, message, protocol +rednet.host( protocol, hostname ) +rednet.unhost( protocol ) +rednet.lookup( protocol, [hostname] ) -- Returns: ID + +Events fired by the rednet API: +"rednet_message" when a message is received. Arguments are senderID, message, protocol +Type "help events" to learn about the event system. + +Rednet is not the only way to use modems for networking. Interfacing with the modem directly using the peripheral API and listening for the "modem_message" event allows for lower level control, at the expense of powerful high level networking features. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/redstone.txt b/src/main/resources/assets/cctweaked/lua/rom/help/redstone.txt new file mode 100644 index 000000000..ce86bb6b4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/redstone.txt @@ -0,0 +1,9 @@ +The redstone program can be used to get, set or pulse redstone inputs and outputs from the computer. + +ex: +"redstone probe" will list all the redstone inputs to the computer +"redstone set left true" turns on the left redstone output. +"redstone set right blue false" turns off the blue wire in the bundled cable on the right redstone output. +"redstone pulse front 10 1" emits 10 one second redstone pulses on the front redstone output. + +Type "help redstoneapi" or "help rs" for information on the redstone Lua API. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/redstoneapi.txt b/src/main/resources/assets/cctweaked/lua/rom/help/redstoneapi.txt new file mode 100644 index 000000000..a8db92dcf --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/redstoneapi.txt @@ -0,0 +1,19 @@ +Functions in the Redstone API: +redstone.getSides( ) +redstone.getInput( side ) +redstone.setOutput( side, boolean ) +redstone.getOutput( side ) +redstone.getAnalogInput( side ) +redstone.setAnalogOutput( side, number ) +redstone.getAnalogOutput( side ) + +Functions in the Redstone API for working with bundled cables: +redstone.getBundledInput( side ) +redstone.testBundledInput( side, color ) +redstone.setBundledOutput( side, colors ) +redstone.getBundledOutput( side ) +Type "help bundled" for usage examples. + +Events emitted by the redstone API: +"redstone", when the state of any redstone input changes. Use getInput() or getBundledInput() to inspect the changes +Type "help events" to learn about the event system. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/refuel.txt b/src/main/resources/assets/cctweaked/lua/rom/help/refuel.txt new file mode 100644 index 000000000..86e806b2c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/refuel.txt @@ -0,0 +1,6 @@ +refuel is a program for Turtles. Refuel will consume items from the inventory as fuel for turtle. + +ex: +"refuel" will refuel with at most one fuel item +"refuel 10" will refuel with at most 10 fuel items +"refuel all" will refuel with as many fuel items as possible diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/rename.txt b/src/main/resources/assets/cctweaked/lua/rom/help/rename.txt new file mode 100644 index 000000000..a050c81c0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/rename.txt @@ -0,0 +1,4 @@ +rename renames a file or directory. + +ex: +"rename foo bar" renames the file "foo" to "bar". diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/repeat.txt b/src/main/resources/assets/cctweaked/lua/rom/help/repeat.txt new file mode 100644 index 000000000..a08ebf8da --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/repeat.txt @@ -0,0 +1 @@ +repeat is a program for repeating rednet messages across long distances. To use, connect 2 or more modems to a computer and run the "repeat" program; from then on, any rednet message sent from any computer in wireless range or connected by networking cable to either of the modems will be repeated to those on the other side. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/rs.txt b/src/main/resources/assets/cctweaked/lua/rom/help/rs.txt new file mode 100644 index 000000000..5b8909d07 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/rs.txt @@ -0,0 +1,19 @@ +Functions in the Redstone API: +rs.getSides( ) +rs.getInput( side ) +rs.setOutput( side, boolean ) +rs.getOutput( side ) +rs.getAnalogInput( side ) +rs.setAnalogOutput( side, number ) +rs.getAnalogOutput( side ) + +Functions in the Redstone API for working with RedPower bundled cables: +rs.getBundledInput( side ) +rs.testBundledInput( side, color ) +rs.setBundledOutput( side, colors ) +rs.getBundledOutput( side ) +Type "help bundled" for usage examples. + +Events emitted by the redstone API: +"redstone", when the state of any redstone input changes. Use getInput() or getBundledInput() to inspect the changes +Type "help events" to learn about the event system. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/set.txt b/src/main/resources/assets/cctweaked/lua/rom/help/set.txt new file mode 100644 index 000000000..580d19164 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/set.txt @@ -0,0 +1,6 @@ +The set program can be used to inspect and change system settings. + +Usage: +"set" will print all the system settings and their values +"set foo" will print the value of the system setting "foo" +"set foo bar" will set the value of the system setting "foo" to "bar" diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/settings.txt b/src/main/resources/assets/cctweaked/lua/rom/help/settings.txt new file mode 100644 index 000000000..7ec46b885 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/settings.txt @@ -0,0 +1,19 @@ +Functions in the Settings API: +settings.get( name, [default] ) +settings.set( name, value ) +settings.unset( name ) +settings.load( path ) +settings.save( path ) +settings.clear() +settings.getNames() + +Default Settings: +shell.autocomplete - enables auto-completion in the Shell. +lua.autocomplete - enables auto-completion in the Lua program. +edit.autocomplete - enables auto-completion in the Edit program. +edit.default_extension - sets the default file extension for files created with the Edit program +paint.default_extension - sets the default file extension for files created with the Paint program +bios.use_multishell - enables Multishell on Advanced Computers, Turtles, Pocket Computers and Command Computers. +shell.allow_disk_startup - if a Disk Drive with a Disk inside that has a 'startup' script is attached to a computer, this setting allows to automatically run that script when the computer starts. +shell.allow_startup - if there is a 'startup' script in a computer's root, this setting allow to automatically run that script when the computer runs. +list.show_hidden - determines, whether the List program will list hidden files or not. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/shell.txt b/src/main/resources/assets/cctweaked/lua/rom/help/shell.txt new file mode 100644 index 000000000..9be848561 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/shell.txt @@ -0,0 +1,2 @@ +shell is the toplevel program which interprets commands and runs program. +Type "help shellapi" for information about the shell lua api. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/shellapi.txt b/src/main/resources/assets/cctweaked/lua/rom/help/shellapi.txt new file mode 100644 index 000000000..287c4dc7a --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/shellapi.txt @@ -0,0 +1,19 @@ +Functions in the Shell API: +shell.exit() +shell.dir() +shell.setDir( path ) +shell.path() +shell.setPath( path ) +shell.resolve( localpath ) +shell.resolveProgram( name ) +shell.aliases() +shell.setAlias( alias, command ) +shell.clearAlias( alias ) +shell.programs() +shell.run( program, arguments ) +shell.getRunningProgram() +shell.complete( line ) +shell.completeProgram( program ) +shell.setCompletionFunction( program, fnComplete ) +shell.openTab( program, arguments ) (Advanced Computer required) +shell.switchTab( n ) (Advanced Computer required) diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/shutdown.txt b/src/main/resources/assets/cctweaked/lua/rom/help/shutdown.txt new file mode 100644 index 000000000..8bc734fc9 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/shutdown.txt @@ -0,0 +1 @@ +shutdown will turn off the computer. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/speaker.md b/src/main/resources/assets/cctweaked/lua/rom/help/speaker.md new file mode 100644 index 000000000..0ea3c2916 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/speaker.md @@ -0,0 +1,9 @@ +The speaker program plays audio files using speakers attached to this computer. + +It supports audio files in a limited number of formats: +* DFPWM: You can convert music to DFPWM with external tools like https://music.madefor.cc. +* WAV: WAV files must be 8-bit PCM or DFPWM, with exactly one channel and a sample rate of 48kHz. + +## Examples: +* `speaker play example.dfpwm left` plays the "example.dfpwm" audio file using the speaker on the left of the computer. +* `speaker stop` stops any currently playing audio. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/speakers.txt b/src/main/resources/assets/cctweaked/lua/rom/help/speakers.txt new file mode 100644 index 000000000..88911273b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/speakers.txt @@ -0,0 +1,9 @@ +The Speaker is a peripheral device available for CraftOS. Type "help peripheral" to learn about using the Peripheral API to connect with peripherals. When a Speaker is connected, peripheral.getType() will return "speaker". + +Methods exposed by the Speaker: +playSound( sResourceName, nVolume, nPitch ) +playNote( sInstrumentName, nVolume, nPitch ) + +Resource name is the same as used by the /playsound command, such as "minecraft:entity.cow.ambient". +Instruments are as follows: "harp", "bass", "snare", "hat", and "basedrum" with the addition of "flute", "bell", "chime", and "guitar" in Minecraft versions 1.12 and above. +Ticks is the amount of times a noteblock has been tuned (right clicked). diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/string.txt b/src/main/resources/assets/cctweaked/lua/rom/help/string.txt new file mode 100644 index 000000000..ce30dc916 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/string.txt @@ -0,0 +1,2 @@ +string is a standard Lua5.1 API. +Refer to http://www.lua.org/manual/5.1/ for more information. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/table.txt b/src/main/resources/assets/cctweaked/lua/rom/help/table.txt new file mode 100644 index 000000000..d6943d035 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/table.txt @@ -0,0 +1,2 @@ +table is a standard Lua5.1 API. +Refer to http://www.lua.org/manual/5.1/ for more information. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/term.txt b/src/main/resources/assets/cctweaked/lua/rom/help/term.txt new file mode 100644 index 000000000..6f8df30fb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/term.txt @@ -0,0 +1,22 @@ +Functions in the Terminal API: +term.write( text ) +term.blit( text, textColor, backgroundColor ) +term.clear() +term.clearLine() +term.getCursorPos() +term.setCursorPos( x, y ) +term.setCursorBlink( blink ) +term.isColor() +term.setTextColor( color ) +term.setBackgroundColor( color ) +term.getTextColor() +term.getBackgroundColor() +term.getSize() +term.scroll( n ) +term.redirect( object ) +term.current() +term.setPaletteColor( color, r, g, b ) +term.getPaletteColor( color ) + +Events emitted by the terminals: +"term_resize", when the size of a terminal changes. This can happen in multitasking environments, or when the terminal out is being redirected by the "monitor" program. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/textutils.txt b/src/main/resources/assets/cctweaked/lua/rom/help/textutils.txt new file mode 100644 index 000000000..ee91a883f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/textutils.txt @@ -0,0 +1,10 @@ +Functions in the Text Utilities API: +textutils.slowPrint( text ) +textutils.tabulate( table, table2, ... ) +textutils.pagedTabulate( table, table2, ... ) +textutils.formatTime( time, bTwentyFourHour ) +textutils.serialize( table ) +textutils.unserialize( string ) +textutils.serializeJSON( table, [useNBTStyle] ) +textutils.urlEncode( string ) +textutils.complete( string, table ) diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/time.txt b/src/main/resources/assets/cctweaked/lua/rom/help/time.txt new file mode 100644 index 000000000..4a67ab57d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/time.txt @@ -0,0 +1 @@ +time prints the current time of day. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/tunnel.txt b/src/main/resources/assets/cctweaked/lua/rom/help/tunnel.txt new file mode 100644 index 000000000..07d05c482 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/tunnel.txt @@ -0,0 +1,4 @@ +tunnel is a program for Mining Turtles. Tunnel will mine a 3x2 tunnel of the depth specified. + +ex: +"tunnel 20" will tunnel a tunnel 20 blocks long. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/turn.txt b/src/main/resources/assets/cctweaked/lua/rom/help/turn.txt new file mode 100644 index 000000000..15782e348 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/turn.txt @@ -0,0 +1,6 @@ +turn is a program for Turtles, used to turn the turtle around without programming. It accepts one or more commands as a direction and a number of turns. The "go" program can also be used for turning. + +ex: +"turn left" turns the turtle 90 degrees left. +"turn right 2" turns the turtle 180 degrees right. +"turn left 2 right" turns left 180 degrees, then right 90 degrees. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/turtle.txt b/src/main/resources/assets/cctweaked/lua/rom/help/turtle.txt new file mode 100644 index 000000000..0827f3317 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/turtle.txt @@ -0,0 +1,48 @@ +turtle is an api available on Turtles, which controls their movement. +Functions in the Turtle API: +turtle.forward() +turtle.back() +turtle.up() +turtle.down() +turtle.turnLeft() +turtle.turnRight() +turtle.select( slotNum ) +turtle.getSelectedSlot() +turtle.getItemCount( [slotNum] ) +turtle.getItemSpace( [slotNum] ) +turtle.getItemDetail( [slotNum] ) +turtle.equipLeft() +turtle.equipRight() +turtle.dig( [toolSide] ) +turtle.digUp( [toolSide] ) +turtle.digDown( [toolSide] ) +turtle.place() +turtle.placeUp() +turtle.placeDown() +turtle.attack( [toolSide] ) +turtle.attackUp( [toolSide] ) +turtle.attackDown( [toolSide] ) +turtle.detect() +turtle.detectUp() +turtle.detectDown() +turtle.compare() +turtle.compareUp() +turtle.compareDown() +turtle.inspect() +turtle.inspectUp() +turtle.inspectDown() +turtle.compareTo( slotNum ) +turtle.transferTo( slotNum, [quantity] ) +turtle.drop( [quantity] ) +turtle.dropUp( [quantity] ) +turtle.dropDown( [quantity] ) +turtle.suck( [quantity] ) +turtle.suckUp( [quantity] ) +turtle.suckDown( [quantity] ) +turtle.getFuelLevel() +turtle.getFuelLimit() +turtle.refuel( [quantity] ) +turtle.craft( [quantity] ) (requires Crafty Turtle) + +Events fired by the Turtle API: +"turtle_inventory" when any of the items in the inventory are changed. Use comparison operations to inspect the changes. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/type.txt b/src/main/resources/assets/cctweaked/lua/rom/help/type.txt new file mode 100644 index 000000000..1c8cad512 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/type.txt @@ -0,0 +1 @@ +type determines the type of a file or directory. Prints "file", "directory" or "does not exist". diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/unequip.txt b/src/main/resources/assets/cctweaked/lua/rom/help/unequip.txt new file mode 100644 index 000000000..9f52805d0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/unequip.txt @@ -0,0 +1,5 @@ +unequip is a program for Turtles and Pocket Computers. unequip will remove tools of peripherals from the specified side of the turtle. On a Pocket Computer you don't need to write a side. + +ex: +"unequip left" will remove the item on the left side of the turtle +"unequip" on a Pocket Computer will remove the item from the Pocket Computer diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/vector.txt b/src/main/resources/assets/cctweaked/lua/rom/help/vector.txt new file mode 100644 index 000000000..b85a373d3 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/vector.txt @@ -0,0 +1,17 @@ +Functions in the 3D Vector Math API: +vector.new( x,y,z ) + +Vectors returned by vector.new() have the following fields and methods: +vector.x +vector.y +vector.z +vector:add( vector ) +vector:sub( vector ) +vector:mul( number ) +vector:dot( vector ) +vector:cross( vector ) +vector:length() +vector:normalize() +vector:round() +vector:tostring() +The +, - and * operators can also be used on vectors. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/wget.txt b/src/main/resources/assets/cctweaked/lua/rom/help/wget.txt new file mode 100644 index 000000000..65e580c08 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/wget.txt @@ -0,0 +1,8 @@ +wget is a program for downloading files from the internet. This is useful for downloading programs created by other players. +If no filename is specified wget will try to determine the filename from the URL by stripping any anchors, parameters and trailing slashes and then taking everything remaining after the last slash. +The HTTP API must be enabled in ComputerCraft.cfg to use this program. +ex: +"wget http://pastebin.com/raw/CxaWmPrX test" will download the file from the URL http://pastebin.com/raw/CxaWmPrX, and save it as "test". +"wget http://example.org/test.lua/?foo=bar#qzu" will download the file from the URL http://example.org/test.lua/?foo=bar#qzu and save it as "test.lua" +"wget http://example.org/" will download the file from the URL http://example.org and save it as "example.org" +"wget run http://pastebin.com/raw/CxaWmPrX" will download the file from the URL http://pastebin.com/raw/CxaWmPrX and run it immediately. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/whatsnew.md b/src/main/resources/assets/cctweaked/lua/rom/help/whatsnew.md new file mode 100644 index 000000000..1512a2ae6 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/whatsnew.md @@ -0,0 +1,25 @@ +New features in CC: Tweaked 1.105.0 + +* Optimise JSON string parsing. +* Add `colors.fromBlit` (Erb3). +* Upload file size limit is now configurable (khankul). +* Wired cables no longer have a distance limit. +* Java methods now coerce values to strings consistently with Lua. +* Add custom timeout support to the HTTP API. +* Support custom proxies for HTTP requests (Lemmmy). +* The `speaker` program now errors when playing HTML files. +* `edit` now shows an error message when editing read-only files. +* Update Ukranian translation (SirEdvin). + +Several bug fixes: +* Allow GPS hosts to only be 1 block apart. +* Fix "Turn On"/"Turn Off" buttons being inverted in the computer GUI (Erb3). +* Fix arrow keys not working in the printout UI. +* Several documentation fixes (zyxkad, Lupus590, Commandcracker). +* Fix monitor renderer debug text always being visible on Forge. +* Fix crash when another mod changes the LoggerContext. +* Fix the `monitor_renderer` option not being present in Fabric config files. +* Pasting on MacOS/OSX now uses Cmd+V rather than Ctrl+V. +* Fix turtles placing blocks upside down when at y<0. + +Type "help changelog" to see the full version history. diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/window.txt b/src/main/resources/assets/cctweaked/lua/rom/help/window.txt new file mode 100644 index 000000000..ea3ea3d78 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/window.txt @@ -0,0 +1,26 @@ +Functions in the window API: +window.create( parent, x, y, width, height, visible ) + +Windows created with the window API have the following methods: +write( text ) +blit( text, textColor, backgroundColor ) +clear() +clearLine() +getCursorPos() +setCursorPos( x, y ) +setCursorBlink( blink ) +isColor() +setTextColor( color ) +setBackgroundColor( color ) +getTextColor() +getBackgroundColor() +getSize() +scroll( n ) +setVisible( bVisible ) +redraw() +restoreCursor() +getPosition() +reposition( x, y, width, height ) +getPaletteColor( color ) +setPaletteColor( color, r, g, b ) +getLine() diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/workbench.txt b/src/main/resources/assets/cctweaked/lua/rom/help/workbench.txt new file mode 100644 index 000000000..b6b1e9682 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/workbench.txt @@ -0,0 +1,4 @@ +Workbenches are peripheral devices found on Crafty Turtles running CraftOS. Type "help peripheral" to learn about using the Peripheral API to connect with peripherals. When a workbench is attached to a turtle, peripheral.getType() will return "workbench". + +Methods exposed by Workbenches: +craft( channel ) diff --git a/src/main/resources/assets/cctweaked/lua/rom/help/worm.txt b/src/main/resources/assets/cctweaked/lua/rom/help/worm.txt new file mode 100644 index 000000000..366ad3996 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/help/worm.txt @@ -0,0 +1 @@ +You've played it in the arcades, now experience the high-octane thrills of the hit game "WORM!" on your home computer! Only on CraftOS! diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/command/.ignoreme b/src/main/resources/assets/cctweaked/lua/rom/modules/command/.ignoreme new file mode 100644 index 000000000..4c7191cf1 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/command/.ignoreme @@ -0,0 +1,4 @@ +--[[ +Alright then, don't ignore me. This file is to ensure the existence of the "modules/command" folder. +You can use this folder to add modules who can be loaded with require() to your Resourcepack. +]] diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/.ignoreme b/src/main/resources/assets/cctweaked/lua/rom/modules/main/.ignoreme new file mode 100644 index 000000000..046a7aaeb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/.ignoreme @@ -0,0 +1,4 @@ +--[[ +Alright then, don't ignore me. This file is to ensure the existence of the "modules/main" folder. +You can use this folder to add modules who can be loaded with require() to your Resourcepack. +]] diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/audio/dfpwm.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/audio/dfpwm.lua new file mode 100644 index 000000000..fb35af58b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/audio/dfpwm.lua @@ -0,0 +1,234 @@ +-- SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- +Convert between streams of DFPWM audio data and a list of amplitudes. + +DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact +format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode +in real time. + +Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a +string, and converted a format suitable for @{speaker.playAudio}. + +## Encoding and decoding files +This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder. +The returned encoder/decoder is itself a function, which converts between the two kinds of data. + +These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for +a specific audio stream. Typically you will want to create a decoder for each stream of audio you read, and an encoder +for each one you write. + +## Converting audio to DFPWM +DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it. +Instead, you can convert audio files online using [music.madefor.cc], the [LionRay Wav Converter][LionRay] Java +application or development builds of [FFmpeg]. + +[music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked" +[LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter " +[FFmpeg]: https://ffmpeg.org "FFmpeg command-line audio manipulation library" + +@see guide!speaker_audio Gives a more general introduction to audio processing and the speaker. +@see speaker.playAudio To play the decoded audio data. +@since 1.100.0 +@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio +is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program. + +```lua +local dfpwm = require("cc.audio.dfpwm") + +local encoder = dfpwm.make_encoder() +local decoder = dfpwm.make_decoder() + +local out = fs.open("speedy.dfpwm", "wb") +for input in io.lines("data/example.dfpwm", 16 * 1024 * 2) do + local decoded = decoder(input) + local output = {} + + -- Read two samples at once and take the average. + for i = 1, #decoded, 2 do + local value_1, value_2 = decoded[i], decoded[i + 1] + output[(i + 1) / 2] = (value_1 + value_2) / 2 + end + + out.write(encoder(output)) + + sleep(0) -- This program takes a while to run, so we need to make sure we yield. +end +out.close() +``` +]] + +local expect = require "cc.expect".expect + +local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift + +local PREC = 10 +local PREC_POW = 2 ^ PREC +local PREC_POW_HALF = 2 ^ (PREC - 1) +local STRENGTH_MIN = 2 ^ (PREC - 8 + 1) + +local function make_predictor() + local charge, strength, previous_bit = 0, 0, false + + return function(current_bit) + local target = current_bit and 127 or -128 + + local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW) + if next_charge == charge and next_charge ~= target then + next_charge = next_charge + (current_bit and 1 or -1) + end + + local z = current_bit == previous_bit and PREC_POW - 1 or 0 + local next_strength = strength + if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end + if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end + + charge, strength, previous_bit = next_charge, next_strength, current_bit + return charge + end +end + +--[[- Create a new encoder for converting PCM audio data into DFPWM. + +The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and +returns the encoded DFPWM data. + +:::caution Reusing encoders +Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple +streams, or use different encoders for the same stream, the resulting audio may not sound correct. +::: + +@treturn function(pcm: { number... }):string The encoder function +@see encode A helper function for encoding an entire file of audio at once. +]] +local function make_encoder() + local predictor = make_predictor() + local previous_charge = 0 + + return function(input) + expect(1, input, "table") + + local output, output_n = {}, 0 + for i = 1, #input, 8 do + local this_byte = 0 + for j = 0, 7 do + local inp_charge = floor(input[i + j] or 0) + if inp_charge > 127 or inp_charge < -128 then + error(("Amplitude at position %d was %d, but should be between -128 and 127"):format(i + j, inp_charge), 2) + end + + local current_bit = inp_charge > previous_charge or (inp_charge == previous_charge and inp_charge == 127) + this_byte = floor(this_byte / 2) + (current_bit and 128 or 0) + + previous_charge = predictor(current_bit) + end + + output_n = output_n + 1 + output[output_n] = char(this_byte) + end + + return table.concat(output, "", 1, output_n) + end +end + +--[[- Create a new decoder for converting DFPWM into PCM audio data. + +The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value +between -128 and 127. + +:::caution Reusing decoders +Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple +streams, or use different decoders for the same stream, the resulting audio may not sound correct. +::: + +@treturn function(dfpwm: string):{ number... } The encoder function +@see decode A helper function for decoding an entire file of audio at once. + +@usage Reads "data/example.dfpwm" in blocks of 16KiB (the speaker can accept a maximum of 128×1024 samples), decodes +them and then plays them through the speaker. + +```lua {data-peripheral=speaker} +local dfpwm = require "cc.audio.dfpwm" +local speaker = peripheral.find("speaker") + +local decoder = dfpwm.make_decoder() +for input in io.lines("data/example.dfpwm", 16 * 1024) do + local decoded = decoder(input) + while not speaker.playAudio(decoded) do + os.pullEvent("speaker_audio_empty") + end +end +``` +]] +local function make_decoder() + local predictor = make_predictor() + local low_pass_charge = 0 + local previous_charge, previous_bit = 0, false + + return function (input, output) + expect(1, input, "string") + + local output, output_n = {}, 0 + for i = 1, #input do + local input_byte = byte(input, i) + for _ = 1, 8 do + local current_bit = band(input_byte, 1) ~= 0 + local charge = predictor(current_bit) + + local antijerk = charge + if current_bit ~= previous_bit then + antijerk = floor((charge + previous_charge + 1) / 2) + end + + previous_charge, previous_bit = charge, current_bit + + low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256) + + output_n = output_n + 1 + output[output_n] = low_pass_charge + + input_byte = rshift(input_byte, 1) + end + end + + return output + end +end + +--[[- A convenience function for decoding a complete file of audio at once. + +This should only be used for short files. For larger files, one should read the file in chunks and process it using +@{make_decoder}. + +@tparam string input The DFPWM data to convert. +@treturn { number... } The produced amplitude data. +@see make_decoder +]] +local function decode(input) + expect(1, input, "string") + return make_decoder()(input) +end + +--[[- A convenience function for encoding a complete file of audio at once. + +This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place, +you should use an encoder returned by @{make_encoder} instead. + +@tparam { number... } input The table of amplitude data. +@treturn string The encoded DFPWM data. +@see make_encoder +]] +local function encode(input) + expect(1, input, "table") + return make_encoder()(input) +end + +return { + make_encoder = make_encoder, + encode = encode, + + make_decoder = make_decoder, + decode = decode, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/completion.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/completion.lua new file mode 100644 index 000000000..4b5bf9619 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/completion.lua @@ -0,0 +1,119 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--- A collection of helper methods for working with input completion, such +-- as that require by @{_G.read}. +-- +-- @module cc.completion +-- @see cc.shell.completion For additional helpers to use with +-- @{shell.setCompletionFunction}. +-- @since 1.85.0 + +local expect = require "cc.expect".expect + +local function choice_impl(text, choices, add_space) + local results = {} + for n = 1, #choices do + local option = choices[n] + if #option + (add_space and 1 or 0) > #text and option:sub(1, #text) == text then + local result = option:sub(#text + 1) + if add_space then + table.insert(results, result .. " ") + else + table.insert(results, result) + end + end + end + return results +end + +--- Complete from a choice of one or more strings. +-- +-- @tparam string text The input string to complete. +-- @tparam { string... } choices The list of choices to complete from. +-- @tparam[opt] boolean add_space Whether to add a space after the completed item. +-- @treturn { string... } A list of suffixes of matching strings. +-- @usage Call @{_G.read}, completing the names of various animals. +-- +-- local completion = require "cc.completion" +-- local animals = { "dog", "cat", "lion", "unicorn" } +-- read(nil, nil, function(text) return completion.choice(text, animals) end) +local function choice(text, choices, add_space) + expect(1, text, "string") + expect(2, choices, "table") + expect(3, add_space, "boolean", "nil") + return choice_impl(text, choices, add_space) +end + +--- Complete the name of a currently attached peripheral. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed name. +-- @treturn { string... } A list of suffixes of matching peripherals. +-- @usage +-- local completion = require "cc.completion" +-- read(nil, nil, completion.peripheral) +local function peripheral_(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + return choice_impl(text, peripheral.getNames(), add_space) +end + +local sides = redstone.getSides() + +--- Complete the side of a computer. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed side. +-- @treturn { string... } A list of suffixes of matching sides. +-- @usage +-- local completion = require "cc.completion" +-- read(nil, nil, completion.side) +local function side(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + return choice_impl(text, sides, add_space) +end + +--- Complete a @{settings|setting}. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed settings. +-- @treturn { string... } A list of suffixes of matching settings. +-- @usage +-- local completion = require "cc.completion" +-- read(nil, nil, completion.setting) +local function setting(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + return choice_impl(text, settings.getNames(), add_space) +end + +local command_list + +--- Complete the name of a Minecraft @{commands|command}. +-- +-- @tparam string text The input string to complete. +-- @tparam[opt] boolean add_space Whether to add a space after the completed command. +-- @treturn { string... } A list of suffixes of matching commands. +-- @usage +-- local completion = require "cc.completion" +-- read(nil, nil, completion.command) +local function command(text, add_space) + expect(1, text, "string") + expect(2, add_space, "boolean", "nil") + if command_list == nil then + command_list = commands and commands.list() or {} + end + + return choice_impl(text, command_list, add_space) +end + +return { + choice = choice, + peripheral = peripheral_, + side = side, + setting = setting, + command = command, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/expect.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/expect.lua new file mode 100644 index 000000000..02b6c510c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/expect.lua @@ -0,0 +1,145 @@ +-- SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- The @{cc.expect} library provides helper functions for verifying that +function arguments are well-formed and of the correct type. + +@module cc.expect +@since 1.84.0 +@changed 1.96.0 The module can now be called directly as a function, which wraps around `expect.expect`. +@usage Define a basic function and check it has the correct arguments. + + local expect = require "cc.expect" + local expect, field = expect.expect, expect.field + + local function add_person(name, info) + expect(1, name, "string") + expect(2, info, "table", "nil") + + if info then + print("Got age=", field(info, "age", "number")) + print("Got gender=", field(info, "gender", "string", "nil")) + end + end + + add_person("Anastazja") -- `info' is optional + add_person("Kion", { age = 23 }) -- `gender' is optional + add_person("Caoimhin", { age = 23, gender = true }) -- error! +]] + +local native_select, native_type = select, type + +local function get_type_names(...) + local types = table.pack(...) + for i = types.n, 1, -1 do + if types[i] == "nil" then table.remove(types, i) end + end + + if #types <= 1 then + return tostring(...) + else + return table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] + end +end + + +local function get_display_type(value, t) + -- Lua is somewhat inconsistent in whether it obeys __name just for values which + -- have a per-instance metatable (so tables/userdata) or for everything. We follow + -- Cobalt and only read the metatable for tables/userdata. + if t ~= "table" and t ~= "userdata" then return t end + + local metatable = debug.getmetatable(value) + if not metatable then return t end + + local name = rawget(metatable, "__name") + if type(name) == "string" then return name else return t end +end + +--- Expect an argument to have a specific type. +-- +-- @tparam number index The 1-based argument index. +-- @param value The argument's value. +-- @tparam string ... The allowed types of the argument. +-- @return The given `value`. +-- @throws If the value is not one of the allowed types. +local function expect(index, value, ...) + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return value end + end + + -- If we can determine the function name with a high level of confidence, try to include it. + local name + local ok, info = pcall(debug.getinfo, 3, "nS") + if ok and info.name and info.name ~= "" and info.what ~= "C" then name = info.name end + + t = get_display_type(value, t) + + local type_names = get_type_names(...) + if name then + error(("bad argument #%d to '%s' (%s expected, got %s)"):format(index, name, type_names, t), 3) + else + error(("bad argument #%d (%s expected, got %s)"):format(index, type_names, t), 3) + end +end + +--- Expect an field to have a specific type. +-- +-- @tparam table tbl The table to index. +-- @tparam string index The field name to check. +-- @tparam string ... The allowed types of the argument. +-- @return The contents of the given field. +-- @throws If the field is not one of the allowed types. +local function field(tbl, index, ...) + expect(1, tbl, "table") + expect(2, index, "string") + + local value = tbl[index] + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return value end + end + + t = get_display_type(value, t) + + if value == nil then + error(("field '%s' missing from table"):format(index), 3) + else + error(("bad field '%s' (%s expected, got %s)"):format(index, get_type_names(...), t), 3) + end +end + +local function is_nan(num) + return num ~= num +end + +--- Expect a number to be within a specific range. +-- +-- @tparam number num The value to check. +-- @tparam number min The minimum value, if nil then `-math.huge` is used. +-- @tparam number max The maximum value, if nil then `math.huge` is used. +-- @return The given `value`. +-- @throws If the value is outside of the allowed range. +-- @since 1.96.0 +local function range(num, min, max) + expect(1, num, "number") + min = expect(2, min, "number", "nil") or -math.huge + max = expect(3, max, "number", "nil") or math.huge + if min > max then + error("min must be less than or equal to max)", 2) + end + + if is_nan(num) or num < min or num > max then + error(("number outside of range (expected %s to be within %s and %s)"):format(num, min, max), 3) + end + + return num +end + +return setmetatable({ + expect = expect, + field = field, + range = range, +}, { __call = function(_, ...) return expect(...) end }) diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/image/nft.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/image/nft.lua new file mode 100644 index 000000000..2df2ed962 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/image/nft.lua @@ -0,0 +1,113 @@ +-- SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--- Read and draw nbt ("Nitrogen Fingers Text") images. +-- +-- nft ("Nitrogen Fingers Text") is a file format for drawing basic images. +-- Unlike the images that @{paintutils.parseImage} uses, nft supports coloured +-- text as well as simple coloured pixels. +-- +-- @module cc.image.nft +-- @since 1.90.0 +-- @usage Load an image from `example.nft` and draw it. +-- +-- local nft = require "cc.image.nft" +-- local image = assert(nft.load("data/example.nft")) +-- nft.draw(image, term.getCursorPos()) + +local expect = require "cc.expect".expect + +--- Parse an nft image from a string. +-- +-- @tparam string image The image contents. +-- @return table The parsed image. +local function parse(image) + expect(1, image, "string") + + local result = {} + local line = 1 + local foreground = "0" + local background = "f" + + local i, len = 1, #image + while i <= len do + local c = image:sub(i, i) + if c == "\31" and i < len then + i = i + 1 + foreground = image:sub(i, i) + elseif c == "\30" and i < len then + i = i + 1 + background = image:sub(i, i) + elseif c == "\n" then + if result[line] == nil then + result[line] = { text = "", foreground = "", background = "" } + end + + line = line + 1 + foreground, background = "0", "f" + else + local next = image:find("[\n\30\31]", i) or #image + 1 + local seg_len = next - i + + local this_line = result[line] + if this_line == nil then + this_line = { foreground = "", background = "", text = "" } + result[line] = this_line + end + + this_line.text = this_line.text .. image:sub(i, next - 1) + this_line.foreground = this_line.foreground .. foreground:rep(seg_len) + this_line.background = this_line.background .. background:rep(seg_len) + + i = next - 1 + end + + i = i + 1 + end + return result +end + +--- Load an nft image from a file. +-- +-- @tparam string path The file to load. +-- @treturn[1] table The parsed image. +-- @treturn[2] nil If the file does not exist or could not be loaded. +-- @treturn[2] string An error message explaining why the file could not be +-- loaded. +local function load(path) + expect(1, path, "string") + local file, err = io.open(path, "r") + if not file then return nil, err end + + local result = file:read("*a") + file:close() + return parse(result) +end + +--- Draw an nft image to the screen. +-- +-- @tparam table image An image, as returned from @{load} or @{draw}. +-- @tparam number xPos The x position to start drawing at. +-- @tparam number xPos The y position to start drawing at. +-- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the +-- current terminal. +local function draw(image, xPos, yPos, target) + expect(1, image, "table") + expect(2, xPos, "number") + expect(3, yPos, "number") + expect(4, target, "table", "nil") + + if not target then target = term end + + for y, line in ipairs(image) do + target.setCursorPos(xPos, yPos + y - 1) + target.blit(line.text, line.foreground, line.background) + end +end + +return { + parse = parse, + load = load, + draw = draw, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/error_printer.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/error_printer.lua new file mode 100644 index 000000000..6619bee90 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/error_printer.lua @@ -0,0 +1,177 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- A pretty-printer for Lua errors. + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +This consumes a list of messages and "annotations" and displays the error to the +terminal. + +@see cc.internal.syntax.errors For errors produced by the parser. +@local +]] + +local pretty = require "cc.pretty" +local expect = require "cc.expect" +local expect, field = expect.expect, expect.field +local wrap = require "cc.strings".wrap + +--- Write a message to the screen. +-- @tparam cc.pretty.Doc|string msg The message to write. +local function display(msg) + if type(msg) == "table" then pretty.print(msg) else print(msg) end +end + +-- Write a message to the screen, aligning to the current cursor position. +-- @tparam cc.pretty.Doc|string msg The message to write. +local function display_here(msg, preamble) + expect(1, msg, "string", "table") + local x = term.getCursorPos() + local width, height = term.getSize() + width = width - x + 1 + + local function newline() + local _, y = term.getCursorPos() + if y >= height then + term.scroll(1) + else + y = y + 1 + end + + preamble(y) + term.setCursorPos(x, y) + end + + if type(msg) == "string" then + local lines = wrap(msg, width) + term.write(lines[1]) + for i = 2, #lines do + newline() + term.write(lines[i]) + end + else + local def_colour = term.getTextColour() + local function display_impl(doc) + expect(1, doc, "table") + local kind = doc.tag + if kind == "nil" then return + elseif kind == "text" then + -- TODO: cc.strings.wrap doesn't support a leading indent. We should + -- fix that! + -- Might also be nice to add a wrap_iter, which returns an iterator over + -- start_pos, end_pos instead. + + if doc.colour then term.setTextColour(doc.colour) end + local x1 = term.getCursorPos() + + local lines = wrap((" "):rep(x1 - x) .. doc.text, width) + term.write(lines[1]:sub(x1 - x + 1)) + for i = 2, #lines do + newline() + term.write(lines[i]) + end + + if doc.colour then term.setTextColour(def_colour) end + elseif kind == "concat" then + for i = 1, doc.n do display_impl(doc[i]) end + else + error("Unknown doc " .. kind) + end + end + display_impl(msg) + end + print() +end + +--- A list of colours we can use for error messages. +local error_colours = { colours.red, colours.green, colours.magenta, colours.orange } + +--- The accent line used to denote a block of code. +local code_accent = pretty.text("\x95", colours.cyan) + +--[[- +@tparam { get_pos = function, get_line = function } context + The context where the error was reported. This effectively acts as a view + over the underlying source, exposing the following functions: + - `get_pos`: Get the line and column of an opaque position. + - `get_line`: Get the source code for an opaque position. +@tparam table message The message to display, as produced by @{cc.internal.syntax.errors}. +]] +return function(context, message) + expect(1, context, "table") + expect(2, message, "table") + field(context, "get_pos", "function") + field(context, "get_line", "function") + + if #message == 0 then error("Message is empty", 2) end + + local error_colour = 1 + local width = term.getSize() + + for msg_idx = 1, #message do + if msg_idx > 1 then print() end + + local msg = message[msg_idx] + if type(msg) == "table" and msg.tag == "annotate" then + local line, col = context.get_pos(msg.start_pos) + local end_line, end_col = context.get_pos(msg.end_pos) + local contents = context.get_line(msg.start_pos) + + -- Pick a starting column. We pick the left-most position which fits + -- in one of the following: + -- - 10 characters after the start column. + -- - 5 characters after the end column. + -- - The end of the line. + if line ~= end_line then end_col = #contents end + local start_col = math.max(1, math.min(col + 10, end_col + 5, #contents + 1) - width + 1) + + -- Pick a colour for this annotation. + local colour = colours.toBlit(error_colours[error_colour]) + error_colour = (error_colour % #error_colours) + 1 + + -- Print the line number and snippet of code. We display french + -- quotes on either side of the string if it is truncated. + local str_start, str_end = start_col, start_col + width - 2 + local prefix, suffix = "", "" + if start_col > 1 then + str_start = str_start + 1 + prefix = pretty.text("\xab", colours.grey) + end + if str_end < #contents then + str_end = str_end - 1 + suffix = pretty.text("\xbb", colours.grey) + end + + pretty.print(code_accent .. pretty.text("Line " .. line, colours.cyan)) + pretty.print(code_accent .. prefix .. pretty.text(contents:sub(str_start, str_end), colours.lightGrey) .. suffix) + + -- Print a line highlighting the region of text. + local _, y = term.getCursorPos() + pretty.write(code_accent) + + local indicator_end = end_col + if end_col > str_end then indicator_end = str_end end + + local indicator_len = indicator_end - col + 1 + term.setCursorPos(col - start_col + 2, y) + term.blit(("\x83"):rep(indicator_len), colour:rep(indicator_len), ("f"):rep(indicator_len)) + print() + + -- And then print the annotation's message, if present. + if msg.msg ~= "" then + term.blit("\x95", colour, "f") + display_here(msg.msg, function(y) + term.setCursorPos(1, y) + term.blit("\x95", colour, "f") + end) + end + else + display(msg) + end + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/exception.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/exception.lua new file mode 100644 index 000000000..c2def8c61 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/exception.lua @@ -0,0 +1,124 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- Internal tools for working with errors. + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +@local +]] + +local expect = require "cc.expect".expect +local error_printer = require "cc.internal.error_printer" + +local function find_frame(thread, file, line) + -- Scan the first 16 frames for something interesting. + for offset = 0, 15 do + local frame = debug.getinfo(thread, offset, "Sl") + if not frame then break end + + if frame.short_src == file and frame.what ~= "C" and frame.currentline == line then + return frame + end + end +end + +--[[- Attempt to call the provided function `func` with the provided arguments. + +@tparam function func The function to call. +@param ... Arguments to this function. + +@treturn[1] true If the function ran successfully. + @return[1] ... The return values of the function. + +@treturn[2] false If the function failed. +@return[2] The error message +@treturn[2] coroutine The thread where the error occurred. +]] +local function try(func, ...) + expect(1, func, "function") + + local co = coroutine.create(func) + local result = table.pack(coroutine.resume(co, ...)) + + while coroutine.status(co) ~= "dead" do + local event = table.pack(os.pullEventRaw(result[2])) + if result[2] == nil or event[1] == result[2] or event[1] == "terminate" then + result = table.pack(coroutine.resume(co, table.unpack(event, 1, event.n))) + end + end + + if not result[1] then return false, result[2], co end + return table.unpack(result, 1, result.n) +end + +--[[- Report additional context about an error. + +@param err The error to report. +@tparam coroutine thread The coroutine where the error occurred. +@tparam[opt] { [string] = string } source_map Map of chunk names to their contents. +]] +local function report(err, thread, source_map) + expect(2, thread, "thread") + expect(3, source_map, "table", "nil") + + if type(err) ~= "string" then return end + + local file, line = err:match("^([^:]+):(%d+):") + if not file then return end + line = tonumber(line) + + local frame = find_frame(thread, file, line) + if not frame or not frame.currentcolumn then return end + + local column = frame.currentcolumn + local line_contents + if source_map and source_map[frame.source] then + -- File exists in the source map. + local pos, contents = 1, source_map[frame.source] + -- Try to remap our position. The interface for this only makes sense + -- for single line sources, but that's sufficient for where we need it + -- (the REPL). + if type(contents) == "table" then + column = column - contents.offset + contents = contents.contents + end + + for _ = 1, line - 1 do + local next_pos = contents:find("\n", pos) + if not next_pos then return end + pos = next_pos + 1 + end + + local end_pos = contents:find("\n", pos) + line_contents = contents:sub(pos, end_pos and end_pos - 1 or #contents) + + elseif frame.source:sub(1, 2) == "@/" then + -- Read the file from disk. + local handle = fs.open(frame.source:sub(3), "r") + if not handle then return end + for _ = 1, line - 1 do handle.readLine() end + + line_contents = handle.readLine() + end + + -- Could not determine the line. Bail. + if not line_contents or #line_contents == "" then return end + + error_printer({ + get_pos = function() return line, column end, + get_line = function() return line_contents end, + }, { + { tag = "annotate", start_pos = column, end_pos = column, msg = "" }, + }) +end + + +return { + try = try, + report = report, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/import.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/import.lua new file mode 100644 index 000000000..8b03e3c4c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/import.lua @@ -0,0 +1,82 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- Upload a list of files, as received by the @{event!file_transfer} event. + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +@local +]] + +local completion = require "cc.completion" + +--- @tparam { file_transfer.TransferredFile ...} files The files to upload. +return function(files) + local overwrite = {} + for _, file in pairs(files) do + local filename = file.getName() + local path = shell.resolve(filename) + if fs.exists(path) then + if fs.isDir(path) then + return nil, filename .. " is already a directory." + end + + overwrite[#overwrite + 1] = filename + end + end + + if #overwrite > 0 then + table.sort(overwrite) + printError("The following files will be overwritten:") + textutils.pagedTabulate(colours.cyan, overwrite) + + while true do + io.write("Overwrite? (yes/no) ") + local input = read(nil, nil, function(t) + return completion.choice(t, { "yes", "no" }) + end) + if not input then return end + + input = input:lower() + if input == "" or input == "yes" or input == "y" then + break + elseif input == "no" or input == "n" then + return + end + end + end + + for _, file in pairs(files) do + local filename = file.getName() + print("Transferring " .. filename) + + local path = shell.resolve(filename) + local handle, err = fs.open(path, "wb") + if not handle then return nil, err end + + -- Write the file without loading it all into memory. This uses the same buffer size + -- as BinaryReadHandle. It would be really nice to have a way to do this without + -- multiple copies. + while true do + local chunk = file.read(8192) + if not chunk then break end + + local ok, err = pcall(handle.write, chunk) + if not ok then + handle.close() + + -- Probably an out-of-space issue, just bail. + if err:sub(1, 7) == "pcall: " then err = err:sub(8) end + return nil, "Failed to write file (" .. err .. "). File may be corrupted" + end + end + + handle.close() + end + + return true +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/errors.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/errors.lua new file mode 100644 index 000000000..2fcf9aa10 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/errors.lua @@ -0,0 +1,598 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- The error messages reported by our lexer and parser. + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +This provides a list of factory methods which take source positions and produce +appropriate error messages targeting that location. These error messages can +then be displayed to the user via @{cc.internal.error_printer}. + +@local +]] + +local pretty = require "cc.pretty" +local expect = require "cc.expect".expect +local tokens = require "cc.internal.syntax.parser".tokens + +local function annotate(start_pos, end_pos, msg) + if msg == nil and (type(end_pos) == "string" or type(end_pos) == "table" or type(end_pos) == "nil") then + end_pos, msg = start_pos, end_pos + end + + expect(1, start_pos, "number") + expect(2, end_pos, "number") + expect(3, msg, "string", "table", "nil") + + return { tag = "annotate", start_pos = start_pos, end_pos = end_pos, msg = msg or "" } +end + +--- Format a string as a non-highlighted block of code. +-- +-- @tparam string msg The code to format. +-- @treturn cc.pretty.Doc The formatted code. +local function code(msg) return pretty.text(msg, colours.lightGrey) end + +--- Maps tokens to a more friendly version. +local token_names = setmetatable({ + -- Specific tokens. + [tokens.IDENT] = "identifier", + [tokens.NUMBER] = "number", + [tokens.STRING] = "string", + [tokens.EOF] = "end of file", + -- Symbols and keywords + [tokens.ADD] = code("+"), + [tokens.AND] = code("and"), + [tokens.BREAK] = code("break"), + [tokens.CBRACE] = code("}"), + [tokens.COLON] = code(":"), + [tokens.COMMA] = code(","), + [tokens.CONCAT] = code(".."), + [tokens.CPAREN] = code(")"), + [tokens.CSQUARE] = code("]"), + [tokens.DIV] = code("/"), + [tokens.DO] = code("do"), + [tokens.DOT] = code("."), + [tokens.DOTS] = code("..."), + [tokens.ELSE] = code("else"), + [tokens.ELSEIF] = code("elseif"), + [tokens.END] = code("end"), + [tokens.EQ] = code("=="), + [tokens.EQUALS] = code("="), + [tokens.FALSE] = code("false"), + [tokens.FOR] = code("for"), + [tokens.FUNCTION] = code("function"), + [tokens.GE] = code(">="), + [tokens.GT] = code(">"), + [tokens.IF] = code("if"), + [tokens.IN] = code("in"), + [tokens.LE] = code("<="), + [tokens.LEN] = code("#"), + [tokens.LOCAL] = code("local"), + [tokens.LT] = code("<"), + [tokens.MOD] = code("%"), + [tokens.MUL] = code("*"), + [tokens.NE] = code("~="), + [tokens.NIL] = code("nil"), + [tokens.NOT] = code("not"), + [tokens.OBRACE] = code("{"), + [tokens.OPAREN] = code("("), + [tokens.OR] = code("or"), + [tokens.OSQUARE] = code("["), + [tokens.POW] = code("^"), + [tokens.REPEAT] = code("repeat"), + [tokens.RETURN] = code("return"), + [tokens.SEMICOLON] = code(";"), + [tokens.SUB] = code("-"), + [tokens.THEN] = code("then"), + [tokens.TRUE] = code("true"), + [tokens.UNTIL] = code("until"), + [tokens.WHILE] = code("while"), +}, { __index = function(_, name) error("No such token " .. tostring(name), 2) end }) + +local errors = {} + +-------------------------------------------------------------------------------- +-- Lexer errors +-------------------------------------------------------------------------------- + +--[[- A string which ends without a closing quote. + +@tparam number start_pos The start position of the string. +@tparam number end_pos The end position of the string. +@tparam string quote The kind of quote (`"` or `'`). +@return The resulting parse error. +]] +function errors.unfinished_string(start_pos, end_pos, quote) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + expect(3, quote, "string") + + return { + "This string is not finished. Are you missing a closing quote (" .. code(quote) .. ")?", + annotate(start_pos, "String started here."), + annotate(end_pos, "Expected a closing quote here."), + } +end + +--[[- A string which ends with an escape sequence (so a literal `"foo\`). This +is slightly different from @{unfinished_string}, as we don't want to suggest +adding a quote. + +@tparam number start_pos The start position of the string. +@tparam number end_pos The end position of the string. +@tparam string quote The kind of quote (`"` or `'`). +@return The resulting parse error. +]] +function errors.unfinished_string_escape(start_pos, end_pos, quote) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + expect(3, quote, "string") + + return { + "This string is not finished.", + annotate(start_pos, "String started here."), + annotate(end_pos, "An escape sequence was started here, but with nothing following it."), + } +end + +--[[- A long string was never finished. + +@tparam number start_pos The start position of the long string delimiter. +@tparam number end_pos The end position of the long string delimiter. +@tparam number ;em The length of the long string delimiter, excluding the first `[`. +@return The resulting parse error. +]] +function errors.unfinished_long_string(start_pos, end_pos, len) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + expect(3, len, "number") + + return { + "This string was never finished.", + annotate(start_pos, end_pos, "String was started here."), + "We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this string was started.", + } +end + +--[[- Malformed opening to a long string (i.e. `[=`). + +@tparam number start_pos The start position of the long string delimiter. +@tparam number end_pos The end position of the long string delimiter. +@tparam number len The length of the long string delimiter, excluding the first `[`. +@return The resulting parse error. +]] +function errors.malformed_long_string(start_pos, end_pos, len) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + expect(3, len, "number") + + return { + "Incorrect start of a long string.", + annotate(start_pos, end_pos), + "Tip: If you wanted to start a long string here, add an extra " .. code("[") .. " here.", + } +end + +--[[- Malformed nesting of a long string. + +@tparam number start_pos The start position of the long string delimiter. +@tparam number end_pos The end position of the long string delimiter. +@return The resulting parse error. +]] +function errors.nested_long_str(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + code("[[") .. " cannot be nested inside another " .. code("[[ ... ]]"), + annotate(start_pos, end_pos), + } +end + +--[[- A malformed numeric literal. + +@tparam number start_pos The start position of the number. +@tparam number end_pos The end position of the number. +@return The resulting parse error. +]] +function errors.malformed_number(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + "This isn't a valid number.", + annotate(start_pos, end_pos), + "Numbers must be in one of the following formats: " .. code("123") .. ", " + .. code("3.14") .. ", " .. code("23e35") .. ", " .. code("0x01AF") .. ".", + } +end + +--[[- A long comment was never finished. + +@tparam number start_pos The start position of the long string delimiter. +@tparam number end_pos The end position of the long string delimiter. +@tparam number len The length of the long string delimiter, excluding the first `[`. +@return The resulting parse error. +]] +function errors.unfinished_long_comment(start_pos, end_pos, len) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + expect(3, len, "number") + + return { + "This comment was never finished.", + annotate(start_pos, end_pos, "Comment was started here."), + "We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this comment was started.", + } +end + +--[[- `&&` was used instead of `and`. + +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.wrong_and(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + "Unexpected character.", + annotate(start_pos, end_pos), + "Tip: Replace this with " .. code("and") .. " to check if both values are true.", + } +end + +--[[- `||` was used instead of `or`. + +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.wrong_or(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + "Unexpected character.", + annotate(start_pos, end_pos), + "Tip: Replace this with " .. code("or") .. " to check if either value is true.", + } +end + +--[[- `!=` was used instead of `~=`. + +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.wrong_ne(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + "Unexpected character.", + annotate(start_pos, end_pos), + "Tip: Replace this with " .. code("~=") .. " to check if two values are not equal.", + } +end + +--[[- An unexpected character was used. + +@tparam number pos The position of this character. +@return The resulting parse error. +]] +function errors.unexpected_character(pos) + expect(1, pos, "number") + return { + "Unexpected character.", + annotate(pos, "This character isn't usable in Lua code."), + } +end + +-------------------------------------------------------------------------------- +-- Expression parsing errors +-------------------------------------------------------------------------------- + +--[[- A fallback error when we expected an expression but received another token. + +@tparam number token The token id. +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.expected_expression(token, start_pos, end_pos) + expect(1, token, "number") + expect(2, start_pos, "number") + expect(3, end_pos, "number") + return { + "Unexpected " .. token_names[token] .. ". Expected an expression.", + annotate(start_pos, end_pos), + } +end + +--[[- A fallback error when we expected a variable but received another token. + +@tparam number token The token id. +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.expected_var(token, start_pos, end_pos) + expect(1, token, "number") + expect(2, start_pos, "number") + expect(3, end_pos, "number") + return { + "Unexpected " .. token_names[token] .. ". Expected a variable name.", + annotate(start_pos, end_pos), + } +end + +--[[- `=` was used in an expression context. + +@tparam number start_pos The start position of the `=` token. +@tparam number end_pos The end position of the `=` token. +@return The resulting parse error. +]] +function errors.use_double_equals(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + "Unexpected " .. code("=") .. " in expression.", + annotate(start_pos, end_pos), + "Tip: Replace this with " .. code("==") .. " to check if two values are equal.", + } +end + +--[[- `=` was used after an expression inside a table. + +@tparam number start_pos The start position of the `=` token. +@tparam number end_pos The end position of the `=` token. +@return The resulting parse error. +]] +function errors.table_key_equals(start_pos, end_pos) + expect(1, start_pos, "number") + expect(2, end_pos, "number") + + return { + "Unexpected " .. code("=") .. " in expression.", + annotate(start_pos, end_pos), + "Tip: Wrap the preceding expression in " .. code("[") .. " and " .. code("]") .. " to use it as a table key.", + } +end + +--[[- There is a trailing comma in this list of function arguments. + +@tparam number token The token id. +@tparam number token_start The start position of the token. +@tparam number token_end The end position of the token. +@tparam number prev The start position of the previous entry. +@treturn table The resulting parse error. +]] +function errors.missing_table_comma(token, token_start, token_end, prev) + expect(1, token, "number") + expect(2, token_start, "number") + expect(3, token_end, "number") + expect(4, prev, "number") + + return { + "Unexpected " .. token_names[token] .. " in table.", + annotate(token_start, token_end), + annotate(prev + 1, prev + 1, "Are you missing a comma here?"), + } +end + +--[[- There is a trailing comma in this list of function arguments. + +@tparam number comma_start The start position of the `,` token. +@tparam number comma_end The end position of the `,` token. +@tparam number paren_start The start position of the `)` token. +@tparam number paren_end The end position of the `)` token. +@treturn table The resulting parse error. +]] +function errors.trailing_call_comma(comma_start, comma_end, paren_start, paren_end) + expect(1, comma_start, "number") + expect(2, comma_end, "number") + expect(3, paren_start, "number") + expect(4, paren_end, "number") + + return { + "Unexpected " .. code(")") .. " in function call.", + annotate(paren_start, paren_end), + annotate(comma_start, comma_end, "Tip: Try removing this " .. code(",") .. "."), + } +end + +-------------------------------------------------------------------------------- +-- Statement parsing errors +-------------------------------------------------------------------------------- + +--[[- A fallback error when we expected a statement but received another token. + +@tparam number token The token id. +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.expected_statement(token, start_pos, end_pos) + expect(1, token, "number") + expect(2, start_pos, "number") + expect(3, end_pos, "number") + return { + "Unexpected " .. token_names[token] .. ". Expected a statement.", + annotate(start_pos, end_pos), + } +end + +--[[- `local function` was used with a table identifier. + +@tparam number local_start The start position of the `local` token. +@tparam number local_end The end position of the `local` token. +@tparam number dot_start The start position of the `.` token. +@tparam number dot_end The end position of the `.` token. +@return The resulting parse error. +]] +function errors.local_function_dot(local_start, local_end, dot_start, dot_end) + expect(1, local_start, "number") + expect(2, local_end, "number") + expect(3, dot_start, "number") + expect(4, dot_end, "number") + + return { + "Cannot use " .. code("local function") .. " with a table key.", + annotate(dot_start, dot_end, code(".") .. " appears here."), + annotate(local_start, local_end, "Tip: " .. "Try removing this " .. code("local") .. " keyword."), + } +end + +--[[- A statement of the form `x.y z` + +@tparam number pos The position right after this name. +@return The resulting parse error. +]] +function errors.standalone_name(pos) + expect(1, pos, "number") + + return { + "Unexpected symbol after name.", + annotate(pos), + "Did you mean to assign this or call it as a function?", + } +end + +--[[- A statement of the form `x.y`. This is similar to @{standalone_name}, but +when the next token is on another line. + +@tparam number pos The position right after this name. +@return The resulting parse error. +]] +function errors.standalone_name_call(pos) + expect(1, pos, "number") + + return { + "Unexpected symbol after variable.", + annotate(pos + 1, "Expected something before the end of the line."), + "Tip: Use " .. code("()") .. " to call with no arguments.", + } +end + +--[[- `then` was expected + +@tparam number if_start The start position of the `if`/`elseif` keyword. +@tparam number if_end The end position of the `if`/`elseif` keyword. +@tparam number token_pos The current token position. +@return The resulting parse error. +]] +function errors.expected_then(if_start, if_end, token_pos) + expect(1, if_start, "number") + expect(2, if_end, "number") + expect(3, token_pos, "number") + + return { + "Expected " .. code("then") .. " after if condition.", + annotate(if_start, if_end, "If statement started here."), + annotate(token_pos, "Expected " .. code("then") .. " before here."), + } + +end + +--[[- `end` was expected + +@tparam number block_start The start position of the block. +@tparam number block_end The end position of the block. +@tparam number token The current token position. +@tparam number token_start The current token position. +@tparam number token_end The current token position. +@return The resulting parse error. +]] +function errors.expected_end(block_start, block_end, token, token_start, token_end) + return { + "Unexpected " .. token_names[token] .. ". Expected " .. code("end") .. " or another statement.", + annotate(block_start, block_end, "Block started here."), + annotate(token_start, token_end, "Expected end of block here."), + } +end + +--[[- An unexpected `end` in a statement. + +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.unexpected_end(start_pos, end_pos) + return { + "Unexpected " .. code("end") .. ".", + annotate(start_pos, end_pos), + "Your program contains more " .. code("end") .. "s than needed. Check " .. + "each block (" .. code("if") .. ", " .. code("for") .. ", " .. + code("function") .. ", ...) only has one " .. code("end") .. ".", + } +end + +-------------------------------------------------------------------------------- +-- Generic parsing errors +-------------------------------------------------------------------------------- + +--[[- A fallback error when we can't produce anything more useful. + +@tparam number token The token id. +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.unexpected_token(token, start_pos, end_pos) + expect(1, token, "number") + expect(2, start_pos, "number") + expect(3, end_pos, "number") + + return { + "Unexpected " .. token_names[token] .. ".", + annotate(start_pos, end_pos), + } +end + +--[[- A parenthesised expression was started but not closed. + +@tparam number open_start The start position of the opening bracket. +@tparam number open_end The end position of the opening bracket. +@tparam number tok_start The start position of the opening bracket. +@return The resulting parse error. +]] +function errors.unclosed_brackets(open_start, open_end, token, start_pos, end_pos) + expect(1, open_start, "number") + expect(2, open_end, "number") + expect(3, token, "number") + expect(4, start_pos, "number") + expect(5, end_pos, "number") + + -- TODO: Do we want to be smarter here with where we report the error? + return { + "Unexpected " .. token_names[token] .. ". Are you missing a closing bracket?", + annotate(open_start, open_end, "Brackets were opened here."), + annotate(start_pos, end_pos, "Unexpected " .. token_names[token] .. " here."), + + } +end + +--[[- Expected `(` to open our function arguments. + +@tparam number token The token id. +@tparam number start_pos The start position of the token. +@tparam number end_pos The end position of the token. +@return The resulting parse error. +]] +function errors.expected_function_args(token, start_pos, end_pos) + return { + "Unexpected " .. token_names[token] .. ". Expected " .. code("(") .. " to start function arguments.", + annotate(start_pos, end_pos), + } +end + +return errors diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/init.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/init.lua new file mode 100644 index 000000000..7df278186 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/init.lua @@ -0,0 +1,172 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- The main entrypoint to our Lua parser + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +@local +]] + +local expect = require "cc.expect".expect + +local lex_one = require "cc.internal.syntax.lexer".lex_one +local parser = require "cc.internal.syntax.parser" +local error_printer = require "cc.internal.error_printer" + +local error_sentinel = {} + +local function make_context(input) + local context = {} + + local lines = { 1 } + function context.line(pos) lines[#lines + 1] = pos end + + function context.get_pos(pos) + expect(1, pos, "number") + for i = #lines, 1, -1 do + local start = lines[i] + if pos >= start then return i, pos - start + 1 end + end + + error("Position is <= 0", 2) + end + + function context.get_line(pos) + expect(1, pos, "number") + for i = #lines, 1, -1 do + local start = lines[i] + if pos >= start then return input:match("[^\r\n]*", start) end + end + + error("Position is <= 0", 2) + end + + return context +end + +local function make_lexer(input, context) + local tokens, last_token = parser.tokens, parser.tokens.COMMENT + local pos = 1 + return function() + while true do + local token, start, finish = lex_one(context, input, pos) + if not token then return tokens.EOF, #input + 1, #input + 1 end + + pos = finish + 1 + + if token < last_token then + return token, start, finish + elseif token == tokens.ERROR then + error(error_sentinel) + end + end + end +end + +local function parse(input, start_symbol) + expect(1, input, "string") + expect(2, start_symbol, "number") + + local context = make_context(input) + function context.report(msg) + expect(1, msg, "table") + error_printer(context, msg) + error(error_sentinel) + end + + local ok, err = pcall(parser.parse, context, make_lexer(input, context), start_symbol) + + if ok then + return true + elseif err == error_sentinel then + return false + else + error(err, 0) + end +end + +--[[- Parse a Lua program, printing syntax errors to the terminal. + +@tparam string input The string to parse. +@treturn boolean Whether the string was successfully parsed. +]] +local function parse_program(input) return parse(input, parser.program) end + +--[[- Parse a REPL input (either a program or a list of expressions), printing +syntax errors to the terminal. + +@tparam string input The string to parse. +@treturn boolean Whether the string was successfully parsed. +]] +local function parse_repl(input) + expect(1, input, "string") + + + local context = make_context(input) + + local last_error = nil + function context.report(msg) + expect(1, msg, "table") + last_error = msg + error(error_sentinel) + end + + local lexer = make_lexer(input, context) + + local parsers = {} + for i, start_code in ipairs { parser.repl_exprs, parser.program } do + parsers[i] = coroutine.create(parser.parse) + assert(coroutine.resume(parsers[i], context, coroutine.yield, start_code)) + end + + -- Run all parsers together in parallel, feeding them one token at a time. + -- Once all parsers have failed, report the last failure (corresponding to + -- the longest parse). + local ok, err = pcall(function() + local parsers_n = #parsers + while true do + local token, start, finish = lexer() + + local all_failed = true + for i = 1, parsers_n do + local parser = parsers[i] + if parser then + local ok, err = coroutine.resume(parser, token, start, finish) + if ok then + -- This parser accepted our input, succeed immediately. + if coroutine.status(parser) == "dead" then return end + + all_failed = false -- Otherwise continue parsing. + elseif err ~= error_sentinel then + -- An internal error occurred: propagate it. + error(err, 0) + else + -- The parser failed, stub it out so we don't try to continue using it. + parsers[i] = false + end + end + end + + if all_failed then error(error_sentinel) end + end + end) + + if ok then + return true + elseif err == error_sentinel then + error_printer(context, last_error) + return false + else + error(err, 0) + end +end + +return { + parse_program = parse_program, + parse_repl = parse_repl, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/lexer.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/lexer.lua new file mode 100644 index 000000000..16436b3ea --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/lexer.lua @@ -0,0 +1,363 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- A lexer for Lua source code. + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +This module provides utilities for lexing Lua code, returning tokens compatible +with @{cc.internal.syntax.parser}. While all lexers are roughly the same, there +are some design choices worth drawing attention to: + + - The lexer uses Lua patterns (i.e. @{string.find}) as much as possible, + trying to avoid @{string.sub} loops except when needed. This allows us to + move string processing to native code, which ends up being much faster. + + - We try to avoid allocating where possible. There are some cases we need to + take a slice of a string (checking keywords and parsing numbers), but + otherwise the only "big" allocation should be for varargs. + + - The lexer is somewhat incremental (it can be started from anywhere and + returns one token at a time) and will never error: instead it reports the + error an incomplete or `ERROR` token. + +@local +]] + +local errors = require "cc.internal.syntax.errors" +local tokens = require "cc.internal.syntax.parser".tokens +local sub, find = string.sub, string.find + +local keywords = { + ["and"] = tokens.AND, ["break"] = tokens.BREAK, ["do"] = tokens.DO, ["else"] = tokens.ELSE, + ["elseif"] = tokens.ELSEIF, ["end"] = tokens.END, ["false"] = tokens.FALSE, ["for"] = tokens.FOR, + ["function"] = tokens.FUNCTION, ["if"] = tokens.IF, ["in"] = tokens.IN, ["local"] = tokens.LOCAL, + ["nil"] = tokens.NIL, ["not"] = tokens.NOT, ["or"] = tokens.OR, ["repeat"] = tokens.REPEAT, + ["return"] = tokens.RETURN, ["then"] = tokens.THEN, ["true"] = tokens.TRUE, ["until"] = tokens.UNTIL, + ["while"] = tokens.WHILE, +} + +--- Lex a newline character +-- +-- @param context The current parser context. +-- @tparam string str The current string. +-- @tparam number pos The position of the newline character. +-- @tparam string nl The current new line character, either "\n" or "\r". +-- @treturn pos The new position, after the newline. +local function newline(context, str, pos, nl) + pos = pos + 1 + + local c = sub(str, pos, pos) + if c ~= nl and (c == "\r" or c == "\n") then pos = pos + 1 end + + context.line(pos) -- Mark the start of the next line. + return pos +end + + +--- Lex a number +-- +-- @param context The current parser context. +-- @tparam string str The current string. +-- @tparam number start The start position of this number. +-- @treturn number The token id for numbers. +-- @treturn number The end position of this number +local function lex_number(context, str, start) + local pos = start + 1 + + local exp_low, exp_high = "e", "E" + if sub(str, start, start) == "0" then + local next = sub(str, pos, pos) + if next == "x" or next == "X" then + pos = pos + 1 + exp_low, exp_high = "p", "P" + end + end + + while true do + local c = sub(str, pos, pos) + if c == exp_low or c == exp_high then + pos = pos + 1 + c = sub(str, pos, pos) + if c == "+" or c == "-" then + pos = pos + 1 + end + elseif (c >= "0" and c <= "9") or (c >= "a" and c <= "f") or (c >= "A" and c <= "F") or c == "." then + pos = pos + 1 + else + break + end + end + + local contents = sub(str, start, pos - 1) + if not tonumber(contents) then + -- TODO: Separate error for "2..3"? + context.report(errors.malformed_number(start, pos - 1)) + end + + return tokens.NUMBER, pos - 1 +end + +--- Lex a quoted string. +-- +-- @param context The current parser context. +-- @tparam string str The string we're lexing. +-- @tparam number start_pos The start position of the string. +-- @tparam string quote The quote character, either " or '. +-- @treturn number The token id for strings. +-- @treturn number The new position. +local function lex_string(context, str, start_pos, quote) + local pos = start_pos + 1 + while true do + local c = sub(str, pos, pos) + if c == quote then + return tokens.STRING, pos + elseif c == "\n" or c == "\r" or c == "" then + -- We don't call newline here, as that's done for the next token. + context.report(errors.unfinished_string(start_pos, pos, quote)) + return tokens.STRING, pos - 1 + elseif c == "\\" then + c = sub(str, pos + 1, pos + 1) + if c == "\n" or c == "\r" then + pos = newline(context, str, pos + 1, c) + elseif c == "" then + context.report(errors.unfinished_string_escape(start_pos, pos, quote)) + return tokens.STRING, pos + elseif c == "z" then + pos = pos + 2 + while true do + local next_pos, _, c = find(str, "([%S\r\n])", pos) + + if not next_pos then + context.report(errors.unfinished_string(start_pos, #str, quote)) + return tokens.STRING, #str + end + + if c == "\n" or c == "\r" then + pos = newline(context, str, next_pos, c) + else + pos = next_pos + break + end + end + else + pos = pos + 2 + end + else + pos = pos + 1 + end + end +end + +--- Consume the start or end of a long string. +-- @tparam string str The input string. +-- @tparam number pos The start position. This must be after the first `[` or `]`. +-- @tparam string fin The terminating character, either `[` or `]`. +-- @treturn boolean Whether a long string was successfully started. +-- @treturn number The current position. +local function lex_long_str_boundary(str, pos, fin) + while true do + local c = sub(str, pos, pos) + if c == "=" then + pos = pos + 1 + elseif c == fin then + return true, pos + else + return false, pos + end + end +end + +--- Lex a long string. +-- @param context The current parser context. +-- @tparam string str The input string. +-- @tparam number start The start position, after the input boundary. +-- @tparam number len The expected length of the boundary. Equal to 1 + the +-- number of `=`. +-- @treturn number|nil The end position, or @{nil} if this is not terminated. +local function lex_long_str(context, str, start, len) + local pos = start + while true do + pos = find(str, "[%[%]\n\r]", pos) + if not pos then return nil end + + local c = sub(str, pos, pos) + if c == "]" then + local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "]") + if ok and boundary_pos - pos == len then + return boundary_pos + else + pos = boundary_pos + end + elseif c == "[" then + local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[") + if ok and boundary_pos - pos == len and len == 1 then + context.report(errors.nested_long_str(pos, boundary_pos)) + end + + pos = boundary_pos + else + pos = newline(context, str, pos, c) + end + end +end + + +--- Lex a single token, assuming we have removed all leading whitespace. +-- +-- @param context The current parser context. +-- @tparam string str The string we're lexing. +-- @tparam number pos The start position. +-- @treturn number The id of the parsed token. +-- @treturn number The end position of this token. +-- @treturn string|nil The token's current contents (only given for identifiers) +local function lex_token(context, str, pos) + local c = sub(str, pos, pos) + + -- Identifiers and keywords + if (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == "_" then + local _, end_pos = find(str, "^[%w_]+", pos) + if not end_pos then error("Impossible: No position") end + + local contents = sub(str, pos, end_pos) + return keywords[contents] or tokens.IDENT, end_pos, contents + + -- Numbers + elseif c >= "0" and c <= "9" then return lex_number(context, str, pos) + + -- Strings + elseif c == "\"" or c == "\'" then return lex_string(context, str, pos, c) + + elseif c == "[" then + local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[") + if ok then -- Long string + local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos) + if end_pos then return tokens.STRING, end_pos end + + context.report(errors.unfinished_long_string(pos, boundary_pos, boundary_pos - pos)) + return tokens.ERROR, #str + elseif pos + 1 == boundary_pos then -- Just a "[" + return tokens.OSQUARE, pos + else -- Malformed long string, for instance "[=" + context.report(errors.malformed_long_string(pos, boundary_pos, boundary_pos - pos)) + return tokens.ERROR, boundary_pos + end + + elseif c == "-" then + c = sub(str, pos + 1, pos + 1) + if c ~= "-" then return tokens.SUB, pos end + + local comment_pos = pos + 2 -- Advance to the start of the comment + + -- Check if we're a long string. + if sub(str, comment_pos, comment_pos) == "[" then + local ok, boundary_pos = lex_long_str_boundary(str, comment_pos + 1, "[") + if ok then + local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos) + if end_pos then return tokens.COMMENT, end_pos end + + context.report(errors.unfinished_long_comment(pos, boundary_pos, boundary_pos - comment_pos)) + return tokens.ERROR, #str + end + end + + -- Otherwise fall back to a line comment. + local _, end_pos = find(str, "^[^\n\r]*", comment_pos) + return tokens.COMMENT, end_pos + + elseif c == "." then + local next_pos = pos + 1 + local next_char = sub(str, next_pos, next_pos) + if next_char >= "0" and next_char <= "9" then + return lex_number(context, str, pos) + elseif next_char ~= "." then + return tokens.DOT, pos + end + + if sub(str, pos + 2, pos + 2) ~= "." then return tokens.CONCAT, next_pos end + + return tokens.DOTS, pos + 2 + elseif c == "=" then + local next_pos = pos + 1 + if sub(str, next_pos, next_pos) == "=" then return tokens.EQ, next_pos end + return tokens.EQUALS, pos + elseif c == ">" then + local next_pos = pos + 1 + if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end + return tokens.GT, pos + elseif c == "<" then + local next_pos = pos + 1 + if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end + return tokens.GT, pos + elseif c == "~" and sub(str, pos + 1, pos + 1) == "=" then return tokens.NE, pos + 1 + + -- Single character tokens + elseif c == "," then return tokens.COMMA, pos + elseif c == ";" then return tokens.SEMICOLON, pos + elseif c == ":" then return tokens.COLON, pos + elseif c == "(" then return tokens.OPAREN, pos + elseif c == ")" then return tokens.CPAREN, pos + elseif c == "]" then return tokens.CSQUARE, pos + elseif c == "{" then return tokens.OBRACE, pos + elseif c == "}" then return tokens.CBRACE, pos + elseif c == "*" then return tokens.MUL, pos + elseif c == "/" then return tokens.DIV, pos + elseif c == "#" then return tokens.LEN, pos + elseif c == "%" then return tokens.MOD, pos + elseif c == "^" then return tokens.POW, pos + elseif c == "+" then return tokens.ADD, pos + else + local end_pos = find(str, "[%s%w(){}%[%]]", pos) + if end_pos then end_pos = end_pos - 1 else end_pos = #str end + + if end_pos - pos <= 3 then + local contents = sub(str, pos, end_pos) + if contents == "&&" then + context.report(errors.wrong_and(pos, end_pos)) + return tokens.AND, end_pos + elseif contents == "||" then + context.report(errors.wrong_or(pos, end_pos)) + return tokens.OR, end_pos + elseif contents == "!=" or contents == "<>" then + context.report(errors.wrong_ne(pos, end_pos)) + return tokens.NE, end_pos + end + end + + context.report(errors.unexpected_character(pos)) + return tokens.ERROR, end_pos + end +end + +--[[- Lex a single token from an input string. + +@param context The current parser context. +@tparam string str The string we're lexing. +@tparam number pos The start position. +@treturn[1] number The id of the parsed token. +@treturn[1] number The start position of this token. +@treturn[1] number The end position of this token. +@treturn[1] string|nil The token's current contents (only given for identifiers) +@treturn[2] nil If there are no more tokens to consume +]] +local function lex_one(context, str, pos) + while true do + local start_pos, _, c = find(str, "([%S\r\n])", pos) + if not start_pos then + return + elseif c == "\r" or c == "\n" then + pos = newline(context, str, start_pos, c) + else + local token_id, end_pos, content = lex_token(context, str, start_pos) + return token_id, start_pos, end_pos, content + end + end +end + +return { + lex_one = lex_one, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/parser.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/parser.lua new file mode 100644 index 000000000..2ee238c68 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/internal/syntax/parser.lua @@ -0,0 +1,589 @@ +-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- A parser for Lua programs and expressions. + +:::warning +This is an internal module and SHOULD NOT be used in your own code. It may +be removed or changed at any time. +::: + +Most of the code in this module is automatically generated from the Lua grammar, +hence being mostly unreadable! + +@local +]] + +-- Lazily load our map of errors +local errors = setmetatable({}, { + __index = function(self, key) + setmetatable(self, nil) + for k, v in pairs(require "cc.internal.syntax.errors") do self[k] = v end + + return self[key] + end, +}) + +-- Everything below this line is auto-generated. DO NOT EDIT. + +--- A lookup table of valid Lua tokens +local tokens = (function() return {} end)() -- Make tokens opaque to illuaminate. Nasty! +for i, token in ipairs({ + "WHILE", "UNTIL", "TRUE", "THEN", "SUB", "STRING", "SEMICOLON", "RETURN", + "REPEAT", "POW", "OSQUARE", "OR", "OPAREN", "OBRACE", "NUMBER", "NOT", + "NIL", "NE", "MUL", "MOD", "LT", "LOCAL", "LEN", "LE", "IN", "IF", + "IDENT", "GT", "GE", "FUNCTION", "FOR", "FALSE", "EQUALS", "EQ", "EOF", + "END", "ELSEIF", "ELSE", "DOTS", "DOT", "DO", "DIV", "CSQUARE", "CPAREN", + "CONCAT", "COMMA", "COLON", "CBRACE", "BREAK", "AND", "ADD", "COMMENT", + "ERROR", +}) do tokens[token] = i end +setmetatable(tokens, { __index = function(_, name) error("No such token " .. tostring(name), 2) end }) + +--- Read a integer with a given size from a string. +local function get_int(str, offset, size) + if size == 1 then + return str:byte(offset + 1) + elseif size == 2 then + local hi, lo = str:byte(offset + 1, offset + 2) + return hi * 256 + lo + elseif size == 3 then + local b1, b2, b3 = str:byte(offset + 1, offset + 3) + return b1 * 256 + b2 + b3 * 65536 -- Don't ask. + else + error("Unsupported size", 2) + end +end + +--[[ Error handling: + +Errors are extracted from the current parse state in a two-stage process: + - Run a DFA over the current state of the LR1 stack. For each accepting state, + register a parse error. + - Once all possible errors are found, pick the best of these and report it to + the user. + +This process is performed by a tiny register-based virtual machine. The bytecode +for this machine is stored in `error_program`, and the accompanying transition +table in `error_tbl`. + +It would be more efficient to use tables here (`string.byte` is 2-3x slower than +a table lookup) or even define the DFA as a Lua program, however this approach +is much more space efficient - shaving off several kilobytes. + +See https://github.com/let-def/lrgrep/ (namely ./support/lrgrep_runtime.ml) for +more information. +]] + +local function is_same_line(context, previous, token) + local prev_line = context.get_pos(previous) + local tok_line = context.get_pos(token.s) + return prev_line == tok_line and token.v ~= tokens.EOF +end + +local function line_end_position(context, previous, token) + if is_same_line(context, previous, token) then + return token.s + else + return previous + 1 + end +end + +local expr_tokens = {} +for _, v in pairs { tokens.STRING, tokens.NUMBER, tokens.TRUE, tokens.FALSE, tokens.NIL } do + expr_tokens[v] = true +end + +local error_messages = { + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 26 + if token.v == tokens.EQUALS then + return errors.table_key_equals(token.s, token.e) + end + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 34 + if token.v == tokens.EQUALS then + return errors.use_double_equals(token.s, token.e) + end + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 42 + if expr_tokens[token.v] then + return errors.missing_table_comma(token.v, token.s, token.e, stack[stack_n + 2]) + end + end, + function(context, stack, stack_n, regs, token) + local comma = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 52 + if token.v == tokens.CPAREN then + return errors.trailing_call_comma(comma.s, comma.e, token.s, token.e) + end + end, + function(context, stack, stack_n, regs, token) + local lp = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 60 + return errors.unclosed_brackets(lp.s, lp.e, token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + local lp = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 62 + return errors.unclosed_brackets(lp.s, lp.e, token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + local lp = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 64 + return errors.unclosed_brackets(lp.s, lp.e, token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + local loc = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 69 + if token.v == tokens.DOT then + return errors.local_function_dot(loc.s, loc.e, token.s, token.e) + end + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 77 + local end_pos = stack[stack_n + 2] -- Hack to get the last position + if is_same_line(context, end_pos, token) then + return errors.standalone_name(token.s) + else + return errors.standalone_name_call(end_pos) + end + end, + function(context, stack, stack_n, regs, token) + local start = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 88 + return errors.expected_then(start.s, start.e, line_end_position(context, stack[stack_n + 2], token)) + end, + function(context, stack, stack_n, regs, token) + local start = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + -- parse_errors.mlyl, line 116 + return errors.expected_end(start.s, start.e, token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + local func = { s = stack[regs[2] + 1], e = stack[regs[2] + 2] } + local loc = { s = stack[regs[3] + 1], e = stack[regs[3] + 2] } + -- parse_errors.mlyl, line 120 + return errors.expected_end(loc.s, func.e, token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 124 + if token.v == tokens.END then + return errors.unexpected_end(token.s, token.e) + elseif token ~= tokens.EOF then + return errors.expected_statement(token.v, token.s, token.e) + end + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 134 + return errors.expected_function_args(token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 138 + return errors.expected_expression(token.v, token.s, token.e) + end, + function(context, stack, stack_n, regs, token) + -- parse_errors.mlyl, line 142 + return errors.expected_var(token.v, token.s, token.e) + end, +} +local error_program_start, error_program = 471, "\6\1\0\3\5\186\0\3\6B\0\3\0060\0\3\6(\0\3\6 \0\3\6\21\0\3\6\7\0\3\5\253\0\3\5\236\0\3\5\223\0\1\0\3\5\203\0\3\5\195\0\1\0\3\5\186\0\3\5\178\0\3\5\170\0\3\5\170\0\3\5\170\0\3\5\170\0\3\5\170\0\3\5\146\0\3\5\146\0\3\5\170\0\3\5\162\0\3\5\154\0\3\5\154\0\3\5\154\0\3\5\146\0\3\5\138\0\3\5p\0\3\5h\0\3\5`\0\3\5X\0\3\5P\0\3\4\158\0\3\5F\0\3\5B\0\3\0057\0\3\4\171\0\3\4\167\0\3\4\159\0\3\1\226\0\3\4\158\0\3\4\152\0\3\4\146\0\3\4\141\0\3\4\131\0\3\4{\0\3\4p\0\3\4O\0\3\4J\0\1\0\3\4E\0\1\0\3\4@\0\3\0043\0\3\4(\0\3\3\139\0\3\3\131\0\3\3\127\0\3\3w\0\3\3g\0\3\3{\0\3\3w\0\3\3w\0\3\3s\0\3\3o\0\3\2\233\0\3\3k\0\3\2\218\0\3\2\218\0\3\3g\0\3\3`\0\3\2\218\0\3\2\233\0\3\2\218\0\3\2\218\0\3\2\218\0\3\2\218\0\3\2\225\0\3\2\218\0\3\2\218\0\3\2\218\0\3\2\218\0\3\2Q\0\3\2I\0\3\2=\0\3\2E\0\3\2A\0\3\2\25\0\3\2\21\0\3\2=\0\3\0029\0\3\0025\0\3\0021\0\3\2\17\0\3\2-\0\3\2!\0\3\2)\0\3\2%\0\3\2!\0\3\2\29\0\3\2\25\0\3\2\21\0\3\2\17\0\3\2\r\0\3\2\t\0\3\2\5\0\3\2\1\0\3\2\1\0\3\1\253\0\3\1\249\0\3\1\245\0\3\1\245\0\3\1\235\0\3\1\241\0\3\1\235\0\3\1\226\0\5\0\0\3\4\136\0\3\6M\0\5\0\14\1\0\3\4@\0\1\0\3\4@\0\3\6M\0\3\6U\0\3\6U\0\3\1\249\0\3\6^\0\3\6b\0\3\4J\0\3\6f\0\3\2)\0\3\4p\0\3\2\21\0\3\2\25\0\3\2\17\0\3\2I\0\3\2=\0\3\0029\0\3\2-\0\3\6j\0\3\2!\0\3\2%\0\3\6\142\0\3\2A\0\3\6\142\0\3\2\21\0\5\0\5\1\0\3\4@\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\210\0\3\6\210\0\3\6\210\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\3\2\218\0\3\2\218\0\3\2\218\0\3\3w\0\5\0\190\3\6\149\0\3\3g\0\3\7.\0\5\0\8\3\7\182\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\210\0\3\6\210\0\3\6\210\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\5\1\3\3\6\149\0\3\3w\0\3\7\189\0\3\7.\0\3\7\182\0\3\2\218\0\3\7\193\0\3\3\131\0\3\7\197\0\3\7\r\0\5\0\27\1\0\3\7\29\0\3\7\211\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\4J\0\1\0\3\4@\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\210\0\3\6\210\0\3\6\210\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\3\2\218\0\3\2\218\0\3\2\218\0\3\3w\0\3\7\201\0\3\2I\0\3\6b\0\3\6M\0\5\1f\3\6\149\0\3\1\249\0\4\2\0\0\5\0\31\1\0\3\4E\0\4\4\0\1\6\4\5\0\1\6\4\8\0\0\6\4\12\0\0\6\1\0\3\7\219\0\3\7\243\0\3\7\239\0\3\7\235\0\3\7\228\0\1\0\3\7\219\0\4\12\0\0\5\0\214\3\8\1\0\4\12\0\0\3\8$\0\4\r\0\0\6\4\14\0\0\6\4\15\0\0\6\1\0\3\4@\0\1\0\3\4E\0\6\3\7\211\0\3\2I\0\5\0\18\6\3\7\201\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\4J\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\210\0\3\6\210\0\3\6\210\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\3\2\218\0\3\2\218\0\3\2\218\0\3\3w\0\3\6b\0\5\1\233\3\6\149\0\3\7\235\0\5\0\30\6\3\7\239\0\1\0\3\8+\0\4\1\0\0\5\0B\6\4\1\0\0\3\0080\0\4\1\0\0\3\0084\0\4\1\0\0\3\2=\0\4\1\0\0\3\0088\0\1\0\3\4@\0\3\2%\0\3\2=\0\3\2\21\0\4\1\0\0\5\0T\6\4\1\0\0\3\8<\0\4\1\0\0\3\8@\0\4\1\0\0\3\8D\0\4\1\0\0\3\8H\0\4\1\0\0\3\8L\0\4\12\0\0\4\n\0\1\6\4\r\0\0\3\8P\0\4\14\0\0\3\8Z\0\3\8\1\0\3\7\243\0\3\7\228\0\5\1\19\1\0\3\7\219\0\3\8^\0\4\1\0\0\4\0\0\0\5\0008\1\0\3\8f\0\4\1\0\0\1\0\3\4@\0\4\1\0\0\1\0\3\8o\0\3\7\243\0\4\12\0\0\5\0%\3\8t\0\4\12\0\0\3\7\243\0\4\12\0\0\3\8z\0\4\12\0\0\1\0\3\7\219\0\3\7\243\0\3\8\128\0\4\12\0\0\5\0\224\3\8\132\0\3\1\235\0\3\1\249\0\5\0c\1\0\3\4E\0\3\8\136\0\3\2\t\0\3\8\150\0\3\0021\0\1\0\3\5\186\0\3\0060\0\3\6(\0\3\6 \0\3\6\21\0\1\0\3\5\186\0\3\4O\0\5\1L\3\6B\0\4\1\0\0\6\4\1\0\0\3\8\157\0\4\1\0\0\3\8\165\0\4\1\0\0\3\4p\0\4\1\0\0\3\2%\0\4\1\0\0\3\2I\0\4\1\0\0\3\8\192\0\4\1\0\0\3\t \0\4\1\0\0\3\t\139\0\4\1\0\0\3\t\202\0\4\1\0\0\3\n\17\0\4\1\0\0\3\nT\0\4\4\0\1\4\1\0\0\6\4\6\0\1\4\1\0\0\6\4\t\0\1\4\1\0\0\6\4\2\0\0\4\1\0\0\4\0\0\0\3\6U\0\4\5\0\1\4\2\0\0\4\1\0\0\4\0\0\0\6\3\3s\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\210\0\3\6\210\0\3\6\210\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\3\2\218\0\3\2\218\0\3\2\218\0\3\3w\0\3\6\142\0\5\2L\3\6\149\0\3\2\233\0\3\n\227\0\3\0057\0\3\4\167\0\1\0\3\n\234\0\4\r\0\0\5\0V\6\4\n\0\1\6\3\7\243\0\5\0=\3\8t\0\3\7\243\0\3\8z\0\1\0\3\7\219\0\3\7\243\0\3\8\128\0\5\0\230\3\8\132\0\1\0\3\7\219\0\3\7\243\0\3\7\239\0\3\7\235\0\3\7\228\0\1\0\3\7\219\0\5\1U\3\8\1\0\4\t\0\1\6\3\8\157\0\3\8\165\0\3\8\192\0\3\t \0\3\t\139\0\3\t\202\0\3\n\17\0\3\nT\0\1\0\3\n\234\0\3\n\239\0\5\0\198\6\4\2\0\0\3\6U\0\4\5\0\1\4\2\0\0\6\4\6\0\1\6\1\0\3\n\246\0\1\0\3\n\255\0\3\11\7\0\3\11\11\0\3\4\131\0\1\0\3\5\186\0\3\11\15\0\5\0\192\3\0060\0\3\0084\0\3\8^\0\5\0m\1\0\3\8f\0\1\0\3\4@\0\3\2%\0\3\2=\0\3\2\21\0\5\0\148\6\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\5\2\143\3\6\149\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\5\2\212\3\6\149\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\5\2c\3\6\149\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\218\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\5\3\23\3\6\149\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\218\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\5\3 \3\6\149\0\1\0\3\7\29\0\3\7\r\0\1\0\3\7\4\0\1\0\3\6\251\0\1\0\3\6\242\0\3\6\234\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\226\0\3\6\202\0\3\6\202\0\3\6\226\0\3\6\218\0\3\6\210\0\3\6\210\0\3\6\210\0\3\6\202\0\3\6\194\0\3\6\186\0\3\6\178\0\3\6\178\0\3\6\170\0\3\6\162\0\3\6\154\0\3\2\218\0\3\2\218\0\3\2\218\0\3\7\182\0\3\7.\0\3\3w\0\5\3\163\3\6\149\0\4\7\0\1\6\5\0\11\3\11\25\0\2\1\0\1\0\3\11\30\0\4\n\0\1\3\4\158\0\3\7\243\0\3\11'\0\4\12\0\0\1\0\3\n\246\0\4\3\0\1\6\4\11\1\2\6\3\11.\0\5\0\136\3\0112\0\3\7\243\0\3\0116\0\3\11.\0" +local error_tbl_ks, error_tbl_vs, error_tbl = 1, 2, "\0\0\199\1\0\147\2\0\0\3\0\195\0\0\0\5\1K\0\0\0\7\1G\0\0\0\t\0\207\0\0\0\11\1C\0\0\0\r\1?\0\0\0\15\0\223\16\0\187\17\0\213\18\1\211\19\0\167\20\1\207\21\0\0\22\1\203\23\0\179\24\1\163\25\1\199\26\0\11\27\0'\28\1;\29\0017\30\0013\31\0\151 \1/!\1+\"\1'\21\1\222$\1#%\1\31\0\0\0'\1\27(\1\23)\1\19*\0\27\0\0\0,\0o\0\0\0.\0k\0\0\0000\0g\0\0\0002\0c\0\0\0004\0_\0\0\0006\0[\0\0\0008\0W\0\0\0:\0S\0\0\0<\0O\0\0\0>\0K\0\0\0@\0G\0\0\0B\0C\0\0\0D\0?\0\0\0F\0;G\0\235H\0\213I\1OJ\0+K\0wL\0\179M\1\15N\0sO\0\0P\0\231Q\0\0R\0\0S\1\11T\1\7U\1\3V\0\255W\0\251X\0\27Y\0\0R\2\229[\0\131\\\0\227R\0\0^\0\127_\0\219`\1\195a\1\191b\1\187c\1\183d\0#e\0\175f\0\247g\0\31h\0\243i\0\239j\0\135k\0\7l\0'm\1\159n\1\155o\1\151p\1\147q\0\199r\0\147n\2Mt\0{u\0\0v\0\183w\0001x\0\23y\0'z\1\143{\0\159|\1\139b\3\135~\1\135\127\0\183\128\0\155b\4/\130\0\135\131\0\19\132\0\147\133\0\139\134\1\131u\4\163\136\0\135\137\0\15\138\0\143\139\0005\140\0'\141\1\127\142\0\183\143\0\163\144\0\187\145\0\0\146\1\179\147\0\0\148\1\175\149\0\23\150\0'\151\1{\152\0\183\153\0\171b\5\232\155\0\135H\5x\157\0\135\158\0\7\159\0'\160\1w\131\5>\162\0\135\163\0\7\164\0'\165\1s\166\0\0\137\5>\168\0\0\169\0\7\170\0'\171\1o\172\0\1\173\0'\174\1k\175\1g\176\1c\177\1_\178\1[\179\0\0\180\0\203\144\6\17\182\1W\183\0\183\184\1\171\185\1\167\186\0\191\187\1S\188\0\0\189\0\0\190\0\0\191\0\0\192\0\0\3\0\0n\5\134\129\5J\6\2\202b\6Q\8\2p\t\2Z\n\2j\135\5J\12\2\206u\7\205\14\2\210\144\7\224\8\8Vb\8\161\16\8\140}\5\130" .. ("\0"):rep(15) .. "\17\8V\0\0\0\0\0\0\0\0\0\0\0\0H\8\174\0\0\0\0\0\0\0\0\0\0\0\0#\2jZ\11#\0\0\0&\2\214]\11#\0\0\0\0\0\0\0\0\0+\2\170\0\0\0-\2\166\0\0\0/\2\162\0\0\0001\2\158\26\4Z3\2\154\0\0\0005\2\150\0\0\0007\2\146\0\0\0009\2\142\0\0\0;\2\138\0\0\0=\2\134\0\0\0?\2\130\0\0\0A\2~\0\0\0C\2zn\8\188E\2v\0\0\0}\11#H\2p\0\0\0J\2\174\181\5~\0\0\0\8\3\6\t\2\240\n\3\0H\8V\0\0\0\0\0\0}\8\184" .. ("\0"):rep(18) .. "Z\2\198\0\0\0\0\0\0]\2\194\0\0\0\0\0\0\0\0\0\0\0\0b\2`\0\0\0\154\11#\0\0\0\0\0\0\0\0\0#\3\0\0\0\0\0\0\0I\6>\0\0\0\0\0\0n\2\190\26\5\215+\3@I\7\253-\3\137\4^]\3X\139\4j\0\0\0\0\0\0~\7\253b\2\246\26\6t\0\0\0\0\0\0\3\3\170\0\0\0\149\4f\6\4\8\0\0\0\8\3\164\26\8\14\n\3\158n\3T\12\4\12\181\2\178\14\4\16\183\0\0s\3P\17\3\174\0\0\0\0\0\0\0\0\0\21\4$\0\0\0\0\0\0k\5\211\0\0\0}\3L\0\0\0\172\4T\0\0\0\129\2\250\0\0\0\0\0\0\0\0\0\168\6>#\3\158\135\2\250x\5\219&\4\20\0\0\0\168\7\253\0\0\0\0\0\0+\3\232\0\0\0-\3\228\0\0\0/\3\224\182\6>1\3\220\0\0\0003\3\216\0\0\0005\3\212\182\7\2537\3\208\190\6>9\3\204\0\0\0;\3\200\0\0\0=\3\196\190\7\253?\3\192\0\0\0A\3\188\149\5\219C\3\184\0\0\0E\3\180\0\0\0\0\0\0H\3\164\0\0\0J\3\236\158\5\211\0\0\0\0\0\0\0\0\0\0\0\0\163\5\211\0\0\0\181\3H\0\0\0\0\0\0\0\0\0\169\5\211q\6\138\0\0\0\0\0\0Z\4\4\0\0\0\0\0\0]\4\0x\6\128\0\0\0q" .. ("\0"):rep(20) .. "x\8\26\0\0\0\131\6|\0\0\0\0\0\0\0\0\0\0\0\0n\3\252\137\6x\0\0\0\139\6\132\131\8\22s\3\248\0\0\0u\4\28v\3\148\0\0\0\137\8\18\0\0\0\139\8\30\149\6\128\0\0\0}\3\244\0\0\0\127\4\24\0\0\0\129\3\152\0\0\0\0\0\0\149\8\26\0\0\0\3\4\203\135\3\152\0\0\0\6\5#\0\0\0\8\4\197\t\4\175\n\4\191\142\3\148\12\5'\0\0\0\14\5+\172\6n" .. ("\0"):rep(15) .. "\152\0\0\0\0\0\0\0\0\172\8\8" .. ("\0"):rep(30) .. "#\4\191\0\0\0\0\0\0&\5/\0\0\0\0\0\0\0\0\0\0\0\0+\5\3\0\0\0-\4\255\0\0\0/\4\251\0\0\0001\4\247\181\3\2403\4\243\183\4 5\4\239\0\0\0007\4\235\0\0\0009\4\231\0\0\0;\4\227\0\0\0=\4\223\0\0\0?\4\219\0\0\0A\4\215\0\0\0C\4\211\0\0\0E\4\207\0\0\0\0\0\0H\4\197\0\0\0J\5\7" .. ("\0"):rep(45) .. "Z\5\31\0\0\0\0\0\0]\5\27\0\0\0\0\0\0\0\0\0\0\0\0b\4\181\0\0\0\0\0\0\0\0\0\3\7\178\0\0\0\0\0\0\6\7\162\0\0\0\8\7H\t\0072\n\7Bn\5\23\12\7\166\0\0\0\14\7\170\0\0\0s\5\19" .. ("\0"):rep(27) .. "}\5\15\0\0\0\0\0\0\0\0\0\129\4\185\8\t\168\t\t\146\n\t\162\0\0\0#\7B\135\4\185\0\0\0&\7\174\0\0\0\0\0\0\0\0\0\0\0\0+\7\130\0\0\0-\7~\0\0\0/\7z\0\0\0001\7v\0\0\0003\7r\0\0\0005\7n\0\0\0007\7j\0\0\0009\7f#\t\162;\7b\0\0\0=\7^\0\0\0?\7Z\0\0\0A\7V\0\0\0C\7R\0\0\0E\7N\0\0\0\0\0\0H\7H\0\0\0J\7\134\8\8\218\t\8\196\n\8\212\0\0\0\0\0\0\0\0\0\0\0\0\181\5\11\0\0\0\183\0053" .. ("\0"):rep(15) .. "Z\7\158\0\0\0\0\0\0]\7\154\0\0\0H\t\168\0\0\0J\t\174b\0078\0\0\0\0\0\0\0\0\0#\8\212" .. ("\0"):rep(21) .. "n\7\150\0\0\0\0\0\0Z\t\198\0\0\0s\7\146]\t\194\0\0\0003\t\0\0\0\0005\8\252b\t\1527\8\248\0\0\0\0\0\0}\7\142;\8\244\0\0\0=\8\240\129\7 1 then top = top - 3 end + pc = get_int(error_program, pc + 1, 3) + elseif instruction == 4 then -- Accept + local clause, _, count = error_program:byte(pc + 2, pc + 4) + local accept = { clause + 1 } + for i = 1, count do accept[i + 1] = registers[count - i + 1] end + messages[#messages + 1] = accept + + pc = pc + 4 + elseif instruction == 5 then -- Match + local hi, lo = error_program:byte(pc + 2, pc + 3) + local lr1 = stack[top] - 1 + + local offset = (hi * 256 + lo + lr1) * (error_tbl_ks + error_tbl_vs) + if offset + error_tbl_ks + error_tbl_vs <= #error_tbl and + get_int(error_tbl, offset, error_tbl_ks) == lr1 then + pc = get_int(error_tbl, offset + error_tbl_ks, error_tbl_vs) + else + pc = pc + 3 + end + elseif instruction == 6 then -- Halt + break + else + error("Illegal instruction while handling errors " .. tostring(instruction)) + end + end + + -- Sort the list to ensure earlier patterns are used first. + table.sort(messages, function(a, b) return a[1] < b[1] end) + + -- Then loop until we find an error message which actually works! + local t = { v = token, s = token_start, e = token_end } + for i = 1, #messages do + local action = messages[i] + local message = error_messages[action[1]](context, stack, stack_n, action, t) + if message then + context.report(message) + return false + end + end + + context.report(errors.unexpected_token(token, token_start, token_end)) + return false +end + +--- The list of productions in our grammar. Each is a tuple of `terminal * production size`. +local productions = { + { 53, 1 }, { 52, 1 }, { 81, 1 }, { 81, 1 }, { 80, 3 }, { 79, 1 }, + { 79, 1 }, { 79, 1 }, { 79, 1 }, { 79, 1 }, { 79, 1 }, { 79, 1 }, + { 79, 1 }, { 79, 4 }, { 78, 2 }, { 78, 4 }, { 77, 3 }, { 77, 1 }, + { 77, 1 }, { 76, 1 }, { 76, 3 }, { 76, 3 }, { 76, 3 }, { 76, 3 }, + { 76, 3 }, { 76, 3 }, { 76, 3 }, { 76, 3 }, { 76, 3 }, { 76, 3 }, + { 76, 3 }, { 76, 3 }, { 76, 3 }, { 76, 3 }, { 75, 1 }, { 75, 3 }, + { 75, 2 }, { 75, 2 }, { 75, 2 }, { 74, 1 }, { 74, 3 }, { 74, 3 }, + { 73, 0 }, { 73, 2 }, { 73, 3 }, { 73, 1 }, { 73, 2 }, { 72, 1 }, + { 72, 3 }, { 72, 4 }, { 71, 2 }, { 70, 2 }, { 69, 0 }, { 69, 2 }, + { 69, 3 }, { 68, 0 }, { 68, 5 }, { 67, 0 }, { 67, 1 }, { 66, 0 }, + { 66, 1 }, { 65, 1 }, { 65, 3 }, { 64, 1 }, { 64, 3 }, { 63, 1 }, + { 63, 3 }, { 62, 1 }, { 62, 3 }, { 61, 1 }, { 61, 3 }, { 61, 1 }, + { 60, 3 }, { 60, 3 }, { 60, 5 }, { 60, 4 }, { 60, 6 }, { 60, 8 }, + { 60, 9 }, { 60, 11 }, { 60, 7 }, { 60, 2 }, { 60, 4 }, { 60, 6 }, + { 60, 5 }, { 60, 1 }, { 59, 2 }, { 58, 3 }, { 57, 0 }, { 57, 1 }, + { 57, 3 }, { 56, 1 }, { 56, 3 }, { 56, 5 }, { 55, 1 }, { 55, 1 }, + { 54, 1 }, +} + +local f = false + +--[[- The state machine used for our grammar. + +Most LR(1) parsers will encode the transition table in a compact binary format, +optimised for space and fast lookups. However, without access to built-in +bitwise operations, this is harder to justify in Lua. Instead, the transition +table is a 2D lookup table of `action = transitions[state][value]`, where +`action` can be one of the following: + + - `action = false`: This transition is undefined, and thus a parse error. We + use this (rather than nil) to ensure our tables are dense, and thus stored as + arrays rather than maps. + + - `action > 0`: Shift this terminal or non-terminal onto the stack, then + transition to `state = action`. + + - `action < 0`: Apply production `productions[-action]`. This production is a + tuple composed of the next state and the number of values to pop from the + stack. +]] +local transitions = { + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 2, f, f, f, f, f, f, f, f, f, 4, f, 189 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 3 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -51 }, + { 5, -43, f, f, f, f, f, 111, 114, f, f, f, 9, f, f, f, f, f, f, f, f, 118, f, f, f, 130, 16, f, f, 143, 153, f, f, f, -43, -43, -43, -43, f, f, 173, f, f, f, f, f, f, f, 176, f, f, f, f, 32, f, f, f, f, f, 178, 180, f, 181, f, f, f, f, f, f, f, f, 186, 187, f, f, f, f, 188 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 107, f, 41, 42 }, + { -9, -9, f, -9, -9, f, -9, -9, -9, -9, f, -9, -9, f, f, f, f, -9, -9, -9, -9, -9, f, -9, f, -9, -9, -9, -9, -9, -9, f, f, -9, -9, -9, -9, -9, f, f, -9, -9, -9, -9, -9, -9, f, -9, -9, -9, -9 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 106, f, f, 41, 42 }, + { -13, -13, f, -13, -13, f, -13, -13, -13, -13, f, -13, -13, f, f, f, f, -13, -13, -13, -13, -13, f, -13, f, -13, -13, -13, -13, -13, -13, f, f, -13, -13, -13, -13, -13, f, f, -13, -13, -13, -13, -13, -13, f, -13, -13, -13, -13 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 104, f, 41, 42 }, + { f, f, 6, f, 7, 8, f, f, f, f, 11, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 93, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, -89, f, f, f, f, f, 32, f, 96, 102, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 101, f, 41, 42 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 89, f, 41, 42 }, + { -12, -12, f, -12, -12, f, -12, -12, -12, -12, f, -12, -12, f, f, f, f, -12, -12, -12, -12, -12, f, -12, f, -12, -12, -12, -12, -12, -12, f, f, -12, -12, -12, -12, -12, f, f, -12, -12, -12, -12, -12, -12, f, -12, -12, -12, -12 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 88, f, f, 41, 42 }, + { -8, -8, f, -8, -8, f, -8, -8, -8, -8, f, -8, -8, f, f, f, f, -8, -8, -8, -8, -8, f, -8, f, -8, -8, -8, -8, -8, -8, f, f, -8, -8, -8, -8, -8, f, f, -8, -8, -8, -8, -8, -8, f, -8, -8, -8, -8 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 87, f, f, 41, 42 }, + { -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97 }, + { f, f, f, f, f, f, f, f, f, f, f, f, 18, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 27 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, 19, f, f, f, f, -58, f, f, f, f, f, f, f, f, f, 20, f, f, f, f, f, f, f, f, f, f, 21, f, 24, f, f, f, f, f, f, f, f, f, f, f, f, f, 26 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -4, f, -4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -3, f, -3 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -59, f, 22 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, 19, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 20, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 23 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -63, f, -63 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 25 }, + { -5, f, f, f, f, f, f, -5, -5, f, f, f, -5, f, f, f, f, f, f, f, f, -5, f, f, f, -5, -5, f, f, -5, -5, f, f, f, f, -5, f, f, f, f, -5, f, f, f, f, f, f, f, -5 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -62, f, -62 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 28, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 29 }, + { -14, -14, f, -14, -14, f, -14, -14, -14, -14, f, -14, -14, f, f, f, f, -14, -14, -14, -14, -14, f, -14, f, -14, -14, -14, -14, -14, -14, f, f, -14, -14, -14, -14, -14, f, f, -14, -14, -14, -14, -14, -14, f, -14, -14, -14, -14 }, + { -10, -10, f, -10, -10, f, -10, -10, -10, -10, f, -10, -10, f, f, f, f, -10, -10, -10, -10, -10, f, -10, f, -10, -10, -10, -10, -10, -10, f, f, -10, -10, -10, -10, -10, f, f, -10, -10, -10, -10, -10, -10, f, -10, -10, -10, -10 }, + { -11, -11, f, -11, -11, f, -11, -11, -11, -11, f, -11, -11, f, f, f, f, -11, -11, -11, -11, -11, f, -11, f, -11, -11, -11, -11, -11, -11, f, f, -11, -11, -11, -11, -11, f, f, -11, -11, -11, -11, -11, -11, f, -11, -11, -11, -11 }, + { -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48 }, + { -7, -7, f, -7, -7, f, -7, -7, -7, -7, f, -7, -7, f, f, f, f, -7, -7, -7, -7, -7, f, -7, f, -7, -7, -7, -7, -7, -7, f, f, -7, -7, -7, -7, -7, f, f, -7, -7, -7, -7, -7, -7, f, -7, -7, -7, -7 }, + { -6, -6, f, -6, -6, 35, -6, -6, -6, -6, 36, -6, 73, 10, f, f, f, -6, -6, -6, -6, -6, f, -6, f, -6, -6, -6, -6, -6, -6, f, f, -6, -6, -6, -6, -6, f, 80, -6, -6, -6, -6, -6, -6, 82, -6, -6, -6, -6, f, f, f, f, f, f, 84, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 86 }, + { -18, -18, f, -18, -18, -18, -18, -18, -18, -18, -18, -18, -18, -18, f, f, f, -18, -18, -18, -18, -18, f, -18, f, -18, -18, -18, -18, -18, -18, f, f, -18, -18, -18, -18, -18, f, -18, -18, -18, -18, -18, -18, -18, -18, -18, -18, -18, -18 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 43, f, 41, 42 }, + { -70, -70, f, -70, -70, -70, -70, -70, -70, -70, -70, -70, -70, -70, f, f, f, -70, -70, -70, -70, -70, f, -70, f, -70, -70, -70, -70, -70, -70, f, f, -70, -70, -70, -70, -70, f, -70, -70, -70, -70, -70, -70, -70, -70, -70, -70, -70, -70 }, + { -20, -20, f, -20, -20, f, -20, -20, -20, 39, f, -20, -20, f, f, f, f, -20, -20, -20, -20, -20, f, -20, f, -20, -20, -20, -20, -20, -20, f, f, -20, -20, -20, -20, -20, f, f, -20, -20, -20, -20, -20, -20, f, -20, -20, -20, -20 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 40, f, f, 41, 42 }, + { -36, -36, -36, -36, -36, -36, -36, -36, -36, 39, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36, -36 }, + { -72, -72, f, -72, -72, -72, -72, -72, -72, -72, -72, -72, -72, -72, f, f, f, -72, -72, -72, -72, -72, f, -72, f, -72, -72, -72, -72, -72, -72, f, f, -72, -72, -72, -72, -72, f, -72, -72, -72, -72, -72, -72, -72, -72, -72, -72, -72, -72 }, + { -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35, -35 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, 72, f, 56, f, f, f, f, 70, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 45, f, 41, 42 }, + { -24, -24, f, -24, -24, f, -24, -24, -24, f, f, -24, -24, f, f, f, f, -24, 46, 48, -24, -24, f, -24, f, -24, -24, -24, -24, -24, -24, f, f, -24, -24, -24, -24, -24, f, f, -24, 50, -24, -24, -24, -24, f, -24, -24, -24, -24 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 47, f, 41, 42 }, + { -25, -25, f, -25, -25, f, -25, -25, -25, f, f, -25, -25, f, f, f, f, -25, -25, -25, -25, -25, f, -25, f, -25, -25, -25, -25, -25, -25, f, f, -25, -25, -25, -25, -25, f, f, -25, -25, -25, -25, -25, -25, f, -25, -25, -25, -25 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 49, f, 41, 42 }, + { -27, -27, f, -27, -27, f, -27, -27, -27, f, f, -27, -27, f, f, f, f, -27, -27, -27, -27, -27, f, -27, f, -27, -27, -27, -27, -27, -27, f, f, -27, -27, -27, -27, -27, f, f, -27, -27, -27, -27, -27, -27, f, -27, -27, -27, -27 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 51, f, 41, 42 }, + { -26, -26, f, -26, -26, f, -26, -26, -26, f, f, -26, -26, f, f, f, f, -26, -26, -26, -26, -26, f, -26, f, -26, -26, -26, -26, -26, -26, f, f, -26, -26, -26, -26, -26, f, f, -26, -26, -26, -26, -26, -26, f, -26, -26, -26, -26 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 53, f, 41, 42 }, + { -22, -22, f, -22, 44, f, -22, -22, -22, f, f, -22, -22, f, f, f, f, 54, 46, 48, 60, -22, f, 62, f, -22, -22, 64, 66, -22, -22, f, f, 68, -22, -22, -22, -22, f, f, -22, 50, -22, -22, 56, -22, f, -22, -22, 70, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 55, f, 41, 42 }, + { -30, -30, f, -30, 44, f, -30, -30, -30, f, f, -30, -30, f, f, f, f, -30, 46, 48, -30, -30, f, -30, f, -30, -30, -30, -30, -30, -30, f, f, -30, -30, -30, -30, -30, f, f, -30, 50, -30, -30, 56, -30, f, -30, -30, -30, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 57, f, 41, 42 }, + { -28, -28, f, -28, 44, f, -28, -28, -28, f, f, -28, -28, f, f, f, f, -28, 46, 48, -28, -28, f, -28, f, -28, -28, -28, -28, -28, -28, f, f, -28, -28, -28, -28, -28, f, f, -28, 50, -28, -28, 56, -28, f, -28, -28, -28, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 59, f, 41, 42 }, + { -23, -23, f, -23, -23, f, -23, -23, -23, f, f, -23, -23, f, f, f, f, -23, 46, 48, -23, -23, f, -23, f, -23, -23, -23, -23, -23, -23, f, f, -23, -23, -23, -23, -23, f, f, -23, 50, -23, -23, -23, -23, f, -23, -23, -23, -23 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 61, f, 41, 42 }, + { -31, -31, f, -31, 44, f, -31, -31, -31, f, f, -31, -31, f, f, f, f, -31, 46, 48, -31, -31, f, -31, f, -31, -31, -31, -31, -31, -31, f, f, -31, -31, -31, -31, -31, f, f, -31, 50, -31, -31, 56, -31, f, -31, -31, -31, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 63, f, 41, 42 }, + { -32, -32, f, -32, 44, f, -32, -32, -32, f, f, -32, -32, f, f, f, f, -32, 46, 48, -32, -32, f, -32, f, -32, -32, -32, -32, -32, -32, f, f, -32, -32, -32, -32, -32, f, f, -32, 50, -32, -32, 56, -32, f, -32, -32, -32, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 65, f, 41, 42 }, + { -33, -33, f, -33, 44, f, -33, -33, -33, f, f, -33, -33, f, f, f, f, -33, 46, 48, -33, -33, f, -33, f, -33, -33, -33, -33, -33, -33, f, f, -33, -33, -33, -33, -33, f, f, -33, 50, -33, -33, 56, -33, f, -33, -33, -33, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 67, f, 41, 42 }, + { -34, -34, f, -34, 44, f, -34, -34, -34, f, f, -34, -34, f, f, f, f, -34, 46, 48, -34, -34, f, -34, f, -34, -34, -34, -34, -34, -34, f, f, -34, -34, -34, -34, -34, f, f, -34, 50, -34, -34, 56, -34, f, -34, -34, -34, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 69, f, 41, 42 }, + { -29, -29, f, -29, 44, f, -29, -29, -29, f, f, -29, -29, f, f, f, f, -29, 46, 48, -29, -29, f, -29, f, -29, -29, -29, -29, -29, -29, f, f, -29, -29, -29, -29, -29, f, f, -29, 50, -29, -29, 56, -29, f, -29, -29, -29, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 71, f, 41, 42 }, + { -21, -21, f, -21, 44, f, -21, -21, -21, f, f, -21, -21, f, f, f, f, 54, 46, 48, 60, -21, f, 62, f, -21, -21, 64, 66, -21, -21, f, f, 68, -21, -21, -21, -21, f, f, -21, 50, -21, -21, 56, -21, f, -21, -21, -21, 58 }, + { -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50, -50 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, -60, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, 74, f, 77, f, f, f, f, f, 37, f, f, 38, 79, f, 41, 42 }, + { f, -61, f, f, f, f, -61, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -61, -61, -61, -61, f, f, f, f, f, -61, f, 75 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 76, f, 41, 42 }, + { -65, -65, f, f, 44, f, -65, -65, -65, f, f, 52, -65, f, f, f, f, 54, 46, 48, 60, -65, f, 62, f, -65, -65, 64, 66, -65, -65, f, f, 68, -65, -65, -65, -65, f, f, -65, 50, f, -65, 56, -65, f, f, -65, 70, 58 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 78 }, + { -17, -17, f, -17, -17, -17, -17, -17, -17, -17, -17, -17, -17, -17, f, f, f, -17, -17, -17, -17, -17, f, -17, f, -17, -17, -17, -17, -17, -17, f, f, -17, -17, -17, -17, -17, f, -17, -17, -17, -17, -17, -17, -17, -17, -17, -17, -17, -17 }, + { -64, -64, f, f, 44, f, -64, -64, -64, f, f, 52, -64, f, f, f, f, 54, 46, 48, 60, -64, f, 62, f, -64, -64, 64, 66, -64, -64, f, f, 68, -64, -64, -64, -64, f, f, -64, 50, f, -64, 56, -64, f, f, -64, 70, 58 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 81 }, + { -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49, -49 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 83 }, + { f, f, f, f, f, 35, f, f, f, f, f, f, 73, 10, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 84, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 85 }, + { -19, -19, f, -19, -19, -19, -19, -19, -19, -19, -19, -19, -19, -19, f, f, f, -19, -19, -19, -19, -19, f, -19, f, -19, -19, -19, -19, -19, -19, f, f, -19, -19, -19, -19, -19, f, -19, -19, -19, -19, -19, -19, -19, -19, -19, -19, -19, -19 }, + { -16, -16, f, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, f, f, f, -16, -16, -16, -16, -16, f, -16, f, -16, -16, -16, -16, -16, -16, f, f, -16, -16, -16, -16, -16, f, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16 }, + { -15, -15, f, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, f, f, f, -15, -15, -15, -15, -15, f, -15, f, -15, -15, -15, -15, -15, -15, f, f, -15, -15, -15, -15, -15, f, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15, -15 }, + { -38, -38, -38, -38, -38, -38, -38, -38, -38, 39, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38 }, + { -39, -39, -39, -39, -39, -39, -39, -39, -39, 39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, 90, f, 56, f, f, f, f, 70, 58 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 91 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 92, f, 41, 42 }, + { f, f, f, f, 44, f, -94, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, f, 56, -94, f, -94, f, 70, 58 }, + { -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, 94, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97, -97 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 95, f, 41, 42 }, + { f, f, f, f, 44, f, -93, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, f, 56, -93, f, -93, f, 70, 58 }, + { f, f, f, f, f, f, 97, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 98, f, -90, f, f, f, f, f, f, 99 }, + { f, f, -95, f, -95, -95, f, f, f, f, -95, f, -95, -95, -95, -95, -95, f, f, f, f, f, -95, f, f, f, -95, f, f, -95, f, -95, f, f, f, f, f, f, -95, f, f, f, f, f, f, f, f, -95 }, + { f, f, -96, f, -96, -96, f, f, f, f, -96, f, -96, -96, -96, -96, -96, f, f, f, f, f, -96, f, f, f, -96, f, f, -96, f, -96, f, f, f, f, f, f, -96, f, f, f, f, f, f, f, f, -96 }, + { f, f, 6, f, 7, 8, f, f, f, f, 11, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 93, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, -89, f, f, f, f, f, 32, f, 96, 100, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 101, f, 41, 42 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -91 }, + { f, f, f, f, 44, f, -92, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, f, 56, -92, f, -92, f, 70, 58 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 103 }, + { -88, -88, f, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, f, f, f, -88, -88, -88, -88, -88, f, -88, f, -88, -88, -88, -88, -88, -88, f, f, -88, -88, -88, -88, -88, f, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, 105, 56, f, f, f, f, 70, 58 }, + { -71, -71, f, -71, -71, -71, -71, -71, -71, -71, -71, -71, -71, -71, f, f, f, -71, -71, -71, -71, -71, f, -71, f, -71, -71, -71, -71, -71, -71, f, f, -71, -71, -71, -71, -71, f, -71, -71, -71, -71, -71, -71, -71, -71, -71, -71, -71, -71 }, + { -37, -37, -37, -37, -37, -37, -37, -37, -37, 39, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37, -37 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, 108, 50, f, f, 56, f, f, f, f, 70, 58 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 109, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 110 }, + { -75, -75, f, f, f, f, -75, -75, -75, f, f, f, -75, f, f, f, f, f, f, f, f, -75, f, f, f, -75, -75, f, f, -75, -75, f, f, f, -75, -75, -75, -75, f, f, -75, f, f, f, f, f, f, f, -75 }, + { f, -60, 6, f, 7, 8, -60, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, -60, -60, -60, -60, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, 74, f, 112, f, f, f, f, f, 37, f, f, 38, 79, f, 41, 42 }, + { f, -44, f, f, f, f, 113, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -44, -44, -44, -44 }, + { f, -45, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -45, -45, -45, -45 }, + { -53, -53, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 115, f, f, f, f, f, f, f, f, f, 4 }, + { f, 116 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 117, f, 41, 42 }, + { -76, -76, f, f, 44, f, -76, -76, -76, f, f, 52, -76, f, f, f, f, 54, 46, 48, 60, -76, f, 62, f, -76, -76, 64, 66, -76, -76, f, f, 68, -76, -76, -76, -76, f, f, -76, 50, f, f, 56, f, f, f, -76, 70, 58 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, 119, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 124, f, f, f, f, f, f, f, 125 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 120 }, + { f, f, f, f, f, f, f, f, f, f, f, f, 18, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 121 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 122, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 123 }, + { -84, -84, f, f, f, f, -84, -84, -84, f, f, f, -84, f, f, f, f, f, f, f, f, -84, f, f, f, -84, -84, f, f, -84, -84, f, f, f, -84, -84, -84, -84, f, f, -84, f, f, f, f, f, f, f, -84 }, + { -68, -68, f, f, f, f, -68, -68, -68, f, f, f, -68, f, f, f, f, f, f, f, f, -68, f, f, f, -68, -68, f, f, -68, -68, f, -68, f, -68, -68, -68, -68, f, f, -68, f, f, f, f, -68, f, f, -68 }, + { -82, -82, f, f, f, f, -82, -82, -82, f, f, f, -82, f, f, f, f, f, f, f, f, -82, f, f, f, -82, -82, f, f, -82, -82, f, 126, f, -82, -82, -82, -82, f, f, -82, f, f, f, f, 128, f, f, -82 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, 127, f, f, f, f, f, f, f, 37, f, f, 38, 79, f, 41, 42 }, + { -83, -83, f, f, f, f, -83, -83, -83, f, f, f, -83, f, f, f, f, f, f, f, f, -83, f, f, f, -83, -83, f, f, -83, -83, f, f, f, -83, -83, -83, -83, f, f, -83, f, f, f, f, 75, f, f, -83 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 129 }, + { -69, -69, f, f, f, f, -69, -69, -69, f, f, f, -69, f, f, f, f, f, f, f, f, -69, f, f, -69, -69, -69, f, f, -69, -69, f, -69, f, -69, -69, -69, -69, f, f, -69, f, f, f, f, -69, f, f, -69 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 131, f, 41, 42 }, + { f, f, f, 132, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, f, 56, f, f, f, f, 70, 58 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, -53, -53, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 133, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -56, -56, -56, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 134 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 135, 136, 140 }, + { -77, -77, f, f, f, f, -77, -77, -77, f, f, f, -77, f, f, f, f, f, f, f, f, -77, f, f, f, -77, -77, f, f, -77, -77, f, f, f, -77, -77, -77, -77, f, f, -77, f, f, f, f, f, f, f, -77 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 137, f, 41, 42 }, + { f, f, f, 138, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, f, 56, f, f, f, f, 70, 58 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, -53, -53, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 139, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -57, -57, -57 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 141, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 142 }, + { -78, -78, f, f, f, f, -78, -78, -78, f, f, f, -78, f, f, f, f, f, f, f, f, -78, f, f, f, -78, -78, f, f, -78, -78, f, f, f, -78, -78, -78, -78, f, f, -78, f, f, f, f, f, f, f, -78 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 144, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 145 }, + { f, f, f, f, f, f, f, f, f, f, f, f, -40, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -40, f, f, f, f, f, f, -40 }, + { f, f, f, f, f, f, f, f, f, f, f, f, 18, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 146, f, f, f, f, f, f, 148, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 150 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 147 }, + { f, f, f, f, f, f, f, f, f, f, f, f, -41, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -41, f, f, f, f, f, f, -41 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 149 }, + { f, f, f, f, f, f, f, f, f, f, f, f, -42, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -42, f, f, f, f, f, f, -42 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 151, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 152 }, + { -85, -85, f, f, f, f, -85, -85, -85, f, f, f, -85, f, f, f, f, f, f, f, f, -85, f, f, f, -85, -85, f, f, -85, -85, f, f, f, -85, -85, -85, -85, f, f, -85, f, f, f, f, f, f, f, -85 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 154, f, f, f, f, f, f, f, 167 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -68, f, f, f, f, f, f, f, 155, f, f, f, f, f, f, f, f, f, f, f, f, -68 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 156, f, 41, 42 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, f, 50, f, f, 56, 157, f, f, f, 70, 58 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 158, f, 41, 42 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, 159, 50, f, f, 56, 162, f, f, f, 70, 58 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 160, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 161 }, + { -79, -79, f, f, f, f, -79, -79, -79, f, f, f, -79, f, f, f, f, f, f, f, f, -79, f, f, f, -79, -79, f, f, -79, -79, f, f, f, -79, -79, -79, -79, f, f, -79, f, f, f, f, f, f, f, -79 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, f, f, f, f, f, f, f, f, 37, f, f, 38, 163, f, 41, 42 }, + { f, f, f, f, 44, f, f, f, f, f, f, 52, f, f, f, f, f, 54, 46, 48, 60, f, f, 62, f, f, f, 64, 66, f, f, f, f, 68, f, f, f, f, f, f, 164, 50, f, f, 56, f, f, f, f, 70, 58 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 165, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 166 }, + { -80, -80, f, f, f, f, -80, -80, -80, f, f, f, -80, f, f, f, f, f, f, f, f, -80, f, f, f, -80, -80, f, f, -80, -80, f, f, f, -80, -80, -80, -80, f, f, -80, f, f, f, f, f, f, f, -80 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 168, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 128 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, 169, f, f, f, f, f, f, f, 37, f, f, 38, 79, f, 41, 42 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 170, f, f, f, f, 75 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 171, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 172 }, + { -81, -81, f, f, f, f, -81, -81, -81, f, f, f, -81, f, f, f, f, f, f, f, f, -81, f, f, f, -81, -81, f, f, -81, -81, f, f, f, -81, -81, -81, -81, f, f, -81, f, f, f, f, f, f, f, -81 }, + { -53, f, f, f, f, f, f, -53, -53, f, f, f, -53, f, f, f, f, f, f, f, f, -53, f, f, f, -53, -53, f, f, -53, -53, f, f, f, f, -53, f, f, f, f, -53, f, f, f, f, f, f, f, -53, f, f, f, f, f, f, f, f, f, 174, f, f, f, f, f, f, f, f, f, 4 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 175 }, + { -73, -73, f, f, f, f, -73, -73, -73, f, f, f, -73, f, f, f, f, f, f, f, f, -73, f, f, f, -73, -73, f, f, -73, -73, f, f, f, -73, -73, -73, -73, f, f, -73, f, f, f, f, f, f, f, -73 }, + { f, -46, f, f, f, f, 177, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -46, -46, -46, -46 }, + { f, -47, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -47, -47, -47, -47 }, + { -54, -54, f, f, f, f, 179, -54, -54, f, f, f, -54, f, f, f, f, f, f, f, f, -54, f, f, f, -54, -54, f, f, -54, -54, f, f, f, -54, -54, -54, -54, f, f, -54, f, f, f, f, f, f, f, -54 }, + { -55, -55, f, f, f, f, f, -55, -55, f, f, f, -55, f, f, f, f, f, f, f, f, -55, f, f, f, -55, -55, f, f, -55, -55, f, f, f, -55, -55, -55, -55, f, f, -55, f, f, f, f, f, f, f, -55 }, + { f, f, f, f, f, 35, f, f, f, f, 36, f, 73, 10, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 80, f, f, f, f, f, f, 82, f, f, f, f, f, f, f, f, f, f, 84, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 86 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 182, f, f, f, f, f, f, f, f, f, f, f, f, 184 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, 183, f, f, f, f, f, f, f, 37, f, f, 38, 79, f, 41, 42 }, + { -74, -74, f, f, f, f, -74, -74, -74, f, f, f, -74, f, f, f, f, f, f, f, f, -74, f, f, f, -74, -74, f, f, -74, -74, f, f, f, -74, -74, -74, -74, f, f, -74, f, f, f, f, 75, f, f, -74 }, + { f, f, f, f, f, f, f, f, f, f, f, f, 9, f, f, f, f, f, f, f, f, f, f, f, f, f, 16, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, f, f, f, 180, f, f, f, f, f, f, f, f, f, f, 185, f, f, f, f, f, 41 }, + { f, f, f, f, f, -70, f, f, f, f, -70, f, -70, -70, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -67, f, f, f, f, f, f, -70, f, f, f, f, f, -67, -70 }, + { f, f, f, f, f, -70, f, f, f, f, -70, f, -70, -70, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -66, f, f, f, f, f, f, -70, f, f, f, f, f, -66, -70 }, + { -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87, -87 }, + { -86, -86, f, f, f, -72, -86, -86, -86, f, -72, f, -72, -72, f, f, f, f, f, f, f, -86, f, f, f, -86, -86, f, f, -86, -86, f, f, f, -86, -86, -86, -86, f, -72, -86, f, f, f, f, f, -72, f, -86 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -1 }, + { f, f, 6, f, 7, 8, f, f, f, f, f, f, 9, 10, 12, 13, 14, f, f, f, f, f, 15, f, f, f, 16, f, f, 17, f, 30, f, f, f, f, f, f, 31, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 32, f, f, f, 33, f, f, 34, f, f, 191, f, f, f, f, f, 193, f, 37, f, f, 38, 79, f, 41, 42 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, 192, f, f, f, f, f, f, f, f, f, f, 75 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -52 }, + { f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, -2 }, +} + +--- Run the parser across a sequence of tokens. +-- +-- @tparam table context The current parser context. +-- @tparam function get_next A stateful function which returns the next token. +-- @treturn boolean Whether the parse succeeded or not. +local function parse(context, get_next, start) + local stack, stack_n = { start or 1, 1, 1 }, 1 + local reduce_stack = {} + + while true do + local token, token_start, token_end = get_next() + local state = stack[stack_n] + local action = transitions[state][token] + + if not action then -- Error + return handle_error(context, stack, stack_n, token, token_start, token_end) + elseif action >= 0 then -- Shift + stack_n = stack_n + 3 + stack[stack_n], stack[stack_n + 1], stack[stack_n + 2] = action, token_start, token_end + elseif action >= -2 then -- Accept + return true + else -- Reduce + -- Reduction is quite complex to get right, as the error code expects the parser + -- to be shifting rather than reducing. Menhir achieves this by making the parser + -- stack be immutable, but that's hard to do efficiently in Lua: instead we track + -- what symbols we've pushed/popped, and only perform this change when we're ready + -- to shift again. + + local popped, pushed = 0, 0 + while true do + -- Look at the current item to reduce + local reduce = productions[-action] + local terminal, to_pop = reduce[1], reduce[2] + + -- Find the state at the start of this production. If to_pop == 0 + -- then use the current state. + local lookback = state + if to_pop > 0 then + pushed = pushed - to_pop + if pushed <= 0 then + -- If to_pop >= pushed, then clear the reduction stack + -- and consult the normal stack. + popped = popped - pushed + pushed = 0 + lookback = stack[stack_n - popped * 3] + else + -- Otherwise consult the stack of temporary reductions. + lookback = reduce_stack[pushed] + end + end + + state = transitions[lookback][terminal] + if not state or state <= 0 then error("reduce must shift!") end + + -- And fetch the next action + action = transitions[state][token] + + if not action then -- Error + return handle_error(context, stack, stack_n, token, token_start, token_end) + elseif action >= 0 then -- Shift + break + elseif action >= -2 then -- Accept + return true + else + pushed = pushed + 1 + reduce_stack[pushed] = state + end + end + + if popped == 1 and pushed == 0 then + -- Handle the easy case: Popped one item and replaced it with another + stack[stack_n] = state + else + -- Otherwise pop and push. + -- FIXME: The positions of everything here are entirely wrong. + local end_pos = stack[stack_n + 2] + stack_n = stack_n - popped * 3 + local start_pos = stack[stack_n + 1] + + for i = 1, pushed do + stack_n = stack_n + 3 + stack[stack_n], stack[stack_n + 1], stack[stack_n + 2] = reduce_stack[i], end_pos, end_pos + end + + stack_n = stack_n + 3 + stack[stack_n], stack[stack_n + 1], stack[stack_n + 2] = state, start_pos, end_pos + end + + -- Shift the token onto the stack + stack_n = stack_n + 3 + stack[stack_n], stack[stack_n + 1], stack[stack_n + 2] = action, token_start, token_end + end + end +end + +return { + tokens = tokens, + parse = parse, + repl_exprs = 190, --[[- The repl_exprs starting state. ]] + program = 1, --[[- The program starting state. ]] +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/pretty.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/pretty.lua new file mode 100644 index 000000000..d0cdf6c41 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/pretty.lua @@ -0,0 +1,526 @@ +-- SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- A pretty printer for rendering data structures in an aesthetically +pleasing manner. + +In order to display something using @{cc.pretty}, you build up a series of +@{Doc|documents}. These behave a little bit like strings; you can concatenate +them together and then print them to the screen. + +However, documents also allow you to control how they should be printed. There +are several functions (such as @{nest} and @{group}) which allow you to control +the "layout" of the document. When you come to display the document, the 'best' +(most compact) layout is used. + +The structure of this module is based on [A Prettier Printer][prettier]. + +[prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer" + +@module cc.pretty +@since 1.87.0 +@usage Print a table to the terminal + + local pretty = require "cc.pretty" + pretty.pretty_print({ 1, 2, 3 }) + +@usage Build a custom document and display it + + local pretty = require "cc.pretty" + pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) +]] + +local expect = require "cc.expect" +local expect, field = expect.expect, expect.field + +local type, getmetatable, setmetatable, colours, str_write, tostring = type, getmetatable, setmetatable, colours, write, tostring +local debug_info, debug_local = debug.getinfo, debug.getlocal + +--- @{table.insert} alternative, but with the length stored inline. +local function append(out, value) + local n = out.n + 1 + out[n], out.n = value, n +end + +--- A document containing formatted text, with multiple possible layouts. +-- +-- Documents effectively represent a sequence of strings in alternative layouts, +-- which we will try to print in the most compact form necessary. +-- +-- @type Doc +local Doc = { } + +local function mk_doc(tbl) return setmetatable(tbl, Doc) end + +--- An empty document. +local empty = mk_doc({ tag = "nil" }) + +--- A document with a single space in it. +local space = mk_doc({ tag = "text", text = " " }) + +--- A line break. When collapsed with @{group}, this will be replaced with @{empty}. +local line = mk_doc({ tag = "line", flat = empty }) + +--- A line break. When collapsed with @{group}, this will be replaced with @{space}. +local space_line = mk_doc({ tag = "line", flat = space }) + +local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line } + +local function mk_text(text, colour) + return text_cache[text] or setmetatable({ tag = "text", text = text, colour = colour }, Doc) +end + +--- Create a new document from a string. +-- +-- If your string contains multiple lines, @{group} will flatten the string +-- into a single line, with spaces between each line. +-- +-- @tparam string text The string to construct a new document with. +-- @tparam[opt] number colour The colour this text should be printed with. If not given, we default to the current +-- colour. +-- @treturn Doc The document with the provided text. +-- @usage Write some blue text. +-- +-- local pretty = require "cc.pretty" +-- pretty.print(pretty.text("Hello!", colours.blue)) +local function text(text, colour) + expect(1, text, "string") + expect(2, colour, "number", "nil") + + local cached = text_cache[text] + if cached then return cached end + + local new_line = text:find("\n", 1) + if not new_line then return mk_text(text, colour) end + + -- Split the string by "\n". With a micro-optimisation to skip empty strings. + local doc = setmetatable({ tag = "concat", n = 0 }, Doc) + if new_line ~= 1 then append(doc, mk_text(text:sub(1, new_line - 1), colour)) end + + new_line = new_line + 1 + while true do + local next_line = text:find("\n", new_line) + append(doc, space_line) + if not next_line then + if new_line <= #text then append(doc, mk_text(text:sub(new_line), colour)) end + return doc + else + if new_line <= next_line - 1 then + append(doc, mk_text(text:sub(new_line, next_line - 1), colour)) + end + new_line = next_line + 1 + end + end +end + +--- Concatenate several documents together. This behaves very similar to string concatenation. +-- +-- @tparam Doc|string ... The documents to concatenate. +-- @treturn Doc The concatenated documents. +-- @usage +-- local pretty = require "cc.pretty" +-- local doc1, doc2 = pretty.text("doc1"), pretty.text("doc2") +-- print(pretty.concat(doc1, " - ", doc2)) +-- print(doc1 .. " - " .. doc2) -- Also supports .. +local function concat(...) + local args = table.pack(...) + for i = 1, args.n do + if type(args[i]) == "string" then args[i] = text(args[i]) end + if getmetatable(args[i]) ~= Doc then expect(i, args[i], "document") end + end + + if args.n == 0 then return empty end + if args.n == 1 then return args[1] end + + args.tag = "concat" + return setmetatable(args, Doc) +end + +Doc.__concat = concat --- @local + +--- Indent later lines of the given document with the given number of spaces. +-- +-- For instance, nesting the document +-- ```txt +-- foo +-- bar +-- ``` +-- by two spaces will produce +-- ```txt +-- foo +-- bar +-- ``` +-- +-- @tparam number depth The number of spaces with which the document should be indented. +-- @tparam Doc doc The document to indent. +-- @treturn Doc The nested document. +-- @usage +-- local pretty = require "cc.pretty" +-- print(pretty.nest(2, pretty.text("foo\nbar"))) +local function nest(depth, doc) + expect(1, depth, "number") + if getmetatable(doc) ~= Doc then expect(2, doc, "document") end + if depth <= 0 then error("depth must be a positive number", 2) end + + return setmetatable({ tag = "nest", depth = depth, doc }, Doc) +end + +local function flatten(doc) + if doc.flat then return doc.flat end + + local kind = doc.tag + if kind == "nil" or kind == "text" then + return doc + elseif kind == "concat" then + local out = setmetatable({ tag = "concat", n = doc.n }, Doc) + for i = 1, doc.n do out[i] = flatten(doc[i]) end + doc.flat, out.flat = out, out -- cache the flattened node + return out + elseif kind == "nest" then + return flatten(doc[1]) + elseif kind == "group" then + return doc[1] + else + error("Unknown doc " .. kind) + end +end + +--- Builds a document which is displayed on a single line if there is enough +-- room, or as normal if not. +-- +-- @tparam Doc doc The document to group. +-- @treturn Doc The grouped document. +-- @usage Uses group to show things being displayed on one or multiple lines. +-- +-- local pretty = require "cc.pretty" +-- local doc = pretty.group("Hello" .. pretty.space_line .. "World") +-- print(pretty.render(doc, 5)) -- On multiple lines +-- print(pretty.render(doc, 20)) -- Collapsed onto one. +local function group(doc) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + + if doc.tag == "group" then return doc end -- Skip if already grouped. + + local flattened = flatten(doc) + if flattened == doc then return doc end -- Also skip if flattening does nothing. + return setmetatable({ tag = "group", flattened, doc }, Doc) +end + +local function get_remaining(doc, width) + local kind = doc.tag + if kind == "nil" or kind == "line" then + return width + elseif kind == "text" then + return width - #doc.text + elseif kind == "concat" then + for i = 1, doc.n do + width = get_remaining(doc[i], width) + if width < 0 then break end + end + return width + elseif kind == "group" or kind == "nest" then + return get_remaining(kind[1]) + else + error("Unknown doc " .. kind) + end +end + +--- Display a document on the terminal. +-- +-- @tparam Doc doc The document to render +-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in. +local function write(doc, ribbon_frac) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + expect(2, ribbon_frac, "number", "nil") + + local term = term + local width, height = term.getSize() + local ribbon_width = (ribbon_frac or 0.6) * width + if ribbon_width < 0 then ribbon_width = 0 end + if ribbon_width > width then ribbon_width = width end + + local def_colour = term.getTextColour() + local current_colour = def_colour + + local function go(doc, indent, col) + local kind = doc.tag + if kind == "nil" then + return col + elseif kind == "text" then + local doc_colour = doc.colour or def_colour + if doc_colour ~= current_colour then + term.setTextColour(doc_colour) + current_colour = doc_colour + end + + str_write(doc.text) + + return col + #doc.text + elseif kind == "line" then + local _, y = term.getCursorPos() + if y < height then + term.setCursorPos(indent + 1, y + 1) + else + term.scroll(1) + term.setCursorPos(indent + 1, height) + end + + return indent + elseif kind == "concat" then + for i = 1, doc.n do col = go(doc[i], indent, col) end + return col + elseif kind == "nest" then + return go(doc[1], indent + doc.depth, col) + elseif kind == "group" then + if get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then + return go(doc[1], indent, col) + else + return go(doc[2], indent, col) + end + else + error("Unknown doc " .. kind) + end + end + + local col = math.max(term.getCursorPos() - 1, 0) + go(doc, 0, col) + if current_colour ~= def_colour then term.setTextColour(def_colour) end +end + +--- Display a document on the terminal with a trailing new line. +-- +-- @tparam Doc doc The document to render. +-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in. +local function print(doc, ribbon_frac) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + expect(2, ribbon_frac, "number", "nil") + write(doc, ribbon_frac) + str_write("\n") +end + +--- Render a document, converting it into a string. +-- +-- @tparam Doc doc The document to render. +-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to fit +-- this width - it is only used for finding the best layout. +-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in. +-- @treturn string The rendered document as a string. +local function render(doc, width, ribbon_frac) + if getmetatable(doc) ~= Doc then expect(1, doc, "document") end + expect(2, width, "number", "nil") + expect(3, ribbon_frac, "number", "nil") + + local ribbon_width + if width then + ribbon_width = (ribbon_frac or 0.6) * width + if ribbon_width < 0 then ribbon_width = 0 end + if ribbon_width > width then ribbon_width = width end + end + + local out = { n = 0 } + local function go(doc, indent, col) + local kind = doc.tag + if kind == "nil" then + return col + elseif kind == "text" then + append(out, doc.text) + return col + #doc.text + elseif kind == "line" then + append(out, "\n" .. (" "):rep(indent)) + return indent + elseif kind == "concat" then + for i = 1, doc.n do col = go(doc[i], indent, col) end + return col + elseif kind == "nest" then + return go(doc[1], indent + doc.depth, col) + elseif kind == "group" then + if not width or get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then + return go(doc[1], indent, col) + else + return go(doc[2], indent, col) + end + else + error("Unknown doc " .. kind) + end + end + + go(doc, 0, 0) + return table.concat(out, "", 1, out.n) +end + +Doc.__tostring = render --- @local + +local keywords = { + ["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, + } + +local comma = text(",") +local braces = text("{}") +local obrace, cbrace = text("{"), text("}") +local obracket, cbracket = text("["), text("] = ") + +local function key_compare(a, b) + local ta, tb = type(a), type(b) + + if ta == "string" then return tb ~= "string" or a < b + elseif tb == "string" then return false + end + + if ta == "number" then return tb ~= "number" or a < b end + return false +end + +local function show_function(fn, options) + local info = debug_info and debug_info(fn, "Su") + + -- Include function source position if available + local name + if options.function_source and info and info.short_src and info.linedefined and info.linedefined >= 1 then + name = "function<" .. info.short_src .. ":" .. info.linedefined .. ">" + else + name = tostring(fn) + end + + -- Include arguments if a Lua function and if available. Lua will report "C" + -- functions as variadic. + if options.function_args and info and info.what == "Lua" and info.nparams and debug_local then + local args = {} + for i = 1, info.nparams do args[i] = debug_local(fn, i) or "?" end + if info.isvararg then args[#args + 1] = "..." end + name = name .. "(" .. table.concat(args, ", ") .. ")" + end + + return name +end + +local function pretty_impl(obj, options, tracking) + local obj_type = type(obj) + if obj_type == "string" then + local formatted = ("%q"):format(obj):gsub("\\\n", "\\n") + return text(formatted, colours.red) + elseif obj_type == "number" then + return text(tostring(obj), colours.magenta) + elseif obj_type == "function" then + return text(show_function(obj, options), colours.lightGrey) + elseif obj_type ~= "table" or tracking[obj] then + return text(tostring(obj), colours.lightGrey) + elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then + return text(tostring(obj)) + elseif next(obj) == nil then + return braces + else + tracking[obj] = true + local doc = setmetatable({ tag = "concat", n = 1, space_line }, Doc) + + local length, keys, keysn = #obj, {}, 1 + for k in pairs(obj) do + if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > length then + keys[keysn], keysn = k, keysn + 1 + end + end + table.sort(keys, key_compare) + + for i = 1, length do + if i > 1 then append(doc, comma) append(doc, space_line) end + append(doc, pretty_impl(obj[i], options, tracking)) + end + + for i = 1, keysn - 1 do + if i > 1 or length >= 1 then append(doc, comma) append(doc, space_line) end + + local k = keys[i] + local v = obj[k] + if type(k) == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then + append(doc, text(k .. " = ")) + append(doc, pretty_impl(v, options, tracking)) + else + append(doc, obracket) + append(doc, pretty_impl(k, options, tracking)) + append(doc, cbracket) + append(doc, pretty_impl(v, options, tracking)) + end + end + + tracking[obj] = nil + return group(concat(obrace, nest(2, concat(table.unpack(doc, 1, doc.n))), space_line, cbrace)) + end +end + +--- Pretty-print an arbitrary object, converting it into a document. +-- +-- This can then be rendered with @{write} or @{print}. +-- +-- @param obj The object to pretty-print. +-- @tparam[opt] { function_args = boolean, function_source = boolean } options +-- Controls how various properties are displayed. +-- - `function_args`: Show the arguments to a function if known (`false` by default). +-- - `function_source`: Show where the function was defined, instead of +-- `function: xxxxxxxx` (`false` by default). +-- @treturn Doc The object formatted as a document. +-- @changed 1.88.0 Added `options` argument. +-- @usage Display a table on the screen +-- +-- local pretty = require "cc.pretty" +-- pretty.print(pretty.pretty({ 1, 2, 3 })) +-- @see pretty_print for a shorthand to prettify and print an object. +local function pretty(obj, options) + expect(2, options, "table", "nil") + options = options or {} + + local actual_options = { + function_source = field(options, "function_source", "boolean", "nil") or false, + function_args = field(options, "function_args", "boolean", "nil") or false, + } + return pretty_impl(obj, actual_options, {}) +end + +--[[- A shortcut for calling @{pretty} and @{print} together. + +@param obj The object to pretty-print. +@tparam[opt] { function_args = boolean, function_source = boolean } options +Controls how various properties are displayed. + - `function_args`: Show the arguments to a function if known (`false` by default). + - `function_source`: Show where the function was defined, instead of + `function: xxxxxxxx` (`false` by default). +@tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in. + +@usage Display a table on the screen. + + local pretty = require "cc.pretty" + pretty.pretty_print({ 1, 2, 3 }) + +@see pretty +@see print +@since 1.99 +]] +local function pretty_print(obj, options, ribbon_frac) + expect(2, options, "table", "nil") + options = options or {} + expect(3, ribbon_frac, "number", "nil") + + return print(pretty(obj, options), ribbon_frac) +end + +return { + empty = empty, + space = space, + line = line, + space_line = space_line, + text = text, + concat = concat, + nest = nest, + group = group, + + write = write, + print = print, + render = render, + + pretty = pretty, + + pretty_print = pretty_print, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/require.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/require.lua new file mode 100644 index 000000000..b6ae8bbee --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/require.lua @@ -0,0 +1,148 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--[[- A pure Lua implementation of the builtin @{require} function and +@{package} library. + +Generally you do not need to use this module - it is injected into the every +program's environment. However, it may be useful when building a custom shell or +when running programs yourself. + +@module cc.require +@since 1.88.0 +@see using_require For an introduction on how to use @{require}. +@usage Construct the package and require function, and insert them into a +custom environment. + + local r = require "cc.require" + local env = setmetatable({}, { __index = _ENV }) + env.require, env.package = r.make(env, "/") + + -- Now we have our own require function, separate to the original. + local r2 = env.require "cc.require" + print(r, r2) +]] + +local expect = require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua") +local expect = expect.expect + +local function preload(package) + return function(name) + if package.preload[name] then + return package.preload[name] + else + return nil, "no field package.preload['" .. name .. "']" + end + end +end + +local function from_file(package, env) + return function(name) + local sPath, sError = package.searchpath(name, package.path) + if not sPath then + return nil, sError + end + local fnFile, sError = loadfile(sPath, nil, env) + if fnFile then + return fnFile, sPath + else + return nil, sError + end + end +end + +local function make_searchpath(dir) + return function(name, path, sep, rep) + expect(1, name, "string") + expect(2, path, "string") + sep = expect(3, sep, "string", "nil") or "." + rep = expect(4, rep, "string", "nil") or "/" + + local fname = string.gsub(name, sep:gsub("%.", "%%%."), rep) + local sError = "" + for pattern in string.gmatch(path, "[^;]+") do + local sPath = string.gsub(pattern, "%?", fname) + if sPath:sub(1, 1) ~= "/" then + sPath = fs.combine(dir, sPath) + end + if fs.exists(sPath) and not fs.isDir(sPath) then + return sPath + else + if #sError > 0 then + sError = sError .. "\n " + end + sError = sError .. "no file '" .. sPath .. "'" + end + end + return nil, sError + end +end + +local function make_require(package) + local sentinel = {} + return function(name) + expect(1, name, "string") + + if package.loaded[name] == sentinel then + error("loop or previous error loading module '" .. name .. "'", 0) + end + + if package.loaded[name] then + return package.loaded[name] + end + + local sError = "module '" .. name .. "' not found:" + for _, searcher in ipairs(package.loaders) do + local loader = table.pack(searcher(name)) + if loader[1] then + package.loaded[name] = sentinel + local result = loader[1](name, table.unpack(loader, 2, loader.n)) + if result == nil then result = true end + + package.loaded[name] = result + return result + else + sError = sError .. "\n " .. loader[2] + end + end + error(sError, 2) + end +end + +--- Build an implementation of Lua's @{package} library, and a @{require} +-- function to load modules within it. +-- +-- @tparam table env The environment to load packages into. +-- @tparam string dir The directory that relative packages are loaded from. +-- @treturn function The new @{require} function. +-- @treturn table The new @{package} library. +local function make_package(env, dir) + expect(1, env, "table") + expect(2, dir, "string") + + local package = {} + package.loaded = { + _G = _G, + bit32 = bit32, + coroutine = coroutine, + math = math, + package = package, + string = string, + table = table, + } + package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua" + if turtle then + package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua" + elseif commands then + package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua" + end + package.config = "/\n;\n?\n!\n-" + package.preload = {} + package.loaders = { preload(package), from_file(package, env) } + package.searchpath = make_searchpath(dir) + + return make_require(package), package +end + +return { make = make_package } diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/shell/completion.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/shell/completion.lua new file mode 100644 index 000000000..a701e571c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/shell/completion.lua @@ -0,0 +1,208 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--[[- A collection of helper methods for working with shell completion. + +Most programs may be completed using the @{build} helper method, rather than +manually switching on the argument index. + +Note, the helper functions within this module do not accept an argument index, +and so are not directly usable with the @{shell.setCompletionFunction}. Instead, +wrap them using @{build}, or your own custom function. + +@module cc.shell.completion +@since 1.85.0 +@see cc.completion For more general helpers, suitable for use with @{_G.read}. +@see shell.setCompletionFunction + +@usage Register a completion handler for example.lua which prompts for a +choice of options, followed by a directory, and then multiple files. + + local completion = require "cc.shell.completion" + local complete = completion.build( + { completion.choice, { "get", "put" } }, + completion.dir, + { completion.file, many = true } + ) + shell.setCompletionFunction("example.lua", complete) + read(nil, nil, shell.complete, "example ") +]] + +local expect = require "cc.expect".expect +local completion = require "cc.completion" + +--- Complete the name of a file relative to the current working directory. +-- +-- @tparam table shell The shell we're completing in. +-- @tparam string text Current text to complete. +-- @treturn { string... } A list of suffixes of matching files. +local function file(shell, text) + return fs.complete(text, shell.dir(), { + include_files = true, + include_dirs = false, + include_hidden = settings.get("shell.autocomplete_hidden"), + }) +end + +--- Complete the name of a directory relative to the current working directory. +-- +-- @tparam table shell The shell we're completing in. +-- @tparam string text Current text to complete. +-- @treturn { string... } A list of suffixes of matching directories. +local function dir(shell, text) + return fs.complete(text, shell.dir(), { + include_files = false, + include_dirs = true, + include_hidden = settings.get("shell.autocomplete_hidden"), + }) +end + +--- Complete the name of a file or directory relative to the current working +-- directory. +-- +-- @tparam table shell The shell we're completing in. +-- @tparam string text Current text to complete. +-- @tparam { string... } previous The shell arguments before this one. +-- @tparam[opt] boolean add_space Whether to add a space after the completed item. +-- @treturn { string... } A list of suffixes of matching files and directories. +local function dirOrFile(shell, text, previous, add_space) + local results = fs.complete(text, shell.dir(), { + include_files = true, + include_dirs = true, + include_hidden = settings.get("shell.autocomplete_hidden"), + }) + if add_space then + for n = 1, #results do + local result = results[n] + if result:sub(-1) ~= "/" then + results[n] = result .. " " + end + end + end + return results +end + +local function wrap(func) + return function(shell, text, previous, ...) + return func(text, ...) + end +end + +--- Complete the name of a program. +-- +-- @tparam table shell The shell we're completing in. +-- @tparam string text Current text to complete. +-- @treturn { string... } A list of suffixes of matching programs. +-- @see shell.completeProgram +local function program(shell, text) + return shell.completeProgram(text) +end + +--- Complete arguments of a program. +-- +-- @tparam table shell The shell we're completing in. +-- @tparam string text Current text to complete. +-- @tparam { string... } previous The shell arguments before this one. +-- @tparam number starting Which argument index this program and args start at. +-- @treturn { string... } A list of suffixes of matching programs or arguments. +-- @since 1.97.0 +local function programWithArgs(shell, text, previous, starting) + if #previous + 1 == starting then + local tCompletionInfo = shell.getCompletionInfo() + if text:sub(-1) ~= "/" and tCompletionInfo[shell.resolveProgram(text)] then + return { " " } + else + local results = shell.completeProgram(text) + for n = 1, #results do + local sResult = results[n] + if sResult:sub(-1) ~= "/" and tCompletionInfo[shell.resolveProgram(text .. sResult)] then + results[n] = sResult .. " " + end + end + return results + end + else + local program = previous[starting] + local resolved = shell.resolveProgram(program) + if not resolved then return end + local tCompletion = shell.getCompletionInfo()[resolved] + if not tCompletion then return end + return tCompletion.fnComplete(shell, #previous - starting + 1, text, { program, table.unpack(previous, starting + 1, #previous) }) + end +end + +--[[- A helper function for building shell completion arguments. + +This accepts a series of single-argument completion functions, and combines +them into a function suitable for use with @{shell.setCompletionFunction}. + +@tparam nil|table|function ... Every argument to @{build} represents an argument +to the program you wish to complete. Each argument can be one of three types: + + - `nil`: This argument will not be completed. + + - A function: This argument will be completed with the given function. It is + called with the @{shell} object, the string to complete and the arguments + before this one. + + - A table: This acts as a more powerful version of the function case. The table + must have a function as the first item - this will be called with the shell, + string and preceding arguments as above, but also followed by any additional + items in the table. This provides a more convenient interface to pass + options to your completion functions. + + If this table is the last argument, it may also set the `many` key to true, + which states this function should be used to complete any remaining arguments. +]] +local function build(...) + local arguments = table.pack(...) + for i = 1, arguments.n do + local arg = arguments[i] + if arg ~= nil then + expect(i, arg, "table", "function") + if type(arg) == "function" then + arg = { arg } + arguments[i] = arg + end + + if type(arg[1]) ~= "function" then + error(("Bad table entry #1 at argument #%d (function expected, got %s)"):format(i, type(arg[1])), 2) + end + + if arg.many and i < arguments.n then + error(("Unexpected 'many' field on argument #%d (should only occur on the last argument)"):format(i), 2) + end + end + end + + return function(shell, index, text, previous) + local arg = arguments[index] + if not arg then + if index <= arguments.n then return end + + arg = arguments[arguments.n] + if not arg or not arg.many then return end + end + + return arg[1](shell, text, previous, table.unpack(arg, 2)) + end +end + +return { + file = file, + dir = dir, + dirOrFile = dirOrFile, + program = program, + programWithArgs = programWithArgs, + + -- Re-export various other functions + help = wrap(help.completeTopic), --- Wraps @{help.completeTopic} as a @{build} compatible function. + choice = wrap(completion.choice), --- Wraps @{cc.completion.choice} as a @{build} compatible function. + peripheral = wrap(completion.peripheral), --- Wraps @{cc.completion.peripheral} as a @{build} compatible function. + side = wrap(completion.side), --- Wraps @{cc.completion.side} as a @{build} compatible function. + setting = wrap(completion.setting), --- Wraps @{cc.completion.setting} as a @{build} compatible function. + command = wrap(completion.command), --- Wraps @{cc.completion.command} as a @{build} compatible function. + + build = build, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/strings.lua b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/strings.lua new file mode 100644 index 000000000..11bf4cad4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/main/cc/strings.lua @@ -0,0 +1,115 @@ +-- SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--- Various utilities for working with strings and text. +-- +-- @module cc.strings +-- @since 1.95.0 +-- @see textutils For additional string related utilities. + +local expect = (require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")).expect + +--[[- Wraps a block of text, so that each line fits within the given width. + +This may be useful if you want to wrap text before displaying it to a +@{monitor} or @{printer} without using @{_G.print|print}. + +@tparam string text The string to wrap. +@tparam[opt] number width The width to constrain to, defaults to the width of +the terminal. +@treturn { string... } The wrapped input string as a list of lines. +@usage Wrap a string and write it to the terminal. + + term.clear() + local lines = require "cc.strings".wrap("This is a long piece of text", 10) + for i = 1, #lines do + term.setCursorPos(1, i) + term.write(lines[i]) + end +]] +local function wrap(text, width) + expect(1, text, "string") + expect(2, width, "number", "nil") + width = width or term.getSize() + + + local lines, lines_n, current_line = {}, 0, "" + local function push_line() + lines_n = lines_n + 1 + lines[lines_n] = current_line + current_line = "" + end + + local pos, length = 1, #text + local sub, match = string.sub, string.match + while pos <= length do + local head = sub(text, pos, pos) + if head == " " or head == "\t" then + local whitespace = match(text, "^[ \t]+", pos) + current_line = current_line .. whitespace + pos = pos + #whitespace + elseif head == "\n" then + push_line() + pos = pos + 1 + else + local word = match(text, "^[^ \t\n]+", pos) + pos = pos + #word + if #word > width then + -- Print a multiline word + while #word > 0 do + local space_remaining = width - #current_line - 1 + if space_remaining <= 0 then + push_line() + space_remaining = width + end + + current_line = current_line .. sub(word, 1, space_remaining) + word = sub(word, space_remaining + 1) + end + else + -- Print a word normally + if width - #current_line < #word then push_line() end + current_line = current_line .. word + end + end + end + + push_line() + + -- Trim whitespace longer than width. + for k, line in pairs(lines) do + line = line:sub(1, width) + lines[k] = line + end + + return lines +end + +--- Makes the input string a fixed width. This either truncates it, or pads it +-- with spaces. +-- +-- @tparam string line The string to normalise. +-- @tparam[opt] number width The width to constrain to, defaults to the width of +-- the terminal. +-- +-- @treturn string The string with a specific width. +-- @usage require "cc.strings".ensure_width("a short string", 20) +-- @usage require "cc.strings".ensure_width("a rather long string which is truncated", 20) +local function ensure_width(line, width) + expect(1, line, "string") + expect(2, width, "number", "nil") + width = width or term.getSize() + + line = line:sub(1, width) + if #line < width then + line = line .. (" "):rep(width - #line) + end + + return line +end + +return { + wrap = wrap, + ensure_width = ensure_width, +} diff --git a/src/main/resources/assets/cctweaked/lua/rom/modules/turtle/.ignoreme b/src/main/resources/assets/cctweaked/lua/rom/modules/turtle/.ignoreme new file mode 100644 index 000000000..3fcc8a952 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/modules/turtle/.ignoreme @@ -0,0 +1,4 @@ +--[[ +Alright then, don't ignore me. This file is to ensure the existence of the "modules/turtle" folder. +You can use this folder to add modules who can be loaded with require() to your Resourcepack. +]] diff --git a/src/main/resources/assets/cctweaked/lua/rom/motd.txt b/src/main/resources/assets/cctweaked/lua/rom/motd.txt new file mode 100644 index 000000000..972589050 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/motd.txt @@ -0,0 +1,24 @@ +Please report bugs at https://github.com/cc-tweaked/CC-Tweaked. Thanks! +View the documentation at https://tweaked.cc +Show off your programs or ask for help at our forum: https://forums.computercraft.cc +You can disable these messages by running "set motd.enable false". +Use "pastebin put" to upload a program to pastebin. +Use the "edit" program to create and edit your programs. +You can use "wget" to download a file from the internet. +On an advanced computer you can use "fg" or "bg" to run multiple programs at the same time. +Use an advanced computer to use colours and the mouse. +With a speaker you can play sounds. +Programs that are placed in the "startup" folder in the root of a computer are started on boot. +Use a modem to connect with other computers. +With the "gps" program you can get the position of a computer. +Use "monitor" to run a program on a attached monitor. +Don't forget to label your computer with "label set". +Feeling creative? Use a printer to print a book! +Files beginning with a "." are hidden from "list" by default. +Running "set" lists the current values of all settings. +Some programs are only available on advanced computers, turtles, pocket computers or command computers. +The "equip" programs let you add upgrades to a turtle without crafting. +You can change the color of a disk by crafting or right clicking it with dye. +You can print on a printed page again to get multiple colors. +Holding the Ctrl and T keys terminates the running program. +You can drag and drop files onto an open computer to upload them. diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/about.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/about.lua new file mode 100644 index 000000000..c854ed73b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/about.lua @@ -0,0 +1,7 @@ +-- SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +term.setTextColor(colors.yellow) +print(os.version() .. " on " .. _HOST) +term.setTextColor(colors.white) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/bg.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/bg.lua new file mode 100644 index 000000000..fe0cd26e8 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/bg.lua @@ -0,0 +1,15 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not shell.openTab then + printError("Requires multishell") + return +end + +local tArgs = { ... } +if #tArgs > 0 then + shell.openTab(table.unpack(tArgs)) +else + shell.openTab("shell") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/fg.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/fg.lua new file mode 100644 index 000000000..a3778ac8f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/fg.lua @@ -0,0 +1,21 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not shell.openTab then + printError("Requires multishell") + return +end + +local tArgs = { ... } +if #tArgs > 0 then + local nTask = shell.openTab(table.unpack(tArgs)) + if nTask then + shell.switchTab(nTask) + end +else + local nTask = shell.openTab("shell") + if nTask then + shell.switchTab(nTask) + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/multishell.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/multishell.lua new file mode 100644 index 000000000..8ab5b7bcf --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/advanced/multishell.lua @@ -0,0 +1,433 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--- Multishell allows multiple programs to be run at the same time. +-- +-- When multiple programs are running, it displays a tab bar at the top of the +-- screen, which allows you to switch between programs. New programs can be +-- launched using the `fg` or `bg` programs, or using the @{shell.openTab} and +-- @{multishell.launch} functions. +-- +-- Each process is identified by its ID, which corresponds to its position in +-- the tab list. As tabs may be opened and closed, this ID is _not_ constant +-- over a program's run. As such, be careful not to use stale IDs. +-- +-- As with @{shell}, @{multishell} is not a "true" API. Instead, it is a +-- standard program, which launches a shell and injects its API into the shell's +-- environment. This API is not available in the global environment, and so is +-- not available to @{os.loadAPI|APIs}. +-- +-- @module[module] multishell +-- @since 1.6 + +local expect = dofile("rom/modules/main/cc/expect.lua").expect + +-- Setup process switching +local parentTerm = term.current() +local w, h = parentTerm.getSize() + +local tProcesses = {} +local nCurrentProcess = nil +local nRunningProcess = nil +local bShowMenu = false +local bWindowsResized = false +local nScrollPos = 1 +local bScrollRight = false + +local function selectProcess(n) + if nCurrentProcess ~= n then + if nCurrentProcess then + local tOldProcess = tProcesses[nCurrentProcess] + tOldProcess.window.setVisible(false) + end + nCurrentProcess = n + if nCurrentProcess then + local tNewProcess = tProcesses[nCurrentProcess] + tNewProcess.window.setVisible(true) + tNewProcess.bInteracted = true + end + end +end + +local function setProcessTitle(n, sTitle) + tProcesses[n].sTitle = sTitle +end + +local function resumeProcess(nProcess, sEvent, ...) + local tProcess = tProcesses[nProcess] + local sFilter = tProcess.sFilter + if sFilter == nil or sFilter == sEvent or sEvent == "terminate" then + local nPreviousProcess = nRunningProcess + nRunningProcess = nProcess + term.redirect(tProcess.terminal) + local ok, result = coroutine.resume(tProcess.co, sEvent, ...) + tProcess.terminal = term.current() + if ok then + tProcess.sFilter = result + else + printError(result) + end + nRunningProcess = nPreviousProcess + end +end + +local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...) + local tProgramArgs = table.pack(...) + local nProcess = #tProcesses + 1 + local tProcess = {} + tProcess.sTitle = fs.getName(sProgramPath) + if bShowMenu then + tProcess.window = window.create(parentTerm, 1, 2, w, h - 1, false) + else + tProcess.window = window.create(parentTerm, 1, 1, w, h, false) + end + + -- Restrict the public view of the window to normal redirect functions. + tProcess.terminal = {} + for k in pairs(term.native()) do tProcess.terminal[k] = tProcess.window[k] end + + tProcess.co = coroutine.create(function() + os.run(tProgramEnv, sProgramPath, table.unpack(tProgramArgs, 1, tProgramArgs.n)) + if not tProcess.bInteracted then + term.setCursorBlink(false) + print("Press any key to continue") + os.pullEvent("char") + end + end) + tProcess.sFilter = nil + tProcess.bInteracted = false + tProcesses[nProcess] = tProcess + if bFocus then + selectProcess(nProcess) + end + resumeProcess(nProcess) + return nProcess +end + +local function cullProcess(nProcess) + local tProcess = tProcesses[nProcess] + if coroutine.status(tProcess.co) == "dead" then + if nCurrentProcess == nProcess then + selectProcess(nil) + end + table.remove(tProcesses, nProcess) + if nCurrentProcess == nil then + if nProcess > 1 then + selectProcess(nProcess - 1) + elseif #tProcesses > 0 then + selectProcess(1) + end + end + if nScrollPos ~= 1 then + nScrollPos = nScrollPos - 1 + end + return true + end + return false +end + +local function cullProcesses() + local culled = false + for n = #tProcesses, 1, -1 do + culled = culled or cullProcess(n) + end + return culled +end + +-- Setup the main menu +local menuMainTextColor, menuMainBgColor, menuOtherTextColor, menuOtherBgColor +if parentTerm.isColor() then + menuMainTextColor, menuMainBgColor = colors.yellow, colors.black + menuOtherTextColor, menuOtherBgColor = colors.black, colors.gray +else + menuMainTextColor, menuMainBgColor = colors.white, colors.black + menuOtherTextColor, menuOtherBgColor = colors.black, colors.gray +end + +local function redrawMenu() + if bShowMenu then + -- Draw menu + parentTerm.setCursorPos(1, 1) + parentTerm.setBackgroundColor(menuOtherBgColor) + parentTerm.clearLine() + local nCharCount = 0 + local nSize = parentTerm.getSize() + if nScrollPos ~= 1 then + parentTerm.setTextColor(menuOtherTextColor) + parentTerm.setBackgroundColor(menuOtherBgColor) + parentTerm.write("<") + nCharCount = 1 + end + for n = nScrollPos, #tProcesses do + if n == nCurrentProcess then + parentTerm.setTextColor(menuMainTextColor) + parentTerm.setBackgroundColor(menuMainBgColor) + else + parentTerm.setTextColor(menuOtherTextColor) + parentTerm.setBackgroundColor(menuOtherBgColor) + end + parentTerm.write(" " .. tProcesses[n].sTitle .. " ") + nCharCount = nCharCount + #tProcesses[n].sTitle + 2 + end + if nCharCount > nSize then + parentTerm.setTextColor(menuOtherTextColor) + parentTerm.setBackgroundColor(menuOtherBgColor) + parentTerm.setCursorPos(nSize, 1) + parentTerm.write(">") + bScrollRight = true + else + bScrollRight = false + end + + -- Put the cursor back where it should be + local tProcess = tProcesses[nCurrentProcess] + if tProcess then + tProcess.window.restoreCursor() + end + end +end + +local function resizeWindows() + local windowY, windowHeight + if bShowMenu then + windowY = 2 + windowHeight = h - 1 + else + windowY = 1 + windowHeight = h + end + for n = 1, #tProcesses do + local tProcess = tProcesses[n] + local x, y = tProcess.window.getCursorPos() + if y > windowHeight then + tProcess.window.scroll(y - windowHeight) + tProcess.window.setCursorPos(x, windowHeight) + end + tProcess.window.reposition(1, windowY, w, windowHeight) + end + bWindowsResized = true +end + +local function setMenuVisible(bVis) + if bShowMenu ~= bVis then + bShowMenu = bVis + resizeWindows() + redrawMenu() + end +end + +local multishell = {} --- @export + +--- Get the currently visible process. This will be the one selected on +-- the tab bar. +-- +-- Note, this is different to @{getCurrent}, which returns the process which is +-- currently executing. +-- +-- @treturn number The currently visible process's index. +-- @see setFocus +function multishell.getFocus() + return nCurrentProcess +end + +--- Change the currently visible process. +-- +-- @tparam number n The process index to switch to. +-- @treturn boolean If the process was changed successfully. This will +-- return @{false} if there is no process with this id. +-- @see getFocus +function multishell.setFocus(n) + expect(1, n, "number") + if n >= 1 and n <= #tProcesses then + selectProcess(n) + redrawMenu() + return true + end + return false +end + +--- Get the title of the given tab. +-- +-- This starts as the name of the program, but may be changed using +-- @{multishell.setTitle}. +-- @tparam number n The process index. +-- @treturn string|nil The current process title, or @{nil} if the +-- process doesn't exist. +function multishell.getTitle(n) + expect(1, n, "number") + if n >= 1 and n <= #tProcesses then + return tProcesses[n].sTitle + end + return nil +end + +--- Set the title of the given process. +-- +-- @tparam number n The process index. +-- @tparam string title The new process title. +-- @see getTitle +-- @usage Change the title of the current process +-- +-- multishell.setTitle(multishell.getCurrent(), "Hello") +function multishell.setTitle(n, title) + expect(1, n, "number") + expect(2, title, "string") + if n >= 1 and n <= #tProcesses then + setProcessTitle(n, title) + redrawMenu() + end +end + +--- Get the index of the currently running process. +-- +-- @treturn number The currently running process. +function multishell.getCurrent() + return nRunningProcess +end + +--- Start a new process, with the given environment, program and arguments. +-- +-- The returned process index is not constant over the program's run. It can be +-- safely used immediately after launching (for instance, to update the title or +-- switch to that tab). However, after your program has yielded, it may no +-- longer be correct. +-- +-- @tparam table tProgramEnv The environment to load the path under. +-- @tparam string sProgramPath The path to the program to run. +-- @param ... Additional arguments to pass to the program. +-- @treturn number The index of the created process. +-- @see os.run +-- @usage Run the "hello" program, and set its title to "Hello!" +-- +-- local id = multishell.launch({}, "/rom/programs/fun/hello.lua") +-- multishell.setTitle(id, "Hello!") +function multishell.launch(tProgramEnv, sProgramPath, ...) + expect(1, tProgramEnv, "table") + expect(2, sProgramPath, "string") + local previousTerm = term.current() + setMenuVisible(#tProcesses + 1 >= 2) + local nResult = launchProcess(false, tProgramEnv, sProgramPath, ...) + redrawMenu() + term.redirect(previousTerm) + return nResult +end + +--- Get the number of processes within this multishell. +-- +-- @treturn number The number of processes. +function multishell.getCount() + return #tProcesses +end + +-- Begin +parentTerm.clear() +setMenuVisible(false) +launchProcess(true, { + ["shell"] = shell, + ["multishell"] = multishell, +}, "/rom/programs/shell.lua") + +-- Run processes +while #tProcesses > 0 do + -- Get the event + local tEventData = table.pack(os.pullEventRaw()) + local sEvent = tEventData[1] + if sEvent == "term_resize" then + -- Resize event + w, h = parentTerm.getSize() + resizeWindows() + redrawMenu() + + elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then + -- Basic input, just passthrough to current process + resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n)) + if cullProcess(nCurrentProcess) then + setMenuVisible(#tProcesses >= 2) + redrawMenu() + end + + elseif sEvent == "mouse_click" then + -- Click event + local button, x, y = tEventData[2], tEventData[3], tEventData[4] + if bShowMenu and y == 1 then + -- Switch process + if x == 1 and nScrollPos ~= 1 then + nScrollPos = nScrollPos - 1 + redrawMenu() + elseif bScrollRight and x == term.getSize() then + nScrollPos = nScrollPos + 1 + redrawMenu() + else + local tabStart = 1 + if nScrollPos ~= 1 then + tabStart = 2 + end + for n = nScrollPos, #tProcesses do + local tabEnd = tabStart + #tProcesses[n].sTitle + 1 + if x >= tabStart and x <= tabEnd then + selectProcess(n) + redrawMenu() + break + end + tabStart = tabEnd + 1 + end + end + else + -- Passthrough to current process + resumeProcess(nCurrentProcess, sEvent, button, x, bShowMenu and y - 1 or y) + if cullProcess(nCurrentProcess) then + setMenuVisible(#tProcesses >= 2) + redrawMenu() + end + end + + elseif sEvent == "mouse_drag" or sEvent == "mouse_up" or sEvent == "mouse_scroll" then + -- Other mouse event + local p1, x, y = tEventData[2], tEventData[3], tEventData[4] + if bShowMenu and sEvent == "mouse_scroll" and y == 1 then + if p1 == -1 and nScrollPos ~= 1 then + nScrollPos = nScrollPos - 1 + redrawMenu() + elseif bScrollRight and p1 == 1 then + nScrollPos = nScrollPos + 1 + redrawMenu() + end + elseif not (bShowMenu and y == 1) then + -- Passthrough to current process + resumeProcess(nCurrentProcess, sEvent, p1, x, bShowMenu and y - 1 or y) + if cullProcess(nCurrentProcess) then + setMenuVisible(#tProcesses >= 2) + redrawMenu() + end + end + + else + -- Other event + -- Passthrough to all processes + local nLimit = #tProcesses -- Storing this ensures any new things spawned don't get the event + for n = 1, nLimit do + resumeProcess(n, table.unpack(tEventData, 1, tEventData.n)) + end + if cullProcesses() then + setMenuVisible(#tProcesses >= 2) + redrawMenu() + end + end + + if bWindowsResized then + -- Pass term_resize to all processes + local nLimit = #tProcesses -- Storing this ensures any new things spawned don't get the event + for n = 1, nLimit do + resumeProcess(n, "term_resize") + end + bWindowsResized = false + if cullProcesses() then + setMenuVisible(#tProcesses >= 2) + redrawMenu() + end + end +end + +-- Shutdown +term.redirect(parentTerm) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/alias.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/alias.lua new file mode 100644 index 000000000..de2191fc9 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/alias.lua @@ -0,0 +1,30 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs > 2 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sAlias = tArgs[1] +local sProgram = tArgs[2] + +if sAlias and sProgram then + -- Set alias + shell.setAlias(sAlias, sProgram) +elseif sAlias then + -- Clear alias + shell.clearAlias(sAlias) +else + -- List aliases + local tAliases = shell.aliases() + local tList = {} + for sAlias, sCommand in pairs(tAliases) do + table.insert(tList, sAlias .. ":" .. sCommand) + end + table.sort(tList) + textutils.pagedTabulate(tList) +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/apis.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/apis.lua new file mode 100644 index 000000000..b4bcfcbbc --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/apis.lua @@ -0,0 +1,18 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tApis = {} +for k, v in pairs(_G) do + if type(k) == "string" and type(v) == "table" and k ~= "_G" then + table.insert(tApis, k) + end +end +table.insert(tApis, "shell") +table.insert(tApis, "package") +if multishell then + table.insert(tApis, "multishell") +end +table.sort(tApis) + +textutils.pagedTabulate(tApis) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/cd.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/cd.lua new file mode 100644 index 000000000..ef17d26eb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/cd.lua @@ -0,0 +1,18 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs < 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sNewDir = shell.resolve(tArgs[1]) +if fs.isDir(sNewDir) then + shell.setDir(sNewDir) +else + print("Not a directory") + return +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/clear.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/clear.lua new file mode 100644 index 000000000..5e8eb93ff --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/clear.lua @@ -0,0 +1,37 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName) + print(programName .. " screen") + print(programName .. " palette") + print(programName .. " all") +end + +local function clear() + term.clear() + term.setCursorPos(1, 1) +end + +local function resetPalette() + for i = 0, 15 do + term.setPaletteColour(math.pow(2, i), term.nativePaletteColour(math.pow(2, i))) + end +end + +local sCommand = tArgs[1] or "screen" +if sCommand == "screen" then + clear() +elseif sCommand == "palette" then + resetPalette() +elseif sCommand == "all" then + clear() + resetPalette() +else + printUsage() +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/command/commands.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/command/commands.lua new file mode 100644 index 000000000..6f42593c0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/command/commands.lua @@ -0,0 +1,19 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not commands then + printError("Requires a Command Computer.") + return +end + +local tCommands = commands.list() +table.sort(tCommands) + +if term.isColor() then + term.setTextColor(colors.green) +end +print("Available commands:") +term.setTextColor(colors.white) + +textutils.pagedTabulate(tCommands) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/command/exec.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/command/exec.lua new file mode 100644 index 000000000..79fb45e27 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/command/exec.lua @@ -0,0 +1,44 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if not commands then + printError("Requires a Command Computer.") + return +end +if #tArgs == 0 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + printError("Usage: " .. programName .. " ") + return +end + +local function printSuccess(text) + if term.isColor() then + term.setTextColor(colors.green) + end + print(text) + term.setTextColor(colors.white) +end + +local sCommand = string.lower(tArgs[1]) +for n = 2, #tArgs do + sCommand = sCommand .. " " .. tArgs[n] +end + +local bResult, tOutput = commands.exec(sCommand) +if bResult then + printSuccess("Success") + if #tOutput > 0 then + for n = 1, #tOutput do + print(tOutput[n]) + end + end +else + printError("Failed") + if #tOutput > 0 then + for n = 1, #tOutput do + print(tOutput[n]) + end + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/copy.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/copy.lua new file mode 100644 index 000000000..c4ae8f5df --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/copy.lua @@ -0,0 +1,36 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs < 2 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sSource = shell.resolve(tArgs[1]) +local sDest = shell.resolve(tArgs[2]) +local tFiles = fs.find(sSource) +if #tFiles > 0 then + for _, sFile in ipairs(tFiles) do + if fs.isDir(sDest) then + fs.copy(sFile, fs.combine(sDest, fs.getName(sFile))) + elseif #tFiles == 1 then + if fs.exists(sDest) then + printError("Destination exists") + elseif fs.isReadOnly(sDest) then + printError("Destination is read-only") + elseif fs.getFreeSpace(sDest) < fs.getSize(sFile) then + printError("Not enough space") + else + fs.copy(sFile, sDest) + end + else + printError("Cannot overwrite file multiple times") + return + end + end +else + printError("No matching files") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/delete.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/delete.lua new file mode 100644 index 000000000..727c12bcb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/delete.lua @@ -0,0 +1,34 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local args = table.pack(...) + +if args.n < 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +for i = 1, args.n do + local files = fs.find(shell.resolve(args[i])) + if #files > 0 then + for _, file in ipairs(files) do + if fs.isReadOnly(file) then + printError("Cannot delete read-only file /" .. file) + elseif fs.isDriveRoot(file) then + printError("Cannot delete mount /" .. file) + if fs.isDir(file) then + print("To delete its contents run rm /" .. fs.combine(file, "*")) + end + else + local ok, err = pcall(fs.delete, file) + if not ok then + printError((err:gsub("^pcall: ", ""))) + end + end + end + else + printError(args[i] .. ": No matching files") + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/drive.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/drive.lua new file mode 100644 index 000000000..2c41935b5 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/drive.lua @@ -0,0 +1,25 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +-- Get where a directory is mounted +local sPath = shell.dir() +if tArgs[1] ~= nil then + sPath = shell.resolve(tArgs[1]) +end + +if fs.exists(sPath) then + write(fs.getDrive(sPath) .. " (") + local nSpace = fs.getFreeSpace(sPath) + if nSpace >= 1000 * 1000 then + print(math.floor(nSpace / (100 * 1000)) / 10 .. "MB remaining)") + elseif nSpace >= 1000 then + print(math.floor(nSpace / 100) / 10 .. "KB remaining)") + else + print(nSpace .. "B remaining)") + end +else + print("No such path") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/edit.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/edit.lua new file mode 100644 index 000000000..05c35d956 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/edit.lua @@ -0,0 +1,877 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +-- Get file to edit +local tArgs = { ... } +if #tArgs == 0 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +-- Error checking +local sPath = shell.resolve(tArgs[1]) +local bReadOnly = fs.isReadOnly(sPath) +if fs.exists(sPath) and fs.isDir(sPath) then + print("Cannot edit a directory.") + return +end + +-- Create .lua files by default +if not fs.exists(sPath) and not string.find(sPath, "%.") then + local sExtension = settings.get("edit.default_extension") + if sExtension ~= "" and type(sExtension) == "string" then + sPath = sPath .. "." .. sExtension + end +end + +local x, y = 1, 1 +local w, h = term.getSize() +local scrollX, scrollY = 0, 0 + +local tLines = {} +local bRunning = true + +-- Colours +local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour, errorColour +if term.isColour() then + bgColour = colours.black + textColour = colours.white + highlightColour = colours.yellow + keywordColour = colours.yellow + commentColour = colours.green + stringColour = colours.red + errorColour = colours.red +else + bgColour = colours.black + textColour = colours.white + highlightColour = colours.white + keywordColour = colours.white + commentColour = colours.white + stringColour = colours.white + errorColour = colours.white +end + +local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q) +local current = term.current() +local contents, name = %q, %q +local fn, err = load(contents, name, nil, _ENV) +if fn then + local exception = require "cc.internal.exception" + local ok, err, co = exception.try(fn, ...) + + term.redirect(current) + term.setTextColor(term.isColour() and colours.yellow or colours.white) + term.setBackgroundColor(colours.black) + term.setCursorBlink(false) + + if not ok then + printError(err) + exception.report(err, co, { [name] = contents }) + end +else + local parser = require "cc.internal.syntax" + if parser.parse_program(contents) then printError(err) end +end + +local message = "Press any key to continue." +if ok then message = "Program finished. " .. message end +local _, y = term.getCursorPos() +local w, h = term.getSize() +local wrapped = require("cc.strings").wrap(message, w) + +local start_y = h - #wrapped + 1 +if y >= start_y then term.scroll(y - start_y + 1) end +for i = 1, #wrapped do + term.setCursorPos(1, start_y + i - 1) + term.write(wrapped[i]) +end +os.pullEvent('key') +]] + +-- Menus +local bMenu = false +local nMenuItem = 1 +local tMenuItems = {} +if not bReadOnly then + table.insert(tMenuItems, "Save") +end +if shell.openTab then + table.insert(tMenuItems, "Run") +end +if peripheral.find("printer") then + table.insert(tMenuItems, "Print") +end +table.insert(tMenuItems, "Exit") + +local status_ok, status_text +local function set_status(text, ok) + status_ok = ok ~= false + status_text = text +end + +if bReadOnly then + set_status("File is read only", false) +elseif fs.getFreeSpace(sPath) < 1024 then + set_status("Disk is low on space", false) +else + local message + if term.isColour() then + message = "Press Ctrl or click here to access menu" + else + message = "Press Ctrl to access menu" + end + + if #message > w - 5 then + message = "Press Ctrl for menu" + end + + set_status(message) +end + +local function load(_sPath) + tLines = {} + if fs.exists(_sPath) then + local file = io.open(_sPath, "r") + local sLine = file:read() + while sLine do + table.insert(tLines, sLine) + sLine = file:read() + end + file:close() + end + + if #tLines == 0 then + table.insert(tLines, "") + end +end + +local function save(_sPath, fWrite) + -- Create intervening folder + local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len()) + if not fs.exists(sDir) then + fs.makeDir(sDir) + end + + -- Save + local file, fileerr + local function innerSave() + file, fileerr = fs.open(_sPath, "w") + if file then + if file then + fWrite(file) + end + else + error("Failed to open " .. _sPath) + end + end + + local ok, err = pcall(innerSave) + if file then + file.close() + end + return ok, err, fileerr +end + +local tKeywords = { + ["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, +} + +local function tryWrite(sLine, regex, colour) + local match = string.match(sLine, regex) + if match then + if type(colour) == "number" then + term.setTextColour(colour) + else + term.setTextColour(colour(match)) + end + term.write(match) + term.setTextColour(textColour) + return string.sub(sLine, #match + 1) + end + return nil +end + +local function writeHighlighted(sLine) + while #sLine > 0 do + sLine = + tryWrite(sLine, "^%-%-%[%[.-%]%]", commentColour) or + tryWrite(sLine, "^%-%-.*", commentColour) or + tryWrite(sLine, "^\"\"", stringColour) or + tryWrite(sLine, "^\".-[^\\]\"", stringColour) or + tryWrite(sLine, "^\'\'", stringColour) or + tryWrite(sLine, "^\'.-[^\\]\'", stringColour) or + tryWrite(sLine, "^%[%[.-%]%]", stringColour) or + tryWrite(sLine, "^[%w_]+", function(match) + if tKeywords[match] then + return keywordColour + end + return textColour + end) or + tryWrite(sLine, "^[^%w_]", textColour) + end +end + +local tCompletions +local nCompletion + +local tCompleteEnv = _ENV +local function complete(sLine) + if settings.get("edit.autocomplete") then + local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$") + if nStartPos then + sLine = string.sub(sLine, nStartPos) + end + if #sLine > 0 then + return textutils.complete(sLine, tCompleteEnv) + end + end + return nil +end + +local function recomplete() + local sLine = tLines[y] + if not bMenu and not bReadOnly and x == #sLine + 1 then + tCompletions = complete(sLine) + if tCompletions and #tCompletions > 0 then + nCompletion = 1 + else + nCompletion = nil + end + else + tCompletions = nil + nCompletion = nil + end +end + +local function writeCompletion(sLine) + if nCompletion then + local sCompletion = tCompletions[nCompletion] + term.setTextColor(colours.white) + term.setBackgroundColor(colours.grey) + term.write(sCompletion) + term.setTextColor(textColour) + term.setBackgroundColor(bgColour) + end +end + +local function redrawText() + local cursorX, cursorY = x, y + for y = 1, h - 1 do + term.setCursorPos(1 - scrollX, y) + term.clearLine() + + local sLine = tLines[y + scrollY] + if sLine ~= nil then + writeHighlighted(sLine) + if cursorY == y and cursorX == #sLine + 1 then + writeCompletion() + end + end + end + term.setCursorPos(x - scrollX, y - scrollY) +end + +local function redrawLine(_nY) + local sLine = tLines[_nY] + if sLine then + term.setCursorPos(1 - scrollX, _nY - scrollY) + term.clearLine() + writeHighlighted(sLine) + if _nY == y and x == #sLine + 1 then + writeCompletion() + end + term.setCursorPos(x - scrollX, _nY - scrollY) + end +end + +local function redrawMenu() + -- Clear line + term.setCursorPos(1, h) + term.clearLine() + + -- Draw line numbers + term.setCursorPos(w - #("Ln " .. y) + 1, h) + term.setTextColour(highlightColour) + term.write("Ln ") + term.setTextColour(textColour) + term.write(y) + + term.setCursorPos(1, h) + if bMenu then + -- Draw menu + term.setTextColour(textColour) + for nItem, sItem in pairs(tMenuItems) do + if nItem == nMenuItem then + term.setTextColour(highlightColour) + term.write("[") + term.setTextColour(textColour) + term.write(sItem) + term.setTextColour(highlightColour) + term.write("]") + term.setTextColour(textColour) + else + term.write(" " .. sItem .. " ") + end + end + else + -- Draw status + term.setTextColour(status_ok and highlightColour or errorColour) + term.write(status_text) + term.setTextColour(textColour) + end + + -- Reset cursor + term.setCursorPos(x - scrollX, y - scrollY) +end + +local tMenuFuncs = { + Save = function() + if bReadOnly then + set_status("Access denied", false) + else + local ok, _, fileerr = save(sPath, function(file) + for _, sLine in ipairs(tLines) do + file.write(sLine .. "\n") + end + end) + if ok then + set_status("Saved to " .. sPath) + else + if fileerr then + set_status("Error saving: " .. fileerr, false) + else + set_status("Error saving to " .. sPath, false) + end + end + end + redrawMenu() + end, + Print = function() + local printer = peripheral.find("printer") + if not printer then + set_status("No printer attached", false) + return + end + + local nPage = 0 + local sName = fs.getName(sPath) + if printer.getInkLevel() < 1 then + set_status("Printer out of ink", false) + return + elseif printer.getPaperLevel() < 1 then + set_status("Printer out of paper", false) + return + end + + local screenTerminal = term.current() + local printerTerminal = { + getCursorPos = printer.getCursorPos, + setCursorPos = printer.setCursorPos, + getSize = printer.getPageSize, + write = printer.write, + } + printerTerminal.scroll = function() + if nPage == 1 then + printer.setPageTitle(sName .. " (page " .. nPage .. ")") + end + + while not printer.newPage() do + if printer.getInkLevel() < 1 then + set_status("Printer out of ink, please refill", false) + elseif printer.getPaperLevel() < 1 then + set_status("Printer out of paper, please refill", false) + else + set_status("Printer output tray full, please empty", false) + end + + term.redirect(screenTerminal) + redrawMenu() + term.redirect(printerTerminal) + + sleep(0.5) + end + + nPage = nPage + 1 + if nPage == 1 then + printer.setPageTitle(sName) + else + printer.setPageTitle(sName .. " (page " .. nPage .. ")") + end + end + + bMenu = false + term.redirect(printerTerminal) + local ok, error = pcall(function() + term.scroll() + for _, sLine in ipairs(tLines) do + print(sLine) + end + end) + term.redirect(screenTerminal) + if not ok then + print(error) + end + + while not printer.endPage() do + set_status("Printer output tray full, please empty") + redrawMenu() + sleep(0.5) + end + bMenu = true + + if nPage > 1 then + set_status("Printed " .. nPage .. " Pages") + else + set_status("Printed 1 Page") + end + redrawMenu() + end, + Exit = function() + bRunning = false + end, + Run = function() + local sTitle = fs.getName(sPath) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1, -5) + end + local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle) + if fs.exists(sTempPath) then + set_status("Error saving to " .. sTempPath, false) + return + end + local ok = save(sTempPath, function(file) + file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@/" .. sPath)) + end) + if ok then + local nTask = shell.openTab("/" .. sTempPath) + if nTask then + shell.switchTab(nTask) + else + set_status("Error starting Task", false) + end + fs.delete(sTempPath) + else + set_status("Error saving to " .. sTempPath, false) + end + redrawMenu() + end, +} + +local function doMenuItem(_n) + tMenuFuncs[tMenuItems[_n]]() + if bMenu then + bMenu = false + term.setCursorBlink(true) + end + redrawMenu() +end + +local function setCursor(newX, newY) + local _, oldY = x, y + x, y = newX, newY + local screenX = x - scrollX + local screenY = y - scrollY + + local bRedraw = false + if screenX < 1 then + scrollX = x - 1 + screenX = 1 + bRedraw = true + elseif screenX > w then + scrollX = x - w + screenX = w + bRedraw = true + end + + if screenY < 1 then + scrollY = y - 1 + screenY = 1 + bRedraw = true + elseif screenY > h - 1 then + scrollY = y - (h - 1) + screenY = h - 1 + bRedraw = true + end + + recomplete() + if bRedraw then + redrawText() + elseif y ~= oldY then + redrawLine(oldY) + redrawLine(y) + else + redrawLine(y) + end + term.setCursorPos(screenX, screenY) + + redrawMenu() +end + +-- Actual program functionality begins +load(sPath) + +term.setBackgroundColour(bgColour) +term.clear() +term.setCursorPos(x, y) +term.setCursorBlink(true) + +recomplete() +redrawText() +redrawMenu() + +local function acceptCompletion() + if nCompletion then + -- Append the completion + local sCompletion = tCompletions[nCompletion] + tLines[y] = tLines[y] .. sCompletion + setCursor(x + #sCompletion , y) + end +end + +-- Handle input +while bRunning do + local sEvent, param, param2, param3 = os.pullEvent() + if sEvent == "key" then + if param == keys.up then + -- Up + if not bMenu then + if nCompletion then + -- Cycle completions + nCompletion = nCompletion - 1 + if nCompletion < 1 then + nCompletion = #tCompletions + end + redrawLine(y) + + elseif y > 1 then + -- Move cursor up + setCursor( + math.min(x, #tLines[y - 1] + 1), + y - 1 + ) + end + end + + elseif param == keys.down then + -- Down + if not bMenu then + -- Move cursor down + if nCompletion then + -- Cycle completions + nCompletion = nCompletion + 1 + if nCompletion > #tCompletions then + nCompletion = 1 + end + redrawLine(y) + + elseif y < #tLines then + -- Move cursor down + setCursor( + math.min(x, #tLines[y + 1] + 1), + y + 1 + ) + end + end + + elseif param == keys.tab then + -- Tab + if not bMenu and not bReadOnly then + if nCompletion and x == #tLines[y] + 1 then + -- Accept autocomplete + acceptCompletion() + else + -- Indent line + local sLine = tLines[y] + tLines[y] = string.sub(sLine, 1, x - 1) .. " " .. string.sub(sLine, x) + setCursor(x + 4, y) + end + end + + elseif param == keys.pageUp then + -- Page Up + if not bMenu then + -- Move up a page + local newY + if y - (h - 1) >= 1 then + newY = y - (h - 1) + else + newY = 1 + end + setCursor( + math.min(x, #tLines[newY] + 1), + newY + ) + end + + elseif param == keys.pageDown then + -- Page Down + if not bMenu then + -- Move down a page + local newY + if y + (h - 1) <= #tLines then + newY = y + (h - 1) + else + newY = #tLines + end + local newX = math.min(x, #tLines[newY] + 1) + setCursor(newX, newY) + end + + elseif param == keys.home then + -- Home + if not bMenu then + -- Move cursor to the beginning + if x > 1 then + setCursor(1, y) + end + end + + elseif param == keys["end"] then + -- End + if not bMenu then + -- Move cursor to the end + local nLimit = #tLines[y] + 1 + if x < nLimit then + setCursor(nLimit, y) + end + end + + elseif param == keys.left then + -- Left + if not bMenu then + if x > 1 then + -- Move cursor left + setCursor(x - 1, y) + elseif x == 1 and y > 1 then + setCursor(#tLines[y - 1] + 1, y - 1) + end + else + -- Move menu left + nMenuItem = nMenuItem - 1 + if nMenuItem < 1 then + nMenuItem = #tMenuItems + end + redrawMenu() + end + + elseif param == keys.right then + -- Right + if not bMenu then + local nLimit = #tLines[y] + 1 + if x < nLimit then + -- Move cursor right + setCursor(x + 1, y) + elseif nCompletion and x == #tLines[y] + 1 then + -- Accept autocomplete + acceptCompletion() + elseif x == nLimit and y < #tLines then + -- Go to next line + setCursor(1, y + 1) + end + else + -- Move menu right + nMenuItem = nMenuItem + 1 + if nMenuItem > #tMenuItems then + nMenuItem = 1 + end + redrawMenu() + end + + elseif param == keys.delete then + -- Delete + if not bMenu and not bReadOnly then + local nLimit = #tLines[y] + 1 + if x < nLimit then + local sLine = tLines[y] + tLines[y] = string.sub(sLine, 1, x - 1) .. string.sub(sLine, x + 1) + recomplete() + redrawLine(y) + elseif y < #tLines then + tLines[y] = tLines[y] .. tLines[y + 1] + table.remove(tLines, y + 1) + recomplete() + redrawText() + end + end + + elseif param == keys.backspace then + -- Backspace + if not bMenu and not bReadOnly then + if x > 1 then + -- Remove character + local sLine = tLines[y] + if x > 4 and string.sub(sLine, x - 4, x - 1) == " " and not string.sub(sLine, 1, x - 1):find("%S") then + tLines[y] = string.sub(sLine, 1, x - 5) .. string.sub(sLine, x) + setCursor(x - 4, y) + else + tLines[y] = string.sub(sLine, 1, x - 2) .. string.sub(sLine, x) + setCursor(x - 1, y) + end + elseif y > 1 then + -- Remove newline + local sPrevLen = #tLines[y - 1] + tLines[y - 1] = tLines[y - 1] .. tLines[y] + table.remove(tLines, y) + setCursor(sPrevLen + 1, y - 1) + redrawText() + end + end + + elseif param == keys.enter or param == keys.numPadEnter then + -- Enter/Numpad Enter + if not bMenu and not bReadOnly then + -- Newline + local sLine = tLines[y] + local _, spaces = string.find(sLine, "^[ ]+") + if not spaces then + spaces = 0 + end + tLines[y] = string.sub(sLine, 1, x - 1) + table.insert(tLines, y + 1, string.rep(' ', spaces) .. string.sub(sLine, x)) + setCursor(spaces + 1, y + 1) + redrawText() + + elseif bMenu then + -- Menu selection + doMenuItem(nMenuItem) + + end + + elseif param == keys.leftCtrl or param == keys.rightCtrl then + -- Menu toggle + bMenu = not bMenu + if bMenu then + term.setCursorBlink(false) + else + term.setCursorBlink(true) + end + redrawMenu() + elseif param == keys.rightAlt then + if bMenu then + bMenu = false + term.setCursorBlink(true) + redrawMenu() + end + end + + elseif sEvent == "char" then + if not bMenu and not bReadOnly then + -- Input text + local sLine = tLines[y] + tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x) + setCursor(x + 1, y) + + elseif bMenu then + -- Select menu items + for n, sMenuItem in ipairs(tMenuItems) do + if string.lower(string.sub(sMenuItem, 1, 1)) == string.lower(param) then + doMenuItem(n) + break + end + end + end + + elseif sEvent == "paste" then + if not bReadOnly then + -- Close menu if open + if bMenu then + bMenu = false + term.setCursorBlink(true) + redrawMenu() + end + -- Input text + local sLine = tLines[y] + tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x) + setCursor(x + #param , y) + end + + elseif sEvent == "mouse_click" then + local cx, cy = param2, param3 + if not bMenu then + if param == 1 then + -- Left click + if cy < h then + local newY = math.min(math.max(scrollY + cy, 1), #tLines) + local newX = math.min(math.max(scrollX + cx, 1), #tLines[newY] + 1) + setCursor(newX, newY) + else + bMenu = true + redrawMenu() + end + end + else + if cy == h then + local nMenuPosEnd = 1 + local nMenuPosStart = 1 + for n, sMenuItem in ipairs(tMenuItems) do + nMenuPosEnd = nMenuPosEnd + #sMenuItem + 1 + if cx > nMenuPosStart and cx < nMenuPosEnd then + doMenuItem(n) + end + nMenuPosEnd = nMenuPosEnd + 1 + nMenuPosStart = nMenuPosEnd + end + else + bMenu = false + term.setCursorBlink(true) + redrawMenu() + end + end + + elseif sEvent == "mouse_scroll" then + if not bMenu then + if param == -1 then + -- Scroll up + if scrollY > 0 then + -- Move cursor up + scrollY = scrollY - 1 + redrawText() + end + + elseif param == 1 then + -- Scroll down + local nMaxScroll = #tLines - (h - 1) + if scrollY < nMaxScroll then + -- Move cursor down + scrollY = scrollY + 1 + redrawText() + end + + end + end + + elseif sEvent == "term_resize" then + w, h = term.getSize() + setCursor(x, y) + redrawMenu() + redrawText() + + end +end + +-- Cleanup +term.clear() +term.setCursorBlink(false) +term.setCursorPos(1, 1) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/eject.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/eject.lua new file mode 100644 index 000000000..ce538172d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/eject.lua @@ -0,0 +1,22 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +-- Get arguments +local tArgs = { ... } +if #tArgs == 0 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sDrive = tArgs[1] + +-- Check the disk exists +local bPresent = disk.isPresent(sDrive) +if not bPresent then + print("Nothing in " .. sDrive .. " drive") + return +end + +disk.eject(sDrive) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/exit.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/exit.lua new file mode 100644 index 000000000..ca41377e2 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/exit.lua @@ -0,0 +1,5 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +shell.exit() diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/0.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/0.dat new file mode 100644 index 000000000..7231b8421 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/0.dat @@ -0,0 +1,8 @@ +0 +77 77 +718888887 + 8 8 + 8 8 + 8 8 +788888897 +77 77 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/1.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/1.dat new file mode 100644 index 000000000..e51a790de --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/1.dat @@ -0,0 +1,7 @@ +1 + 777 + 7b7 + 787 +7777778777 +7188888887 +7777777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/10.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/10.dat new file mode 100644 index 000000000..bf1d7cb12 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/10.dat @@ -0,0 +1,11 @@ +5 + 777 77777 + 727777778837 + 788888878787 + 787777888887 +77877778777777 +7e8b7888b888e7 +7787787b777877 + 777887887887 + 7487807487 + 7777777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/11.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/11.dat new file mode 100644 index 000000000..304f5808f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/11.dat @@ -0,0 +1,10 @@ +4 + 777777777 + 727872787 + 787878787 +777787878787777 +7be888888888be7 +777787878787777 + 787878787 + 787478747 + 777777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/12.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/12.dat new file mode 100644 index 000000000..cad16afe4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/12.dat @@ -0,0 +1,12 @@ +6 +77 777 77 +72888888897 + 8 8 8 + 8 8b888 8 +78 e8888 87 +78888788887 +78 8888e 87 + 8 888b8 8 + 8 8 8 +75888888807 +77 777 77 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/2.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/2.dat new file mode 100644 index 000000000..0bf3034eb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/2.dat @@ -0,0 +1,10 @@ +1 +777777777 +7888888b7 +787778887 +787 78777 +7877787 +7888887 +7777787 + 707 + 777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/3.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/3.dat new file mode 100644 index 000000000..40c2c2518 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/3.dat @@ -0,0 +1,10 @@ +2 + 77777777 +777888188777 +7b78777787b7 +78787 78787 +78787 78787 +78887 78887 +777877778777 + 78838887 + 77777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/4.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/4.dat new file mode 100644 index 000000000..c77c49a9a --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/4.dat @@ -0,0 +1,10 @@ +2 + 77777777 +777778888887 +788888777787 +7b77787 787 +787 787 787 +7b77787 787 +7888887 787 +7777707 707 + 777 777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/5.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/5.dat new file mode 100644 index 000000000..5d084d9e4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/5.dat @@ -0,0 +1,10 @@ +3 +777777777 +788888887 +787787787 +787787787 +788888887 +787787787 +787787787 +78e748887 +777777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/6.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/6.dat new file mode 100644 index 000000000..38f0f42ea --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/6.dat @@ -0,0 +1,11 @@ +4 +7777777777 +7288888837 +78 87 +788888b 87 +788888b 87 +788888b 87 +788888b 87 +78 87 +7188888807 +7777777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/7.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/7.dat new file mode 100644 index 000000000..1456442e4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/7.dat @@ -0,0 +1,10 @@ +3 +728777778b7 +78888888887 +78777877787 +787 787 787 +787 7877788 +787 7888889 +88777877777 +e888887 +7777887 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/8.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/8.dat new file mode 100644 index 000000000..0eb44de0d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/8.dat @@ -0,0 +1,10 @@ +4 +777777 7777 +7287b7 7867 +788787 7887 +77878777877 + 7888eb8887 + 77877787877 + 7887 787887 + 7487 7e7807 + 7777 777777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/9.dat b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/9.dat new file mode 100644 index 000000000..9965aa10b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/levels/9.dat @@ -0,0 +1,12 @@ +2 + 777 777 + 777877778777 + 788838888887 +7778bbbbbbbb8777 +7888b888888b8897 +7878be8888eb8787 +7588b888888b8887 +7778bbbbbbbb8777 + 788888818887 + 777877778777 + 777 777 diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/paint.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/paint.lua new file mode 100644 index 000000000..2cb3e4bc8 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/paint.lua @@ -0,0 +1,456 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +-- Paint created by nitrogenfingers (edited by dan200) +-- http://www.youtube.com/user/NitrogenFingers + +------------ +-- Fields -- +------------ + +-- The width and height of the terminal +local w, h = term.getSize() + +-- The selected colours on the left and right mouse button, and the colour of the canvas +local leftColour, rightColour = colours.white, nil +local canvasColour = colours.black + +-- The values stored in the canvas +local canvas = {} + +-- The menu options +local mChoices = { "Save", "Exit" } + +-- The message displayed in the footer bar +local fMessage = "Press Ctrl or click here to access menu" + +------------------------- +-- Initialisation -- +------------------------- + +-- Determine if we can even run this +if not term.isColour() then + print("Requires an Advanced Computer") + return +end + +-- Determines if the file exists, and can be edited on this computer +local tArgs = { ... } +if #tArgs == 0 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end +local sPath = shell.resolve(tArgs[1]) +local bReadOnly = fs.isReadOnly(sPath) +if fs.exists(sPath) and fs.isDir(sPath) then + print("Cannot edit a directory.") + return +end + +-- Create .nfp files by default +if not fs.exists(sPath) and not string.find(sPath, "%.") then + local sExtension = settings.get("paint.default_extension") + if sExtension ~= "" and type(sExtension) == "string" then + sPath = sPath .. "." .. sExtension + end +end + + +--------------- +-- Functions -- +--------------- + +local function getCanvasPixel(x, y) + if canvas[y] then + return canvas[y][x] + end + return nil +end + +--[[ + Converts a colour value to a text character + params: colour = the number to convert to a hex value + returns: a string representing the chosen colour +]] +local function getCharOf(colour) + -- Incorrect values always convert to nil + if type(colour) == "number" then + local value = math.floor(math.log(colour) / math.log(2)) + 1 + if value >= 1 and value <= 16 then + return string.sub("0123456789abcdef", value, value) + end + end + return " " +end + +--[[ + Converts a text character to colour value + params: char = the char (from string.byte) to convert to number + returns: the colour number of the hex value +]] +local tColourLookup = {} +for n = 1, 16 do + tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) +end +local function getColourOf(char) + -- Values not in the hex table are transparent (canvas coloured) + return tColourLookup[char] +end + +--[[ + Loads the file into the canvas + params: path = the path of the file to open + returns: nil +]] +local function load(path) + -- Load the file + if fs.exists(path) then + local file = fs.open(sPath, "r") + local sLine = file.readLine() + while sLine do + local line = {} + for x = 1, w - 2 do + line[x] = getColourOf(string.byte(sLine, x, x)) + end + table.insert(canvas, line) + sLine = file.readLine() + end + file.close() + end +end + +--[[ + Saves the current canvas to file + params: path = the path of the file to save + returns: true if save was successful, false otherwise +]] +local function save(path) + -- Open file + local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath)) + if not fs.exists(sDir) then + fs.makeDir(sDir) + end + + local file, err = fs.open(path, "w") + if not file then + return false, err + end + + -- Encode (and trim) + local tLines = {} + local nLastLine = 0 + for y = 1, h - 1 do + local sLine = "" + local nLastChar = 0 + for x = 1, w - 2 do + local c = getCharOf(getCanvasPixel(x, y)) + sLine = sLine .. c + if c ~= " " then + nLastChar = x + end + end + sLine = string.sub(sLine, 1, nLastChar) + tLines[y] = sLine + if #sLine > 0 then + nLastLine = y + end + end + + -- Save out + for n = 1, nLastLine do + file.writeLine(tLines[n]) + end + file.close() + return true +end + +--[[ + Draws colour picker sidebar, the palette and the footer + returns: nil +]] +local function drawInterface() + -- Footer + term.setCursorPos(1, h) + term.setBackgroundColour(colours.black) + term.setTextColour(colours.yellow) + term.clearLine() + term.write(fMessage) + + -- Colour Picker + for i = 1, 16 do + term.setCursorPos(w - 1, i) + term.setBackgroundColour(2 ^ (i - 1)) + term.write(" ") + end + + term.setCursorPos(w - 1, 17) + term.setBackgroundColour(canvasColour) + term.setTextColour(colours.grey) + term.write("\127\127") + + -- Left and Right Selected Colours + do + term.setCursorPos(w - 1, 18) + if leftColour ~= nil then + term.setBackgroundColour(leftColour) + term.write(" ") + else + term.setBackgroundColour(canvasColour) + term.setTextColour(colours.grey) + term.write("\127") + end + if rightColour ~= nil then + term.setBackgroundColour(rightColour) + term.write(" ") + else + term.setBackgroundColour(canvasColour) + term.setTextColour(colours.grey) + term.write("\127") + end + end + + -- Padding + term.setBackgroundColour(canvasColour) + for i = 20, h - 1 do + term.setCursorPos(w - 1, i) + term.write(" ") + end +end + +--[[ + Converts a single pixel of a single line of the canvas and draws it + returns: nil +]] +local function drawCanvasPixel(x, y) + local pixel = getCanvasPixel(x, y) + if pixel then + term.setBackgroundColour(pixel or canvasColour) + term.setCursorPos(x, y) + term.write(" ") + else + term.setBackgroundColour(canvasColour) + term.setTextColour(colours.grey) + term.setCursorPos(x, y) + term.write("\127") + end +end + +local color_hex_lookup = {} +for i = 0, 15 do + color_hex_lookup[2 ^ i] = string.format("%x", i) +end + +--[[ + Converts each colour in a single line of the canvas and draws it + returns: nil +]] +local function drawCanvasLine(y) + local text, fg, bg = "", "", "" + for x = 1, w - 2 do + local pixel = getCanvasPixel(x, y) + if pixel then + text = text .. " " + fg = fg .. "0" + bg = bg .. color_hex_lookup[pixel or canvasColour] + else + text = text .. "\127" + fg = fg .. color_hex_lookup[colours.grey] + bg = bg .. color_hex_lookup[canvasColour] + end + end + + term.setCursorPos(1, y) + term.blit(text, fg, bg) +end + +--[[ + Converts each colour in the canvas and draws it + returns: nil +]] +local function drawCanvas() + for y = 1, h - 1 do + drawCanvasLine(y) + end +end + +local menu_choices = { + Save = function() + if bReadOnly then + fMessage = "Access denied" + return false + end + local success, err = save(sPath) + if success then + fMessage = "Saved to " .. sPath + else + if err then + fMessage = "Error saving to " .. err + else + fMessage = "Error saving to " .. sPath + end + end + return false + end, + Exit = function() + sleep(0) -- Super janky, but consumes stray "char" events from pressing Ctrl then E separately. + return true + end, +} + +--[[ + Draws menu options and handles input from within the menu. + returns: true if the program is to be exited; false otherwise +]] +local function accessMenu() + -- Selected menu option + local selection = 1 + + term.setBackgroundColour(colours.black) + + while true do + -- Draw the menu + term.setCursorPos(1, h) + term.clearLine() + term.setTextColour(colours.white) + for k, v in pairs(mChoices) do + if selection == k then + term.setTextColour(colours.yellow) + term.write("[") + term.setTextColour(colours.white) + term.write(v) + term.setTextColour(colours.yellow) + term.write("]") + term.setTextColour(colours.white) + else + term.write(" " .. v .. " ") + end + end + + -- Handle input in the menu + local id, param1, param2, param3 = os.pullEvent() + if id == "key" then + local key = param1 + + -- Handle menu shortcuts. + for _, menu_item in ipairs(mChoices) do + local k = keys[menu_item:sub(1, 1):lower()] + if k and k == key then + return menu_choices[menu_item]() + end + end + + if key == keys.right then + -- Move right + selection = selection + 1 + if selection > #mChoices then + selection = 1 + end + + elseif key == keys.left and selection > 1 then + -- Move left + selection = selection - 1 + if selection < 1 then + selection = #mChoices + end + + elseif key == keys.enter or key == keys.numPadEnter then + -- Select an option + return menu_choices[mChoices[selection]]() + elseif key == keys.leftCtrl or keys == keys.rightCtrl then + -- Cancel the menu + return false + end + elseif id == "mouse_click" then + local cx, cy = param2, param3 + if cy ~= h then return false end -- Exit the menu + + local nMenuPosEnd = 1 + local nMenuPosStart = 1 + for _, sMenuItem in ipairs(mChoices) do + nMenuPosEnd = nMenuPosEnd + #sMenuItem + 1 + if cx > nMenuPosStart and cx < nMenuPosEnd then + return menu_choices[sMenuItem]() + end + nMenuPosEnd = nMenuPosEnd + 1 + nMenuPosStart = nMenuPosEnd + end + end + end +end + +--[[ + Runs the main thread of execution. Draws the canvas and interface, and handles + mouse and key events. + returns: nil +]] +local function handleEvents() + local programActive = true + while programActive do + local id, p1, p2, p3 = os.pullEvent() + if id == "mouse_click" or id == "mouse_drag" then + if p2 >= w - 1 and p3 >= 1 and p3 <= 17 then + if id ~= "mouse_drag" then + -- Selecting an items in the colour picker + if p3 <= 16 then + if p1 == 1 then + leftColour = 2 ^ (p3 - 1) + else + rightColour = 2 ^ (p3 - 1) + end + else + if p1 == 1 then + leftColour = nil + else + rightColour = nil + end + end + --drawCanvas() + drawInterface() + end + elseif p2 < w - 1 and p3 <= h - 1 then + -- Clicking on the canvas + local paintColour = nil + if p1 == 1 then + paintColour = leftColour + elseif p1 == 2 then + paintColour = rightColour + end + if not canvas[p3] then + canvas[p3] = {} + end + canvas[p3][p2] = paintColour + + drawCanvasPixel(p2, p3) + elseif p3 == h and id == "mouse_click" then + -- Open menu + programActive = not accessMenu() + drawInterface() + end + elseif id == "key" then + if p1 == keys.leftCtrl or p1 == keys.rightCtrl then + programActive = not accessMenu() + drawInterface() + end + elseif id == "term_resize" then + w, h = term.getSize() + drawCanvas() + drawInterface() + end + end +end + +-- Init +load(sPath) +drawCanvas() +drawInterface() + +-- Main loop +handleEvents() + +-- Shutdown +term.setBackgroundColour(colours.black) +term.setTextColour(colours.white) +term.clear() +term.setCursorPos(1, 1) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/redirection.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/redirection.lua new file mode 100644 index 000000000..b6bb26d90 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/advanced/redirection.lua @@ -0,0 +1,707 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--CCRedirection by : RamiLego4Game and Dan200-- +--Based on Redirection by Dan200: http://www.redirectiongame.com-- +--Clearing Screen-- + +--Vars-- +local TermW, TermH = term.getSize() + +local sLevelTitle +local tScreen +local oScreen +local SizeW, SizeH +local aExits +local fExit +local nSpeed +local Speed +local fSpeed +local fSpeedS +local bPaused +local Tick +local Blocks +local XOrgin, YOrgin +local fLevel + +local function reset() + sLevelTitle = "" + tScreen = {} + oScreen = {} + SizeW, SizeH = TermW, TermH + aExits = 0 + fExit = "nop" + nSpeed = 0.6 + Speed = nSpeed + fSpeed = 0.2 + fSpeedS = false + bPaused = false + Tick = os.startTimer(Speed) + Blocks = 0 + XOrgin, YOrgin = 1, 1 + + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() +end + +local InterFace = {} +InterFace.cExit = colors.red +InterFace.cSpeedD = colors.white +InterFace.cSpeedA = colors.red +InterFace.cTitle = colors.red + +local cG = colors.lightGray +local cW = colors.gray +local cS = colors.black +local cR1 = colors.blue +local cR2 = colors.red +local cR3 = colors.green +local cR4 = colors.yellow + +local tArgs = { ... } + +--Functions-- +local function printCentred(yc, stg) + local xc = math.floor((TermW - #stg) / 2) + 1 + term.setCursorPos(xc, yc) + term.write(stg) +end + +local function centerOrgin() + XOrgin = math.floor(TermW / 2 - SizeW / 2) + YOrgin = math.floor(TermH / 2 - SizeH / 2) +end + +local function reMap() + tScreen = nil + tScreen = {} + for x = 1, SizeW do + tScreen[x] = {} + for y = 1, SizeH do + tScreen[x][y] = { space = true, wall = false, ground = false, robot = "zz", start = "zz", exit = "zz" } + end + end +end + +local function tablecopy(t) + local t2 = {} + for k, v in pairs(t) do + t2[k] = v + end + return t2 +end + +local function buMap() + oScreen = nil + oScreen = {} + for x = 1, SizeW do + oScreen[x] = {} + for y = 1, SizeH do + oScreen[x][y] = tablecopy(tScreen[x][y]) + end + end +end + +local function addRobot(x, y, side, color) + local obj = tScreen[x][y] + local data = side .. color + if obj.wall == nil and obj.robot == nil then + tScreen[x][y].robot = data + else + obj.wall = nil + obj.robot = "zz" + tScreen[x][y].robot = data + end +end + +local function addStart(x, y, side, color) + local obj = tScreen[x][y] + local data = side .. color + if obj.wall == nil and obj.space == nil then + tScreen[x][y].start = data + else + obj.wall = nil + obj.space = nil + tScreen[x][y].start = data + end + aExits = aExits + 1 +end + +local function addGround(x, y) + local obj = tScreen[x][y] + if obj.space == nil and obj.exit == nil and obj.wall == nil and obj.robot == nil and obj.start == nil then + tScreen[x][y].ground = true + else + obj.space = nil + obj.exit = "zz" + obj.wall = nil + obj.robot = "zz" + obj.start = "zz" + tScreen[x][y].ground = true + end +end + +local function addExit(x, y, cl) + local obj = tScreen[x][y] + if obj.space == nil and obj.ground == nil and obj.wall == nil and obj.robot == nil and obj.start == nil then + tScreen[x][y].exit = cl + else + obj.space = nil + obj.ground = nil + obj.wall = nil + obj.robot = "zz" + obj.start = "zz" + tScreen[x][y].exit = cl + end +end + +local function addWall(x, y) + local obj = tScreen[x][y] + if obj == nil then + return error("Here X" .. x .. " Y" .. y) + end + if obj.space == nil and obj.exit == nil and obj.ground == nil and obj.robot == nil and obj.start == nil then + tScreen[x][y].wall = true + else + obj.space = nil + obj.exit = nil + obj.ground = nil + obj.robot = nil + obj.start = nil + tScreen[x][y].wall = true + end +end + +local function loadLevel(nNum) + sLevelTitle = "Level " .. nNum + if nNum == nil then return error("nNum == nil") end + local sDir = fs.getDir(shell.getRunningProgram()) + local sLevelD = sDir .. "/levels/" .. tostring(nNum) .. ".dat" + if not (fs.exists(sLevelD) or fs.isDir(sLevelD)) then return error("Level Not Exists : " .. sLevelD) end + fLevel = fs.open(sLevelD, "r") + local wl = true + Blocks = tonumber(string.sub(fLevel.readLine(), 1, 1)) + local xSize = #fLevel.readLine() + 2 + local Lines = 3 + while wl do + local wLine = fLevel.readLine() + if wLine == nil then + fLevel.close() + wl = false + else + xSize = math.max(#wLine + 2, xSize) + Lines = Lines + 1 + end + end + SizeW, SizeH = xSize, Lines + reMap() + fLevel = fs.open(sLevelD, "r") + fLevel.readLine() + for Line = 2, Lines - 1 do + local sLine = fLevel.readLine() + local chars = #sLine + for char = 1, chars do + local el = string.sub(sLine, char, char) + if el == "8" then + addGround(char + 1, Line) + elseif el == "0" then + addStart(char + 1, Line, "a", "a") + elseif el == "1" then + addStart(char + 1, Line, "b", "a") + elseif el == "2" then + addStart(char + 1, Line, "c", "a") + elseif el == "3" then + addStart(char + 1, Line, "d", "a") + elseif el == "4" then + addStart(char + 1, Line, "a", "b") + elseif el == "5" then + addStart(char + 1, Line, "b", "b") + elseif el == "6" then + addStart(char + 1, Line, "c", "b") + elseif el == "9" then + addStart(char + 1, Line, "d", "b") + elseif el == "b" then + addExit(char + 1, Line, "a") + elseif el == "e" then + addExit(char + 1, Line, "b") + elseif el == "7" then + addWall(char + 1, Line) + end + end + end + fLevel.close() +end + +local function drawStars() + --CCR Background By : RamiLego-- + local cStar, cStarG, crStar, crStarB = colors.lightGray, colors.gray, ".", "*" + local DStar, BStar, nStar, gStar = 14, 10, 16, 3 + local TermW, TermH = term.getSize() + + term.clear() + term.setCursorPos(1, 1) + for x = 1, TermW do + for y = 1, TermH do + local StarT = math.random(1, 30) + if StarT == DStar then + term.setCursorPos(x, y) + term.setTextColor(cStar) + write(crStar) + elseif StarT == BStar then + term.setCursorPos(x, y) + term.setTextColor(cStar) + write(crStarB) + elseif StarT == nStar then + term.setCursorPos(x, y) + term.setTextColor(cStarG) + write(crStar) + elseif StarT == gStar then + term.setCursorPos(x, y) + term.setTextColor(cStarG) + write(crStarB) + end + end + end +end + +local function drawMap() + for x = 1, SizeW do + for y = 1, SizeH do + + local obj = tScreen[x][y] + if obj.ground == true then + paintutils.drawPixel(XOrgin + x, YOrgin + y + 1, cG) + end + if obj.wall == true then + paintutils.drawPixel(XOrgin + x, YOrgin + y + 1, cW) + end + + local ex = tostring(tScreen[x][y].exit) + if not(ex == "zz" or ex == "nil") then + if ex == "a" then + ex = cR1 + elseif ex == "b" then + ex = cR2 + elseif ex == "c" then + ex = cR3 + elseif ex == "d" then + ex = cR4 + else + return error("Exit Color Out") + end + term.setBackgroundColor(cG) + term.setTextColor(ex) + term.setCursorPos(XOrgin + x, YOrgin + y + 1) + print("X") + end + + local st = tostring(tScreen[x][y].start) + if not(st == "zz" or st == "nil") then + local Cr = string.sub(st, 2, 2) + if Cr == "a" then + Cr = cR1 + elseif Cr == "b" then + Cr = cR2 + elseif Cr == "c" then + Cr = cR3 + elseif Cr == "d" then + Cr = cR4 + else + return error("Start Color Out") + end + + term.setTextColor(Cr) + term.setBackgroundColor(cG) + term.setCursorPos(XOrgin + x, YOrgin + y + 1) + + local sSide = string.sub(st, 1, 1) + if sSide == "a" then + print("^") + elseif sSide == "b" then + print(">") + elseif sSide == "c" then + print("v") + elseif sSide == "d" then + print("<") + else + print("@") + end + end + + if obj.space == true then + paintutils.drawPixel(XOrgin + x, YOrgin + y + 1, cS) + end + + local rb = tostring(tScreen[x][y].robot) + if not(rb == "zz" or rb == "nil") then + local Cr = string.sub(rb, 2, 2) + if Cr == "a" then + Cr = cR1 + elseif Cr == "b" then + Cr = cR2 + elseif Cr == "c" then + Cr = cR3 + elseif Cr == "d" then + Cr = cR4 + else + Cr = colors.white + end + term.setBackgroundColor(Cr) + term.setTextColor(colors.white) + term.setCursorPos(XOrgin + x, YOrgin + y + 1) + local sSide = string.sub(rb, 1, 1) + if sSide == "a" then + print("^") + elseif sSide == "b" then + print(">") + elseif sSide == "c" then + print("v") + elseif sSide == "d" then + print("<") + else + print("@") + end + end + end + end +end + +local function isBrick(x, y) + local brb = tostring(tScreen[x][y].robot) + local bobj = oScreen[x][y] + if (brb == "zz" or brb == "nil") and not bobj.wall == true then + return false + else + return true + end +end + +local function gRender(sContext) + if sContext == "start" then + for x = 1, SizeW do + for y = 1, SizeH do + local st = tostring(tScreen[x][y].start) + if not(st == "zz" or st == "nil") then + local Cr = string.sub(st, 2, 2) + local sSide = string.sub(st, 1, 1) + addRobot(x, y, sSide, Cr) + end + end + end + elseif sContext == "tick" then + buMap() + for x = 1, SizeW do + for y = 1, SizeH do + local rb = tostring(oScreen[x][y].robot) + if not(rb == "zz" or rb == "nil") then + local Cr = string.sub(rb, 2, 2) + local sSide = string.sub(rb, 1, 1) + local sobj = oScreen[x][y] + if sobj.space == true then + tScreen[x][y].robot = "zz" + if not sSide == "g" then + addRobot(x, y, "g", Cr) + end + elseif sobj.exit == Cr then + if sSide == "a" or sSide == "b" or sSide == "c" or sSide == "d" then + tScreen[x][y].robot = "zz" + addRobot(x, y, "g", Cr) + aExits = aExits - 1 + end + elseif sSide == "a" then + local obj = isBrick(x, y - 1) + tScreen[x][y].robot = "zz" + if not obj == true then + addRobot(x, y - 1, sSide, Cr) + else + local obj2 = isBrick(x - 1, y) + local obj3 = isBrick(x + 1, y) + if not obj2 == true and not obj3 == true then + if Cr == "a" then + addRobot(x, y, "d", Cr) + elseif Cr == "b" then + addRobot(x, y, "b", Cr) + end + elseif obj == true and obj2 == true and obj3 == true then + addRobot(x, y, "c", Cr) + else + if obj3 == true then + addRobot(x, y, "d", Cr) + elseif obj2 == true then + addRobot(x, y, "b", Cr) + end + end + end + elseif sSide == "b" then + local obj = isBrick(x + 1, y) + tScreen[x][y].robot = "zz" + if not obj == true then + addRobot(x + 1, y, sSide, Cr) + else + local obj2 = isBrick(x, y - 1) + local obj3 = isBrick(x, y + 1) + if not obj2 == true and not obj3 == true then + if Cr == "a" then + addRobot(x, y, "a", Cr) + elseif Cr == "b" then + addRobot(x, y, "c", Cr) + end + elseif obj == true and obj2 == true and obj3 == true then + addRobot(x, y, "d", Cr) + else + if obj3 == true then + addRobot(x, y, "a", Cr) + elseif obj2 == true then + addRobot(x, y, "c", Cr) + end + end + end + elseif sSide == "c" then + local obj = isBrick(x, y + 1) + tScreen[x][y].robot = "zz" + if not obj == true then + addRobot(x, y + 1, sSide, Cr) + else + local obj2 = isBrick(x - 1, y) + local obj3 = isBrick(x + 1, y) + if not obj2 == true and not obj3 == true then + if Cr == "a" then + addRobot(x, y, "b", Cr) + elseif Cr == "b" then + addRobot(x, y, "d", Cr) + end + elseif obj == true and obj2 == true and obj3 == true then + addRobot(x, y, "a", Cr) + else + if obj3 == true then + addRobot(x, y, "d", Cr) + elseif obj2 == true then + addRobot(x, y, "b", Cr) + end + end + end + elseif sSide == "d" then + local obj = isBrick(x - 1, y) + tScreen[x][y].robot = "zz" + if not obj == true then + addRobot(x - 1, y, sSide, Cr) + else + local obj2 = isBrick(x, y - 1) + local obj3 = isBrick(x, y + 1) + if not obj2 == true and not obj3 == true then + if Cr == "a" then + addRobot(x, y, "c", Cr) + elseif Cr == "b" then + addRobot(x, y, "a", Cr) + end + elseif obj == true and obj2 == true and obj3 == true then + addRobot(x, y, "b", Cr) + else + if obj3 == true then + addRobot(x, y, "a", Cr) + elseif obj2 == true then + addRobot(x, y, "c", Cr) + end + end + end + else + addRobot(x, y, sSide, "g") + end + end + end + end + end +end + +function InterFace.drawBar() + term.setBackgroundColor(colors.black) + term.setTextColor(InterFace.cTitle) + printCentred(1, " " .. sLevelTitle .. " ") + + term.setCursorPos(1, 1) + term.setBackgroundColor(cW) + write(" ") + term.setBackgroundColor(colors.black) + write(" x " .. tostring(Blocks) .. " ") + + term.setCursorPos(TermW - 8, TermH) + term.setBackgroundColor(colors.black) + term.setTextColour(InterFace.cSpeedD) + write(" <<") + if bPaused then + term.setTextColour(InterFace.cSpeedA) + else + term.setTextColour(InterFace.cSpeedD) + end + write(" ||") + if fSpeedS then + term.setTextColour(InterFace.cSpeedA) + else + term.setTextColour(InterFace.cSpeedD) + end + write(" >>") + + term.setCursorPos(TermW - 1, 1) + term.setBackgroundColor(colors.black) + term.setTextColour(InterFace.cExit) + write(" X") + term.setBackgroundColor(colors.black) +end + +function InterFace.render() + local id, p1, p2, p3 = os.pullEvent() + if id == "mouse_click" then + if p3 == 1 and p2 == TermW then + return "end" + elseif p3 == TermH and p2 >= TermW - 7 and p2 <= TermW - 6 then + return "retry" + elseif p3 == TermH and p2 >= TermW - 4 and p2 <= TermW - 3 then + bPaused = not bPaused + fSpeedS = false + Speed = bPaused and 0 or nSpeed + if Speed > 0 then + Tick = os.startTimer(Speed) + else + Tick = nil + end + InterFace.drawBar() + elseif p3 == TermH and p2 >= TermW - 1 then + bPaused = false + fSpeedS = not fSpeedS + Speed = fSpeedS and fSpeed or nSpeed + Tick = os.startTimer(Speed) + InterFace.drawBar() + elseif p3 - 1 < YOrgin + SizeH + 1 and p3 - 1 > YOrgin and + p2 < XOrgin + SizeW + 1 and p2 > XOrgin then + local eobj = tScreen[p2 - XOrgin][p3 - YOrgin - 1] + local erobj = tostring(tScreen[p2 - XOrgin][p3 - YOrgin - 1].robot) + if (erobj == "zz" or erobj == "nil") and not eobj.wall == true and not eobj.space == true and Blocks > 0 then + addWall(p2 - XOrgin, p3 - YOrgin - 1) + Blocks = Blocks - 1 + InterFace.drawBar() + drawMap() + end + end + elseif id == "timer" and p1 == Tick then + gRender("tick") + drawMap() + if Speed > 0 then + Tick = os.startTimer(Speed) + else + Tick = nil + end + end +end + +local function startG(LevelN) + drawStars() + loadLevel(LevelN) + centerOrgin() + drawMap() + InterFace.drawBar() + gRender("start") + drawMap() + + local NExit = true + if aExits == 0 then + NExit = false + end + + while true do + local isExit = InterFace.render() + if isExit == "end" then + return nil + elseif isExit == "retry" then + return LevelN + elseif fExit == "yes" then + if fs.exists(fs.getDir(shell.getRunningProgram()) .. "/levels/" .. tostring(LevelN + 1) .. ".dat") then + return LevelN + 1 + else + return nil + end + end + if aExits == 0 and NExit == true then + fExit = "yes" + end + end +end + +local ok, err = true, nil + +--Menu-- +local sStartLevel = tArgs[1] +if ok and not sStartLevel then + ok, err = pcall(function() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + drawStars() + term.setTextColor(colors.red) + printCentred(TermH / 2 - 1, " REDIRECTION ") + printCentred(TermH / 2 - 0, " ComputerCraft Edition ") + term.setTextColor(colors.yellow) + printCentred(TermH / 2 + 2, " Click to Begin ") + os.pullEvent("mouse_click") + end) +end + +--Game-- +if ok then + ok, err = pcall(function() + local nLevel + if sStartLevel then + nLevel = tonumber(sStartLevel) + else + nLevel = 1 + end + while nLevel do + reset() + nLevel = startG(nLevel) + end + end) +end + +--Upsell screen-- +if ok then + ok, err = pcall(function() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + drawStars() + term.setTextColor(colors.red) + if TermW >= 40 then + printCentred(TermH / 2 - 1, " Thank you for playing Redirection ") + printCentred(TermH / 2 - 0, " ComputerCraft Edition ") + printCentred(TermH / 2 + 2, " Check out the full game: ") + term.setTextColor(colors.yellow) + printCentred(TermH / 2 + 3, " http://www.redirectiongame.com ") + else + printCentred(TermH / 2 - 2, " Thank you for ") + printCentred(TermH / 2 - 1, " playing Redirection ") + printCentred(TermH / 2 - 0, " ComputerCraft Edition ") + printCentred(TermH / 2 + 2, " Check out the full game: ") + term.setTextColor(colors.yellow) + printCentred(TermH / 2 + 3, " www.redirectiongame.com ") + end + parallel.waitForAll( + function() sleep(2) end, + function() os.pullEvent("mouse_click") end + ) + end) +end + +--Clear and exit-- +term.setCursorPos(1, 1) +term.setTextColor(colors.white) +term.setBackgroundColor(colors.black) +term.clear() +if not ok then + if err == "Terminated" then + print("Check out the full version of Redirection:") + print("http://www.redirectiongame.com") + else + printError(err) + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/adventure.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/adventure.lua new file mode 100644 index 000000000..b374ff835 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/adventure.lua @@ -0,0 +1,1343 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tBiomes = { + "in a forest", + "in a pine forest", + "knee deep in a swamp", + "in a mountain range", + "in a desert", + "in a grassy plain", + "in frozen tundra", +} + +local function hasTrees(_nBiome) + return _nBiome <= 3 +end + +local function hasStone(_nBiome) + return _nBiome == 4 +end + +local function hasRivers(_nBiome) + return _nBiome ~= 3 and _nBiome ~= 5 +end + +local items = { + ["no tea"] = { + droppable = false, + desc = "Pull yourself together man.", + }, + ["a pig"] = { + heavy = true, + creature = true, + drops = { "some pork" }, + aliases = { "pig" }, + desc = "The pig has a square nose.", + }, + ["a cow"] = { + heavy = true, + creature = true, + aliases = { "cow" }, + desc = "The cow stares at you blankly.", + }, + ["a sheep"] = { + heavy = true, + creature = true, + hitDrops = { "some wool" }, + aliases = { "sheep" }, + desc = "The sheep is fluffy.", + }, + ["a chicken"] = { + heavy = true, + creature = true, + drops = { "some chicken" }, + aliases = { "chicken" }, + desc = "The chicken looks delicious.", + }, + ["a creeper"] = { + heavy = true, + creature = true, + monster = true, + aliases = { "creeper" }, + desc = "The creeper needs a hug.", + }, + ["a skeleton"] = { + heavy = true, + creature = true, + monster = true, + aliases = { "skeleton" }, + nocturnal = true, + desc = "The head bone's connected to the neck bone, the neck bone's connected to the chest bone, the chest bone's connected to the arm bone, the arm bone's connected to the bow, and the bow is pointed at you.", + }, + ["a zombie"] = { + heavy = true, + creature = true, + monster = true, + aliases = { "zombie" }, + nocturnal = true, + desc = "All he wants to do is eat your brains.", + }, + ["a spider"] = { + heavy = true, + creature = true, + monster = true, + aliases = { "spider" }, + desc = "Dozens of eyes stare back at you.", + }, + ["a cave entrance"] = { + heavy = true, + aliases = { "cave entance", "cave", "entrance" }, + desc = "The entrance to the cave is dark, but it looks like you can climb down.", + }, + ["an exit to the surface"] = { + heavy = true, + aliases = { "exit to the surface", "exit", "opening" }, + desc = "You can just see the sky through the opening.", + }, + ["a river"] = { + heavy = true, + aliases = { "river" }, + desc = "The river flows majestically towards the horizon. It doesn't do anything else.", + }, + ["some wood"] = { + aliases = { "wood" }, + material = true, + desc = "You could easily craft this wood into planks.", + }, + ["some planks"] = { + aliases = { "planks", "wooden planks", "wood planks" }, + desc = "You could easily craft these planks into sticks.", + }, + ["some sticks"] = { + aliases = { "sticks", "wooden sticks", "wood sticks" }, + desc = "A perfect handle for torches or a pickaxe.", + }, + ["a crafting table"] = { + aliases = { "crafting table", "craft table", "work bench", "workbench", "crafting bench", "table" }, + desc = "It's a crafting table. I shouldn't tell you this, but these don't actually do anything in this game, you can craft tools whenever you like.", + }, + ["a furnace"] = { + aliases = { "furnace" }, + desc = "It's a furnace. Between you and me, these don't actually do anything in this game.", + }, + ["a wooden pickaxe"] = { + aliases = { "pickaxe", "pick", "wooden pick", "wooden pickaxe", "wood pick", "wood pickaxe" }, + tool = true, + toolLevel = 1, + toolType = "pick", + desc = "The pickaxe looks good for breaking stone and coal.", + }, + ["a stone pickaxe"] = { + aliases = { "pickaxe", "pick", "stone pick", "stone pickaxe" }, + tool = true, + toolLevel = 2, + toolType = "pick", + desc = "The pickaxe looks good for breaking iron.", + }, + ["an iron pickaxe"] = { + aliases = { "pickaxe", "pick", "iron pick", "iron pickaxe" }, + tool = true, + toolLevel = 3, + toolType = "pick", + desc = "The pickaxe looks strong enough to break diamond.", + }, + ["a diamond pickaxe"] = { + aliases = { "pickaxe", "pick", "diamond pick", "diamond pickaxe" }, + tool = true, + toolLevel = 4, + toolType = "pick", + desc = "Best. Pickaxe. Ever.", + }, + ["a wooden sword"] = { + aliases = { "sword", "wooden sword", "wood sword" }, + tool = true, + toolLevel = 1, + toolType = "sword", + desc = "Flimsy, but better than nothing.", + }, + ["a stone sword"] = { + aliases = { "sword", "stone sword" }, + tool = true, + toolLevel = 2, + toolType = "sword", + desc = "A pretty good sword.", + }, + ["an iron sword"] = { + aliases = { "sword", "iron sword" }, + tool = true, + toolLevel = 3, + toolType = "sword", + desc = "This sword can slay any enemy.", + }, + ["a diamond sword"] = { + aliases = { "sword", "diamond sword" }, + tool = true, + toolLevel = 4, + toolType = "sword", + desc = "Best. Sword. Ever.", + }, + ["a wooden shovel"] = { + aliases = { "shovel", "wooden shovel", "wood shovel" }, + tool = true, + toolLevel = 1, + toolType = "shovel", + desc = "Good for digging holes.", + }, + ["a stone shovel"] = { + aliases = { "shovel", "stone shovel" }, + tool = true, + toolLevel = 2, + toolType = "shovel", + desc = "Good for digging holes.", + }, + ["an iron shovel"] = { + aliases = { "shovel", "iron shovel" }, + tool = true, + toolLevel = 3, + toolType = "shovel", + desc = "Good for digging holes.", + }, + ["a diamond shovel"] = { + aliases = { "shovel", "diamond shovel" }, + tool = true, + toolLevel = 4, + toolType = "shovel", + desc = "Good for digging holes.", + }, + ["some coal"] = { + aliases = { "coal" }, + ore = true, + toolLevel = 1, + toolType = "pick", + desc = "That coal looks useful for building torches, if only you had a pickaxe to mine it.", + }, + ["some dirt"] = { + aliases = { "dirt" }, + material = true, + desc = "Why not build a mud hut?", + }, + ["some stone"] = { + aliases = { "stone", "cobblestone" }, + material = true, + ore = true, + infinite = true, + toolLevel = 1, + toolType = "pick", + desc = "Stone is useful for building things, and making stone pickaxes.", + }, + ["some iron"] = { + aliases = { "iron" }, + material = true, + ore = true, + toolLevel = 2, + toolType = "pick", + desc = "That iron looks mighty strong, you'll need a stone pickaxe to mine it.", + }, + ["some diamond"] = { + aliases = { "diamond", "diamonds" }, + material = true, + ore = true, + toolLevel = 3, + toolType = "pick", + desc = "Sparkly, rare, and impossible to mine without an iron pickaxe.", + }, + ["some torches"] = { + aliases = { "torches", "torch" }, + desc = "These won't run out for a while.", + }, + ["a torch"] = { + aliases = { "torch" }, + desc = "Fire, fire, burn so bright, won't you light my cave tonight?", + }, + ["some wool"] = { + aliases = { "wool" }, + material = true, + desc = "Soft and good for building.", + }, + ["some pork"] = { + aliases = { "pork", "porkchops" }, + food = true, + desc = "Delicious and nutritious.", + }, + ["some chicken"] = { + aliases = { "chicken" }, + food = true, + desc = "Finger licking good.", + }, +} + +local tAnimals = { + "a pig", "a cow", "a sheep", "a chicken", +} + +local tMonsters = { + "a creeper", "a skeleton", "a zombie", "a spider", +} + +local tRecipes = { + ["some planks"] = { "some wood" }, + ["some sticks"] = { "some planks" }, + ["a crafting table"] = { "some planks" }, + ["a furnace"] = { "some stone" }, + ["some torches"] = { "some sticks", "some coal" }, + + ["a wooden pickaxe"] = { "some planks", "some sticks" }, + ["a stone pickaxe"] = { "some stone", "some sticks" }, + ["an iron pickaxe"] = { "some iron", "some sticks" }, + ["a diamond pickaxe"] = { "some diamond", "some sticks" }, + + ["a wooden sword"] = { "some planks", "some sticks" }, + ["a stone sword"] = { "some stone", "some sticks" }, + ["an iron sword"] = { "some iron", "some sticks" }, + ["a diamond sword"] = { "some diamond", "some sticks" }, + + ["a wooden shovel"] = { "some planks", "some sticks" }, + ["a stone shovel"] = { "some stone", "some sticks" }, + ["an iron shovel"] = { "some iron", "some sticks" }, + ["a diamond shovel"] = { "some diamond", "some sticks" }, +} + +local tGoWest = { + "(life is peaceful there)", + "(lots of open air)", + "(to begin life anew)", + "(this is what we'll do)", + "(sun in winter time)", + "(we will do just fine)", + "(where the skies are blue)", + "(this and more we'll do)", +} +local nGoWest = 0 + +local bRunning = true +local tMap = { { {} } } +local x, y, z = 0, 0, 0 +local inventory = { + ["no tea"] = items["no tea"], +} + +local nTurn = 0 +local nTimeInRoom = 0 +local bInjured = false + +local tDayCycle = { + "It is daytime.", + "It is daytime.", + "It is daytime.", + "It is daytime.", + "It is daytime.", + "It is daytime.", + "It is daytime.", + "It is daytime.", + "The sun is setting.", + "It is night.", + "It is night.", + "It is night.", + "It is night.", + "It is night.", + "The sun is rising.", +} + +local function getTimeOfDay() + return math.fmod(math.floor(nTurn / 3), #tDayCycle) + 1 +end + +local function isSunny() + return getTimeOfDay() < 10 +end + +local function getRoom(x, y, z, dontCreate) + tMap[x] = tMap[x] or {} + tMap[x][y] = tMap[x][y] or {} + if not tMap[x][y][z] and dontCreate ~= true then + local room = { + items = {}, + exits = {}, + nMonsters = 0, + } + tMap[x][y][z] = room + + if y == 0 then + -- Room is above ground + + -- Pick biome + room.nBiome = math.random(1, #tBiomes) + room.trees = hasTrees(room.nBiome) + + -- Add animals + if math.random(1, 3) == 1 then + for _ = 1, math.random(1, 2) do + local sAnimal = tAnimals[math.random(1, #tAnimals)] + room.items[sAnimal] = items[sAnimal] + end + end + + -- Add surface ore + if math.random(1, 5) == 1 or hasStone(room.nBiome) then + room.items["some stone"] = items["some stone"] + end + if math.random(1, 8) == 1 then + room.items["some coal"] = items["some coal"] + end + if math.random(1, 8) == 1 and hasRivers(room.nBiome) then + room.items["a river"] = items["a river"] + end + + -- Add exits + room.exits = { + ["north"] = true, + ["south"] = true, + ["east"] = true, + ["west"] = true, + } + if math.random(1, 8) == 1 then + room.exits.down = true + room.items["a cave entrance"] = items["a cave entrance"] + end + + else + -- Room is underground + -- Add exits + local function tryExit(sDir, sOpp, x, y, z) + local adj = getRoom(x, y, z, true) + if adj then + if adj.exits[sOpp] then + room.exits[sDir] = true + end + else + if math.random(1, 3) == 1 then + room.exits[sDir] = true + end + end + end + + if y == -1 then + local above = getRoom(x, y + 1, z) + if above.exits.down then + room.exits.up = true + room.items["an exit to the surface"] = items["an exit to the surface"] + end + else + tryExit("up", "down", x, y + 1, z) + end + + if y > -3 then + tryExit("down", "up", x, y - 1, z) + end + + tryExit("east", "west", x - 1, y, z) + tryExit("west", "east", x + 1, y, z) + tryExit("north", "south", x, y, z + 1) + tryExit("south", "north", x, y, z - 1) + + -- Add ores + room.items["some stone"] = items["some stone"] + if math.random(1, 3) == 1 then + room.items["some coal"] = items["some coal"] + end + if math.random(1, 8) == 1 then + room.items["some iron"] = items["some iron"] + end + if y == -3 and math.random(1, 15) == 1 then + room.items["some diamond"] = items["some diamond"] + end + + -- Turn out the lights + room.dark = true + end + end + return tMap[x][y][z] +end + +local function itemize(t) + local item = next(t) + if item == nil then + return "nothing" + end + + local text = "" + while item do + text = text .. item + + local nextItem = next(t, item) + if nextItem ~= nil then + local nextNextItem = next(t, nextItem) + if nextNextItem == nil then + text = text .. " and " + else + text = text .. ", " + end + end + item = nextItem + end + return text +end + +local function findItem(_tList, _sQuery) + for sItem, tItem in pairs(_tList) do + if sItem == _sQuery then + return sItem + end + if tItem.aliases ~= nil then + for _, sAlias in pairs(tItem.aliases) do + if sAlias == _sQuery then + return sItem + end + end + end + end + return nil +end + +local tMatches = { + ["wait"] = { + "wait", + }, + ["look"] = { + "look at the ([%a ]+)", + "look at ([%a ]+)", + "look", + "inspect ([%a ]+)", + "inspect the ([%a ]+)", + "inspect", + }, + ["inventory"] = { + "check self", + "check inventory", + "inventory", + "i", + }, + ["go"] = { + "go (%a+)", + "travel (%a+)", + "walk (%a+)", + "run (%a+)", + "go", + }, + ["dig"] = { + "dig (%a+) using ([%a ]+)", + "dig (%a+) with ([%a ]+)", + "dig (%a+)", + "dig", + }, + ["take"] = { + "pick up the ([%a ]+)", + "pick up ([%a ]+)", + "pickup ([%a ]+)", + "take the ([%a ]+)", + "take ([%a ]+)", + "take", + }, + ["drop"] = { + "put down the ([%a ]+)", + "put down ([%a ]+)", + "drop the ([%a ]+)", + "drop ([%a ]+)", + "drop", + }, + ["place"] = { + "place the ([%a ]+)", + "place ([%a ]+)", + "place", + }, + ["cbreak"] = { + "punch the ([%a ]+)", + "punch ([%a ]+)", + "punch", + "break the ([%a ]+) with the ([%a ]+)", + "break ([%a ]+) with ([%a ]+) ", + "break the ([%a ]+)", + "break ([%a ]+)", + "break", + }, + ["mine"] = { + "mine the ([%a ]+) with the ([%a ]+)", + "mine ([%a ]+) with ([%a ]+)", + "mine ([%a ]+)", + "mine", + }, + ["attack"] = { + "attack the ([%a ]+) with the ([%a ]+)", + "attack ([%a ]+) with ([%a ]+)", + "attack ([%a ]+)", + "attack", + "kill the ([%a ]+) with the ([%a ]+)", + "kill ([%a ]+) with ([%a ]+)", + "kill ([%a ]+)", + "kill", + "hit the ([%a ]+) with the ([%a ]+)", + "hit ([%a ]+) with ([%a ]+)", + "hit ([%a ]+)", + "hit", + }, + ["craft"] = { + "craft a ([%a ]+)", + "craft some ([%a ]+)", + "craft ([%a ]+)", + "craft", + "make a ([%a ]+)", + "make some ([%a ]+)", + "make ([%a ]+)", + "make", + }, + ["build"] = { + "build ([%a ]+) out of ([%a ]+)", + "build ([%a ]+) from ([%a ]+)", + "build ([%a ]+)", + "build", + }, + ["eat"] = { + "eat a ([%a ]+)", + "eat the ([%a ]+)", + "eat ([%a ]+)", + "eat", + }, + ["help"] = { + "help me", + "help", + }, + ["exit"] = { + "exit", + "quit", + "goodbye", + "good bye", + "bye", + "farewell", + }, +} + +local commands = {} +local function doCommand(text) + if text == "" then + commands.noinput() + return + end + + for sCommand, t in pairs(tMatches) do + for _, sMatch in pairs(t) do + local tCaptures = { string.match(text, "^" .. sMatch .. "$") } + if #tCaptures ~= 0 then + local fnCommand = commands[sCommand] + if #tCaptures == 1 and tCaptures[1] == sMatch then + fnCommand() + else + fnCommand(table.unpack(tCaptures)) + end + return + end + end + end + commands.badinput() +end + +function commands.wait() + print("Time passes...") +end + +function commands.look(_sTarget) + local room = getRoom(x, y, z) + if room.dark then + print("It is pitch dark.") + return + end + + if _sTarget == nil then + -- Look at the world + if y == 0 then + io.write("You are standing " .. tBiomes[room.nBiome] .. ". ") + print(tDayCycle[getTimeOfDay()]) + else + io.write("You are underground. ") + if next(room.exits) ~= nil then + print("You can travel " .. itemize(room.exits) .. ".") + else + print() + end + end + if next(room.items) ~= nil then + print("There is " .. itemize(room.items) .. " here.") + end + if room.trees then + print("There are trees here.") + end + + else + -- Look at stuff + if room.trees and (_sTarget == "tree" or _sTarget == "trees") then + print("The trees look easy to break.") + elseif _sTarget == "self" or _sTarget == "myself" then + print("Very handsome.") + else + local tItem = nil + local sItem = findItem(room.items, _sTarget) + if sItem then + tItem = room.items[sItem] + else + sItem = findItem(inventory, _sTarget) + if sItem then + tItem = inventory[sItem] + end + end + + if tItem then + print(tItem.desc or "You see nothing special about " .. sItem .. ".") + else + print("You don't see any " .. _sTarget .. " here.") + end + end + end +end + +function commands.go(_sDir) + local room = getRoom(x, y, z) + if _sDir == nil then + print("Go where?") + return + end + + if nGoWest ~= nil then + if _sDir == "west" then + nGoWest = nGoWest + 1 + if nGoWest > #tGoWest then + nGoWest = 1 + end + print(tGoWest[nGoWest]) + else + if nGoWest > 0 or nTurn > 6 then + nGoWest = nil + end + end + end + + if room.exits[_sDir] == nil then + print("You can't go that way.") + return + end + + if _sDir == "north" then + z = z + 1 + elseif _sDir == "south" then + z = z - 1 + elseif _sDir == "east" then + x = x - 1 + elseif _sDir == "west" then + x = x + 1 + elseif _sDir == "up" then + y = y + 1 + elseif _sDir == "down" then + y = y - 1 + else + print("I don't understand that direction.") + return + end + + nTimeInRoom = 0 + doCommand("look") +end + +function commands.dig(_sDir, _sTool) + local room = getRoom(x, y, z) + if _sDir == nil then + print("Dig where?") + return + end + + local sTool = nil + local tTool = nil + if _sTool ~= nil then + sTool = findItem(inventory, _sTool) + if not sTool then + print("You're not carrying a " .. _sTool .. ".") + return + end + tTool = inventory[sTool] + end + + local bActuallyDigging = room.exits[_sDir] ~= true + if bActuallyDigging then + if sTool == nil or tTool.toolType ~= "pick" then + print("You need to use a pickaxe to dig through stone.") + return + end + end + + if _sDir == "north" then + room.exits.north = true + z = z + 1 + getRoom(x, y, z).exits.south = true + + elseif _sDir == "south" then + room.exits.south = true + z = z - 1 + getRoom(x, y, z).exits.north = true + + elseif _sDir == "east" then + room.exits.east = true + x = x - 1 + getRoom(x, y, z).exits.west = true + + elseif _sDir == "west" then + room.exits.west = true + x = x + 1 + getRoom(x, y, z).exits.east = true + + elseif _sDir == "up" then + if y == 0 then + print("You can't dig that way.") + return + end + + room.exits.up = true + if y == -1 then + room.items["an exit to the surface"] = items["an exit to the surface"] + end + y = y + 1 + + room = getRoom(x, y, z) + room.exits.down = true + if y == 0 then + room.items["a cave entrance"] = items["a cave entrance"] + end + + elseif _sDir == "down" then + if y <= -3 then + print("You hit bedrock.") + return + end + + room.exits.down = true + if y == 0 then + room.items["a cave entrance"] = items["a cave entrance"] + end + y = y - 1 + + room = getRoom(x, y, z) + room.exits.up = true + if y == -1 then + room.items["an exit to the surface"] = items["an exit to the surface"] + end + + else + print("I don't understand that direction.") + return + end + + -- + if bActuallyDigging then + if _sDir == "down" and y == -1 or + _sDir == "up" and y == 0 then + inventory["some dirt"] = items["some dirt"] + inventory["some stone"] = items["some stone"] + print("You dig " .. _sDir .. " using " .. sTool .. " and collect some dirt and stone.") + else + inventory["some stone"] = items["some stone"] + print("You dig " .. _sDir .. " using " .. sTool .. " and collect some stone.") + end + end + + nTimeInRoom = 0 + doCommand("look") +end + +function commands.inventory() + print("You are carrying " .. itemize(inventory) .. ".") +end + +function commands.drop(_sItem) + if _sItem == nil then + print("Drop what?") + return + end + + local room = getRoom(x, y, z) + local sItem = findItem(inventory, _sItem) + if sItem then + local tItem = inventory[sItem] + if tItem.droppable == false then + print("You can't drop that.") + else + room.items[sItem] = tItem + inventory[sItem] = nil + print("Dropped.") + end + else + print("You don't have a " .. _sItem .. ".") + end +end + +function commands.place(_sItem) + if _sItem == nil then + print("Place what?") + return + end + + if _sItem == "torch" or _sItem == "a torch" then + local room = getRoom(x, y, z) + if inventory["some torches"] or inventory["a torch"] then + inventory["a torch"] = nil + room.items["a torch"] = items["a torch"] + if room.dark then + print("The cave lights up under the torchflame.") + room.dark = false + elseif y == 0 and not isSunny() then + print("The night gets a little brighter.") + else + print("Placed.") + end + else + print("You don't have torches.") + end + return + end + + commands.drop(_sItem) +end + +function commands.take(_sItem) + if _sItem == nil then + print("Take what?") + return + end + + local room = getRoom(x, y, z) + local sItem = findItem(room.items, _sItem) + if sItem then + local tItem = room.items[sItem] + if tItem.heavy == true then + print("You can't carry " .. sItem .. ".") + elseif tItem.ore == true then + print("You need to mine this ore.") + else + if tItem.infinite ~= true then + room.items[sItem] = nil + end + inventory[sItem] = tItem + + if inventory["some torches"] and inventory["a torch"] then + inventory["a torch"] = nil + end + if sItem == "a torch" and y < 0 then + room.dark = true + print("The cave plunges into darkness.") + else + print("Taken.") + end + end + else + print("You don't see a " .. _sItem .. " here.") + end +end + +function commands.mine(_sItem, _sTool) + if _sItem == nil then + print("Mine what?") + return + end + if _sTool == nil then + print("Mine " .. _sItem .. " with what?") + return + end + commands.cbreak(_sItem, _sTool) +end + +function commands.attack(_sItem, _sTool) + if _sItem == nil then + print("Attack what?") + return + end + commands.cbreak(_sItem, _sTool) +end + +function commands.cbreak(_sItem, _sTool) + if _sItem == nil then + print("Break what?") + return + end + + local sTool = nil + if _sTool ~= nil then + sTool = findItem(inventory, _sTool) + if sTool == nil then + print("You're not carrying a " .. _sTool .. ".") + return + end + end + + local room = getRoom(x, y, z) + if _sItem == "tree" or _sItem == "trees" or _sItem == "a tree" then + print("The tree breaks into blocks of wood, which you pick up.") + inventory["some wood"] = items["some wood"] + return + elseif _sItem == "self" or _sItem == "myself" then + if term.isColour() then + term.setTextColour(colours.red) + end + print("You have died.") + print("Score: &e0") + term.setTextColour(colours.white) + bRunning = false + return + end + + local sItem = findItem(room.items, _sItem) + if sItem then + local tItem = room.items[sItem] + if tItem.ore == true then + -- Breaking ore + if not sTool then + print("You need a tool to break this ore.") + return + end + local tTool = inventory[sTool] + if tTool.tool then + if tTool.toolLevel < tItem.toolLevel then + print(sTool .. " is not strong enough to break this ore.") + elseif tTool.toolType ~= tItem.toolType then + print("You need a different kind of tool to break this ore.") + else + print("The ore breaks, dropping " .. sItem .. ", which you pick up.") + inventory[sItem] = items[sItem] + if tItem.infinite ~= true then + room.items[sItem] = nil + end + end + else + print("You can't break " .. sItem .. " with " .. sTool .. ".") + end + + elseif tItem.creature == true then + -- Fighting monsters (or pigs) + local toolLevel = 0 + local tTool = nil + if sTool then + tTool = inventory[sTool] + if tTool.toolType == "sword" then + toolLevel = tTool.toolLevel + end + end + + local tChances = { 0.2, 0.4, 0.55, 0.8, 1 } + if math.random() <= tChances[toolLevel + 1] then + room.items[sItem] = nil + print("The " .. tItem.aliases[1] .. " dies.") + + if tItem.drops then + for _, sDrop in pairs(tItem.drops) do + if not room.items[sDrop] then + print("The " .. tItem.aliases[1] .. " dropped " .. sDrop .. ".") + room.items[sDrop] = items[sDrop] + end + end + end + + if tItem.monster then + room.nMonsters = room.nMonsters - 1 + end + else + print("The " .. tItem.aliases[1] .. " is injured by your blow.") + end + + if tItem.hitDrops then + for _, sDrop in pairs(tItem.hitDrops) do + if not room.items[sDrop] then + print("The " .. tItem.aliases[1] .. " dropped " .. sDrop .. ".") + room.items[sDrop] = items[sDrop] + end + end + end + + else + print("You can't break " .. sItem .. ".") + end + else + print("You don't see a " .. _sItem .. " here.") + end +end + +function commands.craft(_sItem) + if _sItem == nil then + print("Craft what?") + return + end + + if _sItem == "computer" or _sItem == "a computer" then + print("By creating a computer in a computer in a computer, you tear a hole in the spacetime continuum from which no mortal being can escape.") + if term.isColour() then + term.setTextColour(colours.red) + end + print("You have died.") + print("Score: &e0") + term.setTextColour(colours.white) + bRunning = false + return + end + + local sItem = findItem(items, _sItem) + local tRecipe = sItem and tRecipes[sItem] or nil + if tRecipe then + for _, sReq in ipairs(tRecipe) do + if inventory[sReq] == nil then + print("You don't have the items you need to craft " .. sItem .. ".") + return + end + end + + for _, sReq in ipairs(tRecipe) do + inventory[sReq] = nil + end + inventory[sItem] = items[sItem] + if inventory["some torches"] and inventory["a torch"] then + inventory["a torch"] = nil + end + print("Crafted.") + else + print("You don't know how to make " .. (sItem or _sItem) .. ".") + end +end + +function commands.build(_sThing, _sMaterial) + if _sThing == nil then + print("Build what?") + return + end + + local sMaterial = nil + if _sMaterial == nil then + for sItem, tItem in pairs(inventory) do + if tItem.material then + sMaterial = sItem + break + end + end + if sMaterial == nil then + print("You don't have any building materials.") + return + end + else + sMaterial = findItem(inventory, _sMaterial) + if not sMaterial then + print("You don't have any " .. _sMaterial) + return + end + + if inventory[sMaterial].material ~= true then + print(sMaterial .. " is not a good building material.") + return + end + end + + local alias = nil + if string.sub(_sThing, 1, 1) == "a" then + alias = string.match(_sThing, "a ([%a ]+)") + end + + local room = getRoom(x, y, z) + inventory[sMaterial] = nil + room.items[_sThing] = { + heavy = true, + aliases = { alias }, + desc = "As you look at your creation (made from " .. sMaterial .. "), you feel a swelling sense of pride.", + } + + print("Your construction is complete.") +end + +function commands.help() + local sText = + "Welcome to adventure, the greatest text adventure game on CraftOS. " .. + "To get around the world, type actions, and the adventure will " .. + "be read back to you. The actions available to you are go, look, inspect, inventory, " .. + "take, drop, place, punch, attack, mine, dig, craft, build, eat and exit." + print(sText) +end + +function commands.eat(_sItem) + if _sItem == nil then + print("Eat what?") + return + end + + local sItem = findItem(inventory, _sItem) + if not sItem then + print("You don't have any " .. _sItem .. ".") + return + end + + local tItem = inventory[sItem] + if tItem.food then + print("That was delicious!") + inventory[sItem] = nil + + if bInjured then + print("You are no longer injured.") + bInjured = false + end + else + print("You can't eat " .. sItem .. ".") + end +end + +function commands.exit() + bRunning = false +end + +function commands.badinput() + local tResponses = { + "I don't understand.", + "I don't understand you.", + "You can't do that.", + "Nope.", + "Huh?", + "Say again?", + "That's crazy talk.", + "Speak clearly.", + "I'll think about it.", + "Let me get back to you on that one.", + "That doesn't make any sense.", + "What?", + } + print(tResponses[math.random(1, #tResponses)]) +end + +function commands.noinput() + local tResponses = { + "Speak up.", + "Enunciate.", + "Project your voice.", + "Don't be shy.", + "Use your words.", + } + print(tResponses[math.random(1, #tResponses)]) +end + +local function simulate() + local bNewMonstersThisRoom = false + + -- Spawn monsters in nearby rooms + for sx = -2, 2 do + for sy = -1, 1 do + for sz = -2, 2 do + local h = y + sy + if h >= -3 and h <= 0 then + local room = getRoom(x + sx, h, z + sz) + + -- Spawn monsters + if room.nMonsters < 2 and + (h == 0 and not isSunny() and not room.items["a torch"] or room.dark) and + math.random(1, 6) == 1 then + + local sMonster = tMonsters[math.random(1, #tMonsters)] + if room.items[sMonster] == nil then + room.items[sMonster] = items[sMonster] + room.nMonsters = room.nMonsters + 1 + + if sx == 0 and sy == 0 and sz == 0 and not room.dark then + print("From the shadows, " .. sMonster .. " appears.") + bNewMonstersThisRoom = true + end + end + end + + -- Burn monsters + if h == 0 and isSunny() then + for _, sMonster in ipairs(tMonsters) do + if room.items[sMonster] and items[sMonster].nocturnal then + room.items[sMonster] = nil + if sx == 0 and sy == 0 and sz == 0 and not room.dark then + print("With the sun high in the sky, the " .. items[sMonster].aliases[1] .. " bursts into flame and dies.") + end + room.nMonsters = room.nMonsters - 1 + end + end + end + end + end + end + end + + -- Make monsters attack + local room = getRoom(x, y, z) + if nTimeInRoom >= 2 and not bNewMonstersThisRoom then + for _, sMonster in ipairs(tMonsters) do + if room.items[sMonster] then + if math.random(1, 4) == 1 and + not (y == 0 and isSunny() and sMonster == "a spider") then + if sMonster == "a creeper" then + if room.dark then + print("A creeper explodes.") + else + print("The creeper explodes.") + end + room.items[sMonster] = nil + room.nMonsters = room.nMonsters - 1 + else + if room.dark then + print("A " .. items[sMonster].aliases[1] .. " attacks you.") + else + print("The " .. items[sMonster].aliases[1] .. " attacks you.") + end + end + + if bInjured then + if term.isColour() then + term.setTextColour(colours.red) + end + print("You have died.") + print("Score: &e0") + term.setTextColour(colours.white) + bRunning = false + return + else + bInjured = true + end + + break + end + end + end + end + + -- Always print this + if bInjured then + if term.isColour() then + term.setTextColour(colours.red) + end + print("You are injured.") + term.setTextColour(colours.white) + end + + -- Advance time + nTurn = nTurn + 1 + nTimeInRoom = nTimeInRoom + 1 +end + +doCommand("look") +simulate() + +local tCommandHistory = {} +while bRunning do + if term.isColour() then + term.setTextColour(colours.yellow) + end + write("? ") + term.setTextColour(colours.white) + + local sRawLine = read(nil, tCommandHistory) + table.insert(tCommandHistory, sRawLine) + + local sLine = nil + for match in string.gmatch(sRawLine, "%a+") do + if sLine then + sLine = sLine .. " " .. string.lower(match) + else + sLine = string.lower(match) + end + end + + doCommand(sLine or "") + if bRunning then + simulate() + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/dj.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/dj.lua new file mode 100644 index 000000000..dce6d9f41 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/dj.lua @@ -0,0 +1,55 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName .. " play") + print(programName .. " play ") + print(programName .. " stop") +end + +if #tArgs > 2 then + printUsage() + return +end + +local sCommand = tArgs[1] +if sCommand == "stop" then + -- Stop audio + disk.stopAudio() + +elseif sCommand == "play" or sCommand == nil then + -- Play audio + local sName = tArgs[2] + if sName == nil then + -- No disc specified, pick one at random + local tNames = {} + for _, sName in ipairs(peripheral.getNames()) do + if disk.isPresent(sName) and disk.hasAudio(sName) then + table.insert(tNames, sName) + end + end + if #tNames == 0 then + print("No Music Discs in attached disk drives") + return + end + sName = tNames[math.random(1, #tNames)] + end + + -- Play the disc + if disk.isPresent(sName) and disk.hasAudio(sName) then + print("Playing " .. disk.getAudioTitle(sName)) + disk.playAudio(sName) + else + print("No Music Disc in disk drive: " .. sName) + return + end + +else + printUsage() + +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/hello.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/hello.lua new file mode 100644 index 000000000..c909db669 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/hello.lua @@ -0,0 +1,9 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if term.isColour() then + term.setTextColour(2 ^ math.random(0, 15)) +end +textutils.slowPrint("Hello World!") +term.setTextColour(colours.white) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/speaker.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/speaker.lua new file mode 100644 index 000000000..de6b03e64 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/speaker.lua @@ -0,0 +1,136 @@ +-- SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +local function get_speakers(name) + if name then + local speaker = peripheral.wrap(name) + if speaker == nil then + error(("Speaker %q does not exist"):format(name), 0) + return + elseif not peripheral.hasType(name, "speaker") then + error(("%q is not a speaker"):format(name), 0) + end + + return { speaker } + else + local speakers = { peripheral.find("speaker") } + if #speakers == 0 then + error("No speakers attached", 0) + end + return speakers + end +end + +local function pcm_decoder(chunk) + local buffer = {} + for i = 1, #chunk do + buffer[i] = chunk:byte(i) - 128 + end + return buffer +end + +local function report_invalid_format(format) + printError(("speaker cannot play %s files."):format(format)) + local pp = require "cc.pretty" + pp.print("Run '" .. pp.text("help speaker", colours.lightGrey) .. "' for information on supported formats.") +end + + +local cmd = ... +if cmd == "stop" then + local _, name = ... + for _, speaker in pairs(get_speakers(name)) do speaker.stop() end +elseif cmd == "play" then + local _, file, name = ... + local speaker = get_speakers(name)[1] + + local handle, err + if http and file:match("^https?://") then + print("Downloading...") + handle, err = http.get{ url = file, binary = true } + else + handle, err = fs.open(file, "rb") + end + + if not handle then + printError("Could not play audio:") + error(err, 0) + end + + local start = handle.read(4) + local pcm = false + local size = 16 * 1024 - 4 + if start == "RIFF" then + handle.read(4) + if handle.read(8) ~= "WAVEfmt " then + handle.close() + error("Could not play audio: Unsupported WAV file", 0) + end + + local fmtsize = (" [speaker]") + print(programName .. " stop [speaker]") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/fun/worm.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/worm.lua new file mode 100644 index 000000000..17561b17c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/fun/worm.lua @@ -0,0 +1,285 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +-- Display the start screen +local w, h = term.getSize() + +local headingColour, textColour, wormColour, fruitColour +if term.isColour() then + headingColour = colours.yellow + textColour = colours.white + wormColour = colours.green + fruitColour = colours.red +else + headingColour = colours.white + textColour = colours.white + wormColour = colours.white + fruitColour = colours.white +end + +local function printCentred(y, s) + local x = math.floor((w - #s) / 2) + term.setCursorPos(x, y) + --term.clearLine() + term.write(s) +end + +local xVel, yVel = 1, 0 +local xPos, yPos = math.floor(w / 2), math.floor(h / 2) +local pxVel, pyVel = nil, nil +local nExtraLength = 6 +local bRunning = true + +local tailX, tailY = xPos, yPos +local nScore = 0 +local nDifficulty = 2 +local nSpeed, nInterval + +-- Setup the screen +local screen = {} +for x = 1, w do + screen[x] = {} + for y = 1, h do + screen[x][y] = {} + end +end +screen[xPos][yPos] = { snake = true } + +local nFruit = 1 +local tFruits = { + "A", "B", "C", "D", "E", "F", "G", "H", + "I", "J", "K", "L", "M", "N", "O", "P", + "Q", "R", "S", "T", "U", "V", "W", "X", + "Y", "Z", + "a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", + "y", "z", + "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", + "@", "$", "%", "#", "&", "!", "?", "+", "*", "~", +} + +local function addFruit() + while true do + local x = math.random(1, w) + local y = math.random(2, h) + local fruit = screen[x][y] + if fruit.snake == nil and fruit.wall == nil and fruit.fruit == nil then + screen[x][y] = { fruit = true } + term.setCursorPos(x, y) + term.setBackgroundColour(fruitColour) + term.write(" ") + term.setBackgroundColour(colours.black) + break + end + end + + nFruit = nFruit + 1 + if nFruit > #tFruits then + nFruit = 1 + end +end + +local function drawMenu() + term.setTextColour(headingColour) + term.setCursorPos(1, 1) + term.write("SCORE ") + + term.setTextColour(textColour) + term.setCursorPos(7, 1) + term.write(tostring(nScore)) + + term.setTextColour(headingColour) + term.setCursorPos(w - 11, 1) + term.write("DIFFICULTY ") + + term.setTextColour(textColour) + term.setCursorPos(w, 1) + term.write(tostring(nDifficulty or "?")) + + term.setTextColour(colours.white) +end + +local function update( ) + if pxVel and pyVel then + xVel, yVel = pxVel, pyVel + pxVel, pyVel = nil, nil + end + + -- Remove the tail + if nExtraLength == 0 then + local tail = screen[tailX][tailY] + screen[tailX][tailY] = {} + term.setCursorPos(tailX, tailY) + term.write(" ") + tailX = tail.nextX + tailY = tail.nextY + else + nExtraLength = nExtraLength - 1 + end + + -- Update the head + local head = screen[xPos][yPos] + local newXPos = xPos + xVel + local newYPos = yPos + yVel + if newXPos < 1 then + newXPos = w + elseif newXPos > w then + newXPos = 1 + end + if newYPos < 2 then + newYPos = h + elseif newYPos > h then + newYPos = 2 + end + + local newHead = screen[newXPos][newYPos] + if newHead.snake == true or newHead.wall == true then + bRunning = false + + else + if newHead.fruit == true then + nScore = nScore + 10 + nExtraLength = nExtraLength + 1 + addFruit() + end + xPos = newXPos + yPos = newYPos + head.nextX = newXPos + head.nextY = newYPos + screen[newXPos][newYPos] = { snake = true } + + end + + term.setCursorPos(xPos, yPos) + term.setBackgroundColour(wormColour) + term.write(" ") + term.setBackgroundColour(colours.black) + + drawMenu() +end + +-- Display the frontend +term.clear() +local function drawFrontend() + --term.setTextColour( titleColour ) + --printCentred( math.floor(h/2) - 4, " W O R M " ) + + term.setTextColour(headingColour) + printCentred(math.floor(h / 2) - 3, "") + printCentred(math.floor(h / 2) - 2, " SELECT DIFFICULTY ") + printCentred(math.floor(h / 2) - 1, "") + + printCentred(math.floor(h / 2) + 0, " ") + printCentred(math.floor(h / 2) + 1, " ") + printCentred(math.floor(h / 2) + 2, " ") + printCentred(math.floor(h / 2) - 1 + nDifficulty, " [ ] ") + + term.setTextColour(textColour) + printCentred(math.floor(h / 2) + 0, "EASY") + printCentred(math.floor(h / 2) + 1, "MEDIUM") + printCentred(math.floor(h / 2) + 2, "HARD") + printCentred(math.floor(h / 2) + 3, "") + + term.setTextColour(colours.white) +end + +drawMenu() +drawFrontend() +while true do + local _, key = os.pullEvent("key") + if key == keys.up or key == keys.w then + -- Up + if nDifficulty > 1 then + nDifficulty = nDifficulty - 1 + drawMenu() + drawFrontend() + end + elseif key == keys.down or key == keys.s then + -- Down + if nDifficulty < 3 then + nDifficulty = nDifficulty + 1 + drawMenu() + drawFrontend() + end + elseif key == keys.enter or key == keys.numPadEnter then + -- Enter/Numpad Enter + break + end +end + +local tSpeeds = { 5, 10, 25 } +nSpeed = tSpeeds[nDifficulty] +nInterval = 1 / nSpeed + +-- Grow the snake to its intended size +term.clear() +drawMenu() +screen[tailX][tailY].snake = true +while nExtraLength > 0 do + update() +end +addFruit() +addFruit() + +-- Play the game +local timer = os.startTimer(0) +while bRunning do + local event, p1 = os.pullEvent() + if event == "timer" and p1 == timer then + timer = os.startTimer(nInterval) + update(false) + + elseif event == "key" then + local key = p1 + if key == keys.up or key == keys.w then + -- Up + if yVel == 0 then + pxVel, pyVel = 0, -1 + end + elseif key == keys.down or key == keys.s then + -- Down + if yVel == 0 then + pxVel, pyVel = 0, 1 + end + elseif key == keys.left or key == keys.a then + -- Left + if xVel == 0 then + pxVel, pyVel = -1, 0 + end + + elseif key == keys.right or key == keys.d then + -- Right + if xVel == 0 then + pxVel, pyVel = 1, 0 + end + + end + end +end + +-- Display the gameover screen +term.setTextColour(headingColour) +printCentred(math.floor(h / 2) - 2, " ") +printCentred(math.floor(h / 2) - 1, " G A M E O V E R ") + +term.setTextColour(textColour) +printCentred(math.floor(h / 2) + 0, " ") +printCentred(math.floor(h / 2) + 1, " FINAL SCORE " .. nScore .. " ") +printCentred(math.floor(h / 2) + 2, " ") +term.setTextColour(colours.white) + +local timer = os.startTimer(2.5) +repeat + local e, p = os.pullEvent() + if e == "timer" and p == timer then + term.setTextColour(textColour) + printCentred(math.floor(h / 2) + 2, " PRESS ANY KEY ") + printCentred(math.floor(h / 2) + 3, " ") + term.setTextColour(colours.white) + end +until e == "char" + +term.clear() +term.setCursorPos(1, 1) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/gps.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/gps.lua new file mode 100644 index 000000000..3a65fd8ae --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/gps.lua @@ -0,0 +1,98 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName .. " host") + print(programName .. " host ") + print(programName .. " locate") +end + +local tArgs = { ... } +if #tArgs < 1 then + printUsage() + return +end + + local sCommand = tArgs[1] +if sCommand == "locate" then + -- "gps locate" + -- Just locate this computer (this will print the results) + gps.locate(2, true) + +elseif sCommand == "host" then + -- "gps host" + -- Act as a GPS host + if pocket then + print("GPS Hosts must be stationary") + return + 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 + print("No wireless modems found. 1 required.") + return + end + + -- Determine position + local x, y, z + if #tArgs >= 4 then + -- Position is manually specified + x = tonumber(tArgs[2]) + y = tonumber(tArgs[3]) + z = tonumber(tArgs[4]) + if x == nil or y == nil or z == nil then + printUsage() + return + end + print("Position is " .. x .. "," .. y .. "," .. z) + else + -- Position is to be determined using locate + x, y, z = gps.locate(2, true) + if x == nil then + print("Run \"gps host \" to set position manually") + return + end + end + + -- Open a channel + local modem = peripheral.wrap(sModemSide) + print("Opening channel on modem " .. sModemSide) + modem.open(gps.CHANNEL_GPS) + + -- Serve requests indefinitely + local nServed = 0 + while true do + local e, p1, p2, p3, p4, p5 = os.pullEvent("modem_message") + if e == "modem_message" then + -- We received a message from a modem + local sSide, sChannel, sReplyChannel, sMessage, nDistance = p1, p2, p3, p4, p5 + if sSide == sModemSide and sChannel == gps.CHANNEL_GPS and sMessage == "PING" and nDistance then + -- We received a ping message on the GPS channel, send a response + modem.transmit(sReplyChannel, gps.CHANNEL_GPS, { x, y, z }) + + -- Print the number of requests handled + nServed = nServed + 1 + if nServed > 1 then + local _, y = term.getCursorPos() + term.setCursorPos(1, y - 1) + end + print(nServed .. " GPS requests served") + end + end + end +else + -- "gps somethingelse" + -- Error + printUsage() +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/help.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/help.lua new file mode 100644 index 000000000..3ae6ac7f7 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/help.lua @@ -0,0 +1,290 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +local sTopic +if #tArgs > 0 then + sTopic = tArgs[1] +else + sTopic = "intro" +end + +if sTopic == "index" then + print("Help topics available:") + local tTopics = help.topics() + textutils.pagedTabulate(tTopics) + return +end + +local strings = require "cc.strings" + +local function min_of(a, b, default) + if not a and not b then return default end + if not a then return b end + if not b then return a end + return math.min(a, b) +end + +--[[- Parse a markdown string, extracting headings and highlighting some basic +constructs. + +The implementation of this is horrible. SquidDev shouldn't be allowed to write +parsers, especially ones they think might be "performance critical". +]] +local function parse_markdown(text) + local len = #text + local oob = len + 1 + + -- Some patterns to match headers and bullets on the start of lines. + -- The `%f[^\n\0]` is some wonderful logic to match the start of a line /or/ + -- the start of the document. + local heading = "%f[^\n\0](#+ +)([^\n]*)" + local bullet = "%f[^\n\0]( *)[.*]( +)" + local code = "`([^`]+)`" + + local new_text, fg, bg = "", "", "" + local function append(txt, fore, back) + new_text = new_text .. txt + fg = fg .. (fore or "0"):rep(#txt) + bg = bg .. (back or "f"):rep(#txt) + end + + local next_header = text:find(heading) + local next_bullet = text:find(bullet) + local next_block = min_of(next_header, next_bullet, oob) + + local next_code, next_code_end = text:find(code) + + local sections = {} + + local start = 1 + while start <= len do + if start == next_block then + if start == next_header then + local _, fin, head, content = text:find(heading, start) + sections[#new_text + 1] = content + append(head .. content, "4", "f") + start = fin + 1 + + next_header = text:find(heading, start) + else + local _, fin, space, content = text:find(bullet, start) + append(space .. "\7" .. content) + start = fin + 1 + + next_bullet = text:find(bullet, start) + end + + next_block = min_of(next_header, next_bullet, oob) + elseif next_code and next_code_end < next_block then + -- Basic inline code blocks + if start < next_code then append(text:sub(start, next_code - 1)) end + local content = text:match(code, next_code) + append(content, "0", "7") + + start = next_code_end + 1 + next_code, next_code_end = text:find(code, start) + else + -- Normal text + append(text:sub(start, next_block - 1)) + start = next_block + + -- Rescan for a new code block + if next_code then next_code, next_code_end = text:find(code, start) end + end + end + + return new_text, fg, bg, sections +end + +local function word_wrap_basic(text, width) + local lines, fg, bg = strings.wrap(text, width), {}, {} + local fg_line, bg_line = ("0"):rep(width), ("f"):rep(width) + + -- Normalise the strings suitable for use with blit. We could skip this and + -- just use term.write, but saves us a clearLine call. + for k, line in pairs(lines) do + lines[k] = strings.ensure_width(line, width) + fg[k] = fg_line + bg[k] = bg_line + end + + return lines, fg, bg, {} +end + +local function word_wrap_markdown(text, width) + -- Add in styling for Markdown-formatted text. + local text, fg, bg, sections = parse_markdown(text) + + local lines = strings.wrap(text, width) + local fglines, bglines, section_list, section_n = {}, {}, {}, 1 + + -- Normalise the strings suitable for use with blit. We could skip this and + -- just use term.write, but saves us a clearLine call. + local start = 1 + for k, line in pairs(lines) do + -- I hate this with a burning passion, but it works! + local pos = text:find(line, start, true) + lines[k], fglines[k], bglines[k] = + strings.ensure_width(line, width), + strings.ensure_width(fg:sub(pos, pos + #line), width), + strings.ensure_width(bg:sub(pos, pos + #line), width) + + if sections[pos] then + section_list[section_n], section_n = { content = sections[pos], offset = k - 1 }, section_n + 1 + end + + start = pos + 1 + end + + return lines, fglines, bglines, section_list +end + +local sFile = help.lookup(sTopic) +local file = sFile ~= nil and io.open(sFile) or nil +if not file then + printError("No help available") + return +end + +local contents = file:read("*a") +file:close() +-- Trim trailing newlines from the file to avoid displaying a blank line. +if contents:sub(-1) == "\n" then contents:sub(1, -2) end + +local word_wrap = sFile:sub(-3) == ".md" and word_wrap_markdown or word_wrap_basic +local width, height = term.getSize() +local content_height = height - 1 -- Height of the content box. +local lines, fg, bg, sections = word_wrap(contents, width) +local print_height = #lines + +-- If we fit within the screen, just display without pagination. +if print_height <= content_height then + local _, y = term.getCursorPos() + for i = 1, print_height do + if y + i - 1 > height then + term.scroll(1) + term.setCursorPos(1, height) + else + term.setCursorPos(1, y + i - 1) + end + + term.blit(lines[i], fg[i], bg[i]) + end + return +end + +local current_section = nil +local offset = 0 + +--- Find the currently visible section, or nil if this document has no sections. +-- +-- This could potentially be a binary search, but right now it's not worth it. +local function find_section() + for i = #sections, 1, -1 do + if sections[i].offset <= offset then + return i + end + end +end + +local function draw_menu() + term.setTextColor(colors.yellow) + term.setCursorPos(1, height) + term.clearLine() + + local tag = "Help: " .. sTopic + if current_section then + tag = tag .. (" (%s)"):format(sections[current_section].content) + end + term.write(tag) + + if width >= #tag + 16 then + term.setCursorPos(width - 14, height) + term.write("Press Q to exit") + end +end + + +local function draw() + for y = 1, content_height do + term.setCursorPos(1, y) + if y + offset > print_height then + -- Should only happen if we resize the terminal to a larger one + -- than actually needed for the current text. + term.clearLine() + else + term.blit(lines[y + offset], fg[y + offset], bg[y + offset]) + end + end + + local new_section = find_section() + if new_section ~= current_section then + current_section = new_section + draw_menu() + end +end + +draw() +draw_menu() + +while true do + local event, param = os.pullEventRaw() + if event == "key" then + if param == keys.up and offset > 0 then + offset = offset - 1 + draw() + elseif param == keys.down and offset < print_height - content_height then + offset = offset + 1 + draw() + elseif param == keys.pageUp and offset > 0 then + offset = math.max(offset - content_height + 1, 0) + draw() + elseif param == keys.pageDown and offset < print_height - content_height then + offset = math.min(offset + content_height - 1, print_height - content_height) + draw() + elseif param == keys.home then + offset = 0 + draw() + elseif param == keys.left and current_section and current_section > 1 then + offset = sections[current_section - 1].offset + draw() + elseif param == keys.right and current_section and current_section < #sections then + offset = sections[current_section + 1].offset + draw() + elseif param == keys["end"] then + offset = print_height - content_height + draw() + elseif param == keys.q then + sleep(0) -- Super janky, but consumes stray "char" events. + break + end + elseif event == "mouse_scroll" then + if param < 0 and offset > 0 then + offset = offset - 1 + draw() + elseif param > 0 and offset <= print_height - content_height then + offset = offset + 1 + draw() + end + elseif event == "term_resize" then + local new_width, new_height = term.getSize() + + if new_width ~= width then + lines, fg, bg = word_wrap(contents, new_width) + print_height = #lines + end + + width, height = new_width, new_height + content_height = height - 1 + offset = math.max(math.min(offset, print_height - content_height), 0) + draw() + draw_menu() + elseif event == "terminate" then + break + end +end + +term.setCursorPos(1, 1) +term.clear() diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/http/pastebin.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/http/pastebin.lua new file mode 100644 index 000000000..763433cfe --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/http/pastebin.lua @@ -0,0 +1,164 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName .. " put ") + print(programName .. " get ") + print(programName .. " run ") +end + +local tArgs = { ... } +if #tArgs < 2 then + printUsage() + return +end + +if not http then + printError("Pastebin requires the http API, but it is not enabled") + printError("Set http.enabled to true in CC: Tweaked's server config") + return +end + +--- Attempts to guess the pastebin ID from the given code or URL +local function extractId(paste) + local patterns = { + "^([%a%d]+)$", + "^https?://pastebin.com/([%a%d]+)$", + "^pastebin.com/([%a%d]+)$", + "^https?://pastebin.com/raw/([%a%d]+)$", + "^pastebin.com/raw/([%a%d]+)$", + } + + for i = 1, #patterns do + local code = paste:match(patterns[i]) + if code then return code end + end + + return nil +end + +local function get(url) + local paste = extractId(url) + if not paste then + io.stderr:write("Invalid pastebin code.\n") + io.write("The code is the ID at the end of the pastebin.com URL.\n") + return + end + + write("Connecting to pastebin.com... ") + -- Add a cache buster so that spam protection is re-checked + local cacheBuster = ("%x"):format(math.random(0, 2 ^ 30)) + local response, err = http.get( + "https://pastebin.com/raw/" .. textutils.urlEncode(paste) .. "?cb=" .. cacheBuster + ) + + if response then + -- If spam protection is activated, we get redirected to /paste with Content-Type: text/html + local headers = response.getResponseHeaders() + if not headers["Content-Type"] or not headers["Content-Type"]:find("^text/plain") then + io.stderr:write("Failed.\n") + print("Pastebin blocked the download due to spam protection. Please complete the captcha in a web browser: https://pastebin.com/" .. textutils.urlEncode(paste)) + return + end + + print("Success.") + + local sResponse = response.readAll() + response.close() + return sResponse + else + io.stderr:write("Failed.\n") + print(err) + end +end + +local sCommand = tArgs[1] +if sCommand == "put" then + -- Upload a file to pastebin.com + -- Determine file to upload + local sFile = tArgs[2] + local sPath = shell.resolve(sFile) + if not fs.exists(sPath) or fs.isDir(sPath) then + print("No such file") + return + end + + -- Read in the file + local sName = fs.getName(sPath) + local file = fs.open(sPath, "r") + local sText = file.readAll() + file.close() + + -- POST the contents to pastebin + write("Connecting to pastebin.com... ") + local key = "0ec2eb25b6166c0c27a394ae118ad829" + local response = http.post( + "https://pastebin.com/api/api_post.php", + "api_option=paste&" .. + "api_dev_key=" .. key .. "&" .. + "api_paste_format=lua&" .. + "api_paste_name=" .. textutils.urlEncode(sName) .. "&" .. + "api_paste_code=" .. textutils.urlEncode(sText) + ) + + if response then + print("Success.") + + local sResponse = response.readAll() + response.close() + + local sCode = string.match(sResponse, "[^/]+$") + print("Uploaded as " .. sResponse) + print("Run \"pastebin get " .. sCode .. "\" to download anywhere") + + else + print("Failed.") + end + +elseif sCommand == "get" then + -- Download a file from pastebin.com + if #tArgs < 3 then + printUsage() + return + end + + -- Determine file to download + local sCode = tArgs[2] + local sFile = tArgs[3] + local sPath = shell.resolve(sFile) + if fs.exists(sPath) then + print("File already exists") + return + end + + -- GET the contents from pastebin + local res = get(sCode) + if res then + local file = fs.open(sPath, "w") + file.write(res) + file.close() + + print("Downloaded as " .. sFile) + end +elseif sCommand == "run" then + local sCode = tArgs[2] + + local res = get(sCode) + if res then + local func, err = load(res, sCode, "t", _ENV) + if not func then + printError(err) + return + end + local success, msg = pcall(func, select(3, ...)) + if not success then + printError(msg) + end + end +else + printUsage() + return +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/http/wget.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/http/wget.lua new file mode 100644 index 000000000..bd568c366 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/http/wget.lua @@ -0,0 +1,96 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage:") + print(programName .. " [filename]") + print(programName .. " run ") +end + +local tArgs = { ... } + +local run = false +if tArgs[1] == "run" then + table.remove(tArgs, 1) + run = true +end + +if #tArgs < 1 then + printUsage() + return +end + +local url = table.remove(tArgs, 1) + +if not http then + printError("wget requires the http API, but it is not enabled") + printError("Set http.enabled to true in CC: Tweaked's server config") + return +end + +local function getFilename(sUrl) + sUrl = sUrl:gsub("[#?].*" , ""):gsub("/+$" , "") + return sUrl:match("/([^/]+)$") +end + +local function get(sUrl) + -- Check if the URL is valid + local ok, err = http.checkURL(url) + if not ok then + printError(err or "Invalid URL.") + return + end + + write("Connecting to " .. sUrl .. "... ") + + local response = http.get(sUrl , nil , true) + if not response then + print("Failed.") + return nil + end + + print("Success.") + + local sResponse = response.readAll() + response.close() + return sResponse or "" +end + +if run then + local res = get(url) + if not res then return end + + local func, err = load(res, getFilename(url), "t", _ENV) + if not func then + printError(err) + return + end + + local ok, err = pcall(func, table.unpack(tArgs)) + if not ok then + printError(err) + end +else + local sFile = tArgs[1] or getFilename(url) or url + local sPath = shell.resolve(sFile) + if fs.exists(sPath) then + print("File already exists") + return + end + + local res = get(url) + if not res then return end + + local file, err = fs.open(sPath, "wb") + if not file then + printError("Cannot save file: " .. err) + return + end + + file.write(res) + file.close() + + print("Downloaded as " .. sFile) +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/id.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/id.lua new file mode 100644 index 000000000..fc70a6c57 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/id.lua @@ -0,0 +1,46 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local sDrive = nil +local tArgs = { ... } +if #tArgs > 0 then + sDrive = tostring(tArgs[1]) +end + +if sDrive == nil then + print("This is computer #" .. os.getComputerID()) + + local label = os.getComputerLabel() + if label then + print("This computer is labelled \"" .. label .. "\"") + end + +else + if disk.hasAudio(sDrive) then + local title = disk.getAudioTitle(sDrive) + if title then + print("Has audio track \"" .. title .. "\"") + else + print("Has untitled audio") + end + return + end + + if not disk.hasData(sDrive) then + print("No disk in drive " .. sDrive) + return + end + + local id = disk.getID(sDrive) + if id then + print("The disk is #" .. id) + else + print("Non-disk data source") + end + + local label = disk.getLabel(sDrive) + if label then + print("Labelled \"" .. label .. "\"") + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/import.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/import.lua new file mode 100644 index 000000000..a3b4d44ef --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/import.lua @@ -0,0 +1,26 @@ +-- SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +require "cc.completion" + +print("Drop files to transfer them to this computer") + +local files +while true do + local event, arg = os.pullEvent() + if event == "file_transfer" then + files = arg.getFiles() + break + elseif event == "key" and arg == keys.q then + return + end +end + +if #files == 0 then + printError("No files to transfer") + return +end + +local ok, err = require("cc.internal.import")(files) +if not ok and err then printError(err) end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/label.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/label.lua new file mode 100644 index 000000000..9b0df8937 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/label.lua @@ -0,0 +1,104 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName .. " get") + print(programName .. " get ") + print(programName .. " set ") + print(programName .. " set ") + print(programName .. " clear") + print(programName .. " clear ") +end + +local function checkDrive(sDrive) + if peripheral.getType(sDrive) == "drive" then + -- Check the disk exists + local bData = disk.hasData(sDrive) + if not bData then + print("No disk in " .. sDrive .. " drive") + return false + end + else + print("No disk drive named " .. sDrive) + return false + end + return true +end + +local function get(sDrive) + if sDrive ~= nil then + if checkDrive(sDrive) then + local sLabel = disk.getLabel(sDrive) + if sLabel then + print("Disk label is \"" .. sLabel .. "\"") + else + print("No Disk label") + end + end + else + local sLabel = os.getComputerLabel() + if sLabel then + print("Computer label is \"" .. sLabel .. "\"") + else + print("No Computer label") + end + end +end + +local function set(sDrive, sText) + if sDrive ~= nil then + if checkDrive(sDrive) then + disk.setLabel(sDrive, sText) + local sLabel = disk.getLabel(sDrive) + if sLabel then + print("Disk label set to \"" .. sLabel .. "\"") + else + print("Disk label cleared") + end + end + else + os.setComputerLabel(sText) + local sLabel = os.getComputerLabel() + if sLabel then + print("Computer label set to \"" .. sLabel .. "\"") + else + print("Computer label cleared") + end + end +end + +local tArgs = { ... } +local sCommand = tArgs[1] +if sCommand == "get" then + -- Get a label + if #tArgs == 1 then + get(nil) + elseif #tArgs == 2 then + get(tArgs[2]) + else + printUsage() + end +elseif sCommand == "set" then + -- Set a label + if #tArgs == 2 then + set(nil, tArgs[2]) + elseif #tArgs == 3 then + set(tArgs[2], tArgs[3]) + else + printUsage() + end +elseif sCommand == "clear" then + -- Clear a label + if #tArgs == 1 then + set(nil, nil) + elseif #tArgs == 2 then + set(tArgs[2], nil) + else + printUsage() + end +else + printUsage() +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/list.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/list.lua new file mode 100644 index 000000000..b25349e92 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/list.lua @@ -0,0 +1,41 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +-- Get all the files in the directory +local sDir = shell.dir() +if tArgs[1] ~= nil then + sDir = shell.resolve(tArgs[1]) +end + +if not fs.isDir(sDir) then + printError("Not a directory") + return +end + +-- Sort into dirs/files, and calculate column count +local tAll = fs.list(sDir) +local tFiles = {} +local tDirs = {} + +local bShowHidden = settings.get("list.show_hidden") +for _, sItem in pairs(tAll) do + if bShowHidden or string.sub(sItem, 1, 1) ~= "." then + local sPath = fs.combine(sDir, sItem) + if fs.isDir(sPath) then + table.insert(tDirs, sItem) + else + table.insert(tFiles, sItem) + end + end +end +table.sort(tDirs) +table.sort(tFiles) + +if term.isColour() then + textutils.pagedTabulate(colors.green, tDirs, colors.white, tFiles) +else + textutils.pagedTabulate(colors.lightGray, tDirs, colors.white, tFiles) +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/lua.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/lua.lua new file mode 100644 index 000000000..7e00ead50 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/lua.lua @@ -0,0 +1,129 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs > 0 then + print("This is an interactive Lua prompt.") + print("To run a lua program, just type its name.") + return +end + +local pretty = require "cc.pretty" +local exception = require "cc.internal.exception" + +local running = true +local tCommandHistory = {} +local tEnv = { + ["exit"] = setmetatable({}, { + __tostring = function() return "Call exit() to exit." end, + __call = function() running = false end, + }), + ["_echo"] = function(...) + return ... + end, +} +setmetatable(tEnv, { __index = _ENV }) + +-- Replace our package.path, so that it loads from the current directory, rather +-- than from /rom/programs. This makes it a little more friendly to use and +-- closer to what you'd expect. +do + local dir = shell.dir() + if dir:sub(1, 1) ~= "/" then dir = "/" .. dir end + if dir:sub(-1) ~= "/" then dir = dir .. "/" end + + local strip_path = "?;?.lua;?/init.lua;" + local path = package.path + if path:sub(1, #strip_path) == strip_path then + path = path:sub(#strip_path + 1) + end + + package.path = dir .. "?;" .. dir .. "?.lua;" .. dir .. "?/init.lua;" .. path +end + +if term.isColour() then + term.setTextColour(colours.yellow) +end +print("Interactive Lua prompt.") +print("Call exit() to exit.") +term.setTextColour(colours.white) + +local chunk_idx, chunk_map = 1, {} +while running do + --if term.isColour() then + -- term.setTextColour( colours.yellow ) + --end + write("lua> ") + --term.setTextColour( colours.white ) + + local input = read(nil, tCommandHistory, function(sLine) + if settings.get("lua.autocomplete") then + local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$") + if nStartPos then + sLine = string.sub(sLine, nStartPos) + end + if #sLine > 0 then + return textutils.complete(sLine, tEnv) + end + end + return nil + end) + if input:match("%S") and tCommandHistory[#tCommandHistory] ~= input then + table.insert(tCommandHistory, input) + end + if settings.get("lua.warn_against_use_of_local") and input:match("^%s*local%s+") then + if term.isColour() then + term.setTextColour(colours.yellow) + end + print("To access local variables in later inputs, remove the local keyword.") + term.setTextColour(colours.white) + end + + local name, offset = "=lua[" .. chunk_idx .. "]", 0 + + local force_print = 0 + local func, err = load(input, name, "t", tEnv) + + local expr_func = load("return _echo(" .. input .. ");", name, "t", tEnv) + if not func then + if expr_func then + func = expr_func + offset = 13 + force_print = 1 + end + elseif expr_func then + func = expr_func + offset = 13 + end + + if func then + chunk_map[name] = { contents = input, offset = offset } + chunk_idx = chunk_idx + 1 + + local results = table.pack(exception.try(func)) + if results[1] then + local n = 1 + while n < results.n or n <= force_print do + local value = results[n + 1] + local ok, serialised = pcall(pretty.pretty, value, { + function_args = settings.get("lua.function_args"), + function_source = settings.get("lua.function_source"), + }) + if ok then + pretty.print(serialised) + else + print(tostring(value)) + end + n = n + 1 + end + else + printError(results[2]) + require "cc.internal.exception".report(results[2], results[3], chunk_map) + end + else + local parser = require "cc.internal.syntax" + if parser.parse_repl(input) then printError(err) end + end + +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/mkdir.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/mkdir.lua new file mode 100644 index 000000000..5300fccde --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/mkdir.lua @@ -0,0 +1,22 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +if #tArgs < 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +for _, v in ipairs(tArgs) do + local sNewDir = shell.resolve(v) + if fs.exists(sNewDir) and not fs.isDir(sNewDir) then + printError(v .. ": Destination exists") + elseif fs.isReadOnly(sNewDir) then + printError(v .. ": Access denied") + else + fs.makeDir(sNewDir) + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/monitor.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/monitor.lua new file mode 100644 index 000000000..01f5a1f2f --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/monitor.lua @@ -0,0 +1,98 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage:") + print(" " .. programName .. " ") + print(" " .. programName .. " scale ") + return +end + +local tArgs = { ... } +if #tArgs < 2 or tArgs[1] == "scale" and #tArgs < 3 then + printUsage() + return +end + +if tArgs[1] == "scale" then + local sName = tArgs[2] + if peripheral.getType(sName) ~= "monitor" then + print("No monitor named " .. sName) + return + end + + local nRes = tonumber(tArgs[3]) + if nRes == nil or nRes < 0.5 or nRes > 5 then + print("Invalid scale: " .. tArgs[3]) + return + end + + peripheral.call(sName, "setTextScale", nRes) + return +end + +local sName = tArgs[1] +if peripheral.getType(sName) ~= "monitor" then + print("No monitor named " .. sName) + return +end + +local sProgram = tArgs[2] +local sPath = shell.resolveProgram(sProgram) +if sPath == nil then + print("No such program: " .. sProgram) + return +end + +print("Running " .. sProgram .. " on monitor " .. sName) + +local monitor = peripheral.wrap(sName) +local previousTerm = term.redirect(monitor) + +local co = coroutine.create(function() + (shell.execute or shell.run)(sProgram, table.unpack(tArgs, 3)) +end) + +local function resume(...) + local ok, param = coroutine.resume(co, ...) + if not ok then + printError(param) + end + return param +end + +local timers = {} + +local ok, param = pcall(function() + local sFilter = resume() + while coroutine.status(co) ~= "dead" do + local tEvent = table.pack(os.pullEventRaw()) + if sFilter == nil or tEvent[1] == sFilter or tEvent[1] == "terminate" then + sFilter = resume(table.unpack(tEvent, 1, tEvent.n)) + end + if coroutine.status(co) ~= "dead" and (sFilter == nil or sFilter == "mouse_click") then + if tEvent[1] == "monitor_touch" and tEvent[2] == sName then + timers[os.startTimer(0.1)] = { tEvent[3], tEvent[4] } + sFilter = resume("mouse_click", 1, table.unpack(tEvent, 3, tEvent.n)) + end + end + if coroutine.status(co) ~= "dead" and (sFilter == nil or sFilter == "term_resize") then + if tEvent[1] == "monitor_resize" and tEvent[2] == sName then + sFilter = resume("term_resize") + end + end + if coroutine.status(co) ~= "dead" and (sFilter == nil or sFilter == "mouse_up") then + if tEvent[1] == "timer" and timers[tEvent[2]] then + sFilter = resume("mouse_up", 1, table.unpack(timers[tEvent[2]], 1, 2)) + timers[tEvent[2]] = nil + end + end + end +end) + +term.redirect(previousTerm) +if not ok then + printError(param) +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/motd.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/motd.lua new file mode 100644 index 000000000..518e16bd0 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/motd.lua @@ -0,0 +1,30 @@ +-- SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +local date = os.date("*t") +if date.month == 1 and date.day == 1 then + print("Happy new year!") +elseif date.month == 12 and date.day == 24 then + print("Merry X-mas!") +elseif date.month == 10 and date.day == 31 then + print("OOoooOOOoooo! Spooky!") +elseif date.month == 4 and date.day == 28 then + print("Ed Balls") +else + local tMotd = {} + + for sPath in string.gmatch(settings.get("motd.path"), "[^:]+") do + if fs.exists(sPath) then + for sLine in io.lines(sPath) do + table.insert(tMotd, sLine) + end + end + end + + if #tMotd == 0 then + print("missingno") + else + print(tMotd[math.random(1, #tMotd)]) + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/move.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/move.lua new file mode 100644 index 000000000..ffc55ebf7 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/move.lua @@ -0,0 +1,51 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs < 2 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sSource = shell.resolve(tArgs[1]) +local sDest = shell.resolve(tArgs[2]) +local tFiles = fs.find(sSource) + +local function sanity_checks(source, dest) + if fs.exists(dest) then + printError("Destination exists") + return false + elseif fs.isReadOnly(dest) then + printError("Destination is read-only") + return false + elseif fs.isDriveRoot(source) then + printError("Cannot move mount /" .. source) + return false + elseif fs.isReadOnly(source) then + printError("Cannot move read-only file /" .. source) + return false + end + return true +end + +if #tFiles > 0 then + for _, sFile in ipairs(tFiles) do + if fs.isDir(sDest) then + local dest = fs.combine(sDest, fs.getName(sFile)) + if sanity_checks(sFile, dest) then + fs.move(sFile, dest) + end + elseif #tFiles == 1 then + if sanity_checks(sFile, sDest) then + fs.move(sFile, sDest) + end + else + printError("Cannot overwrite file multiple times") + return + end + end +else + printError("No matching files") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/peripherals.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/peripherals.lua new file mode 100644 index 000000000..e947e6c17 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/peripherals.lua @@ -0,0 +1,14 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tPeripherals = peripheral.getNames() +print("Attached Peripherals:") +if #tPeripherals > 0 then + for n = 1, #tPeripherals do + local sPeripheral = tPeripherals[n] + print(sPeripheral .. " (" .. table.concat({ peripheral.getType(sPeripheral) }, ", ") .. ")") + end +else + print("None") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/equip.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/equip.lua new file mode 100644 index 000000000..7f60c7fc7 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/equip.lua @@ -0,0 +1,15 @@ +-- SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +if not pocket then + printError("Requires a Pocket Computer") + return +end + +local ok, err = pocket.equipBack() +if not ok then + printError(err) +else + print("Item equipped") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/falling.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/falling.lua new file mode 100644 index 000000000..d6f623b66 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/falling.lua @@ -0,0 +1,654 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--[[ +Falling - Based on Tetris by Alexey Pajitnov +This version written by Gopher, at the request of Dan200, for +ComputerCraft v1.6. No particular rights are reserved. +--]] + +local function colorass(c, bw) + return term.isColor() and c or bw +end + +local block_s1 = { + { + { 1, 0, 0, 0 }, + { 1, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 0, 1, 1, 0 }, + { 1, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "{}"), + fg = colorass(colors.blue, colors.black), + bg = colorass(colors.cyan, colors.white), + } +local block_s2 = { + { + { 0, 1, 0, 0 }, + { 1, 1, 0, 0 }, + { 1, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 1, 1, 0, 0 }, + { 0, 1, 1, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "{}"), + fg = colorass(colors.green, colors.black), + bg = colorass(colors.lime, colors.white), + } +local block_line = { + { + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 1, 1, 1, 1 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "[]"), + fg = colorass(colors.pink, colors.black), + bg = colorass(colors.red, colors.white), + } +local block_square = { + { + { 1, 1, 0, 0 }, + { 1, 1, 0, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "[]"), + fg = colorass(colors.lightBlue, colors.black), + bg = colorass(colors.blue, colors.white), + } +local block_L1 = { + { + { 1, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 1, 1, 1, 0 }, + { 1, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 1, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 1, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "()"), + fg = colorass(colors.orange, colors.black), + bg = colorass(colors.yellow, colors.white), + } +local block_L2 = { + { + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 1, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 1, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 1, 1, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 1, 0, 0, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "()"), + fg = colorass(colors.brown, colors.black), + bg = colorass(colors.orange, colors.white), + } +local block_T = { + { + { 0, 1, 0, 0 }, + { 1, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 1, 1, 1, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 1, 0, 0 }, + { 0, 1, 1, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 1, 0, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + ch = colorass(" ", "<>"), + fg = colorass(colors.cyan, colors.black), + bg = colorass(colors.purple, colors.white), + } + +local blocks = { block_line, block_square, block_s1, block_s2, block_L1, block_L2, block_T } + +local points = { 4, 10, 30, 120 } + +local function lpad(text, amt) + text = tostring(text) + return string.rep(" ", amt - #text) .. text +end + +local width, height = term.getSize() + +if height < 19 or width < 26 then + print("Your screen is too small to play :(") + return +end + + +local speedsByLevel = { + 1.2, + 1.0, + .8, + .65, + .5, + .4, + .3, + .25, + .2, + .15, + .1, + .05, } + +local level = 1 + +local function playGame() + local score = 0 + local lines = 0 + local initialLevel = level + local next = blocks[math.random(1, #blocks)] + + local pit = {} + + + local heightAdjust = 0 + + if height <= 19 then + heightAdjust = 1 + end + + + + local function drawScreen() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + + term.setTextColor(colors.black) + term.setBackgroundColor(colorass(colors.lightGray, colors.white)) + term.setCursorPos(22, 2) + term.write("Score") --score + term.setCursorPos(22, 5) + term.write("Level") --level + term.setCursorPos(22, 8) + term.write("Lines") --lines + term.setCursorPos(22, 12) + term.write("Next") --next + + term.setCursorPos(21, 1) + term.write(" ") + term.setCursorPos(21, 2) + term.write(" ") --score + term.setCursorPos(21, 3) + term.write(" ") + term.setCursorPos(21, 4) + term.write(" ") + term.setCursorPos(21, 5) + term.write(" ") --level + term.setCursorPos(21, 6) + term.write(" ") + term.setCursorPos(21, 7) + term.write(" ") + term.setCursorPos(21, 8) + term.write(" ") --lines + term.setCursorPos(21, 9) + term.write(" ") + term.setCursorPos(21, 10) + term.write(" ") + term.setCursorPos(21, 11) + term.write(" ") + term.setCursorPos(21, 12) + term.write(" ") --next + term.setCursorPos(26, 12) + term.write(" ") --next + term.setCursorPos(21, 13) + term.write(" ") + term.setCursorPos(21, 14) + term.write(" ") + term.setCursorPos(21, 15) + term.write(" ") + term.setCursorPos(21, 16) + term.write(" ") + term.setCursorPos(21, 17) + term.write(" ") + term.setCursorPos(21, 18) + term.write(" ") + term.setCursorPos(21, 19) + term.write(" ") + term.setCursorPos(21, 20) + term.write(" ") + end + + local function updateNumbers() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + + term.setCursorPos(22, 3) + term.write(lpad(score, 5)) --score + term.setCursorPos(22, 6) + term.write(lpad(level, 5)) --level + term.setCursorPos(22, 9) + term.write(lpad(lines, 5)) --lines + end + + local function drawBlockAt(block, xp, yp, rot) + term.setTextColor(block.fg) + term.setBackgroundColor(block.bg) + for y = 1, 4 do + for x = 1, 4 do + if block[rot][y][x] == 1 then + term.setCursorPos((xp + x) * 2 - 3, yp + y - 1 - heightAdjust) + term.write(block.ch) + end + end + end + end + + local function eraseBlockAt(block, xp, yp, rot) + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + for y = 1, 4 do + for x = 1, 4 do + if block[rot][y][x] == 1 then + term.setCursorPos((xp + x) * 2 - 3, yp + y - 1 - heightAdjust) + term.write(" ") + end + end + end + end + + local function testBlockAt(block, xp, yp, rot) + for y = 1, 4 do + local ty = yp + y - 1 + for x = 1, 4 do + local tx = xp + x - 1 + if block[rot][y][x] == 1 then + if tx > 10 or tx < 1 or ty > 20 or pit[ty][tx] ~= 0 then + return true + end + end + end + end + end + + local function pitBlock(block, xp, yp, rot) + for y = 1, 4 do + for x = 1, 4 do + if block[rot][y][x] == 1 then + pit[yp + y - 1][xp + x - 1] = block + end + end + end + end + + + local function clearPit() + for row = 1, 20 do + pit[row] = {} + for col = 1, 10 do + pit[row][col] = 0 + end + end + end + + + + drawScreen() + updateNumbers() + + --declare & init the pit + clearPit() + + + + local halt = false + local dropSpeed = speedsByLevel[math.min(level, 12)] + + + local curBlock = next + next = blocks[math.random(1, 7)] + + local curX, curY, curRot = 4, 1, 1 + local dropTimer = os.startTimer(dropSpeed) + + drawBlockAt(next, 11.5, 15 + heightAdjust, 1) + drawBlockAt(curBlock, curX, curY, curRot) + + local function redrawPit() + for r = 1 + heightAdjust, 20 do + term.setCursorPos(1, r - heightAdjust) + for c = 1, 10 do + if pit[r][c] == 0 then + term.setTextColor(colors.black) + term.setBackgroundColor(colors.black) + term.write(" ") + else + term.setTextColor(pit[r][c].fg) + term.setBackgroundColor(pit[r][c].bg) + term.write(pit[r][c].ch) + end + end + end + end + + local function hidePit() + for r = 1 + heightAdjust, 20 do + term.setCursorPos(1, r - heightAdjust) + term.setTextColor(colors.black) + term.setBackgroundColor(colors.black) + term.write(" ") + end + end + + local function msgBox(message) + local x = math.floor((17 - #message) / 2) + term.setBackgroundColor(colorass(colors.lightGray, colors.white)) + term.setTextColor(colors.black) + term.setCursorPos(x, 9) + term.write("+" .. string.rep("-", #message + 2) .. "+") + term.setCursorPos(x, 10) + term.write("|") + term.setCursorPos(x + #message + 3, 10) + term.write("|") + term.setCursorPos(x, 11) + term.write("+" .. string.rep("-", #message + 2) .. "+") + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.setCursorPos(x + 1, 10) + term.write(" " .. message .. " ") + end + + local function clearRows() + local rows = {} + for r = 1, 20 do + local count = 0 + for c = 1, 10 do + if pit[r][c] ~= 0 then + count = count + 1 + else + break + end + end + if count == 10 then + rows[#rows + 1] = r + end + end + + if #rows > 0 then + for _ = 1, 4 do + sleep(.1) + for r = 1, #rows do + r = rows[r] + term.setCursorPos(1, r - heightAdjust) + for c = 1, 10 do + term.setTextColor(pit[r][c].bg) + term.setBackgroundColor(pit[r][c].fg) + term.write(pit[r][c].ch) + end + end + sleep(.1) + for r = 1, #rows do + r = rows[r] + term.setCursorPos(1, r - heightAdjust) + for c = 1, 10 do + term.setTextColor(pit[r][c].fg) + term.setBackgroundColor(pit[r][c].bg) + term.write(pit[r][c].ch) + end + end + end + --now remove the rows and drop everything else + term.setBackgroundColor(colors.black) + for r = 1, #rows do + r = rows[r] + term.setCursorPos(1, r - heightAdjust) + term.write(" ") + end + sleep(.25) + for r = 1, #rows do + table.remove(pit, rows[r]) + table.insert(pit, 1, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }) + end + redrawPit() + lines = lines + #rows + score = score + points[#rows] * math.min(level, 20) + level = math.floor(lines / 10) + initialLevel + dropSpeed = speedsByLevel[math.min(level, 12)] + updateNumbers() + end + sleep(.25) + end + + local function blockFall() + if testBlockAt(curBlock, curX, curY + 1, curRot) then + pitBlock(curBlock, curX, curY, curRot) + --detect rows that clear + clearRows() + + curBlock = next + curX = 4 + curY = 1 + curRot = 1 + if testBlockAt(curBlock, curX, curY, curRot) then + halt = true + end + drawBlockAt(curBlock, curX, curY, curRot) + eraseBlockAt(next, 11.5, 15 + heightAdjust, 1) + next = blocks[math.random(1, 7)] + drawBlockAt(next, 11.5, 15 + heightAdjust, 1) + return true + else + eraseBlockAt(curBlock, curX, curY, curRot) + curY = curY + 1 + drawBlockAt(curBlock, curX, curY, curRot) + return false + end + end + + + while not halt do + local e = { os.pullEvent() } + if e[1] == "timer" then + if e[2] == dropTimer then + blockFall() + dropTimer = os.startTimer(dropSpeed) + end + elseif e[1] == "key" then + local key = e[2] + local dx, dy, dr = 0, 0, 0 + if key == keys.left or key == keys.a then + dx = -1 + elseif key == keys.right or key == keys.d then + dx = 1 + elseif key == keys.up or key == keys.w then + dr = 1 + elseif key == keys.down or key == keys.s then + while not blockFall() do end + dropTimer = os.startTimer(dropSpeed) + elseif key == keys.space then + hidePit() + msgBox("Paused") + while ({ os.pullEvent("key") })[2] ~= keys.space do end + redrawPit() + drawBlockAt(curBlock, curX, curY, curRot) + dropTimer = os.startTimer(dropSpeed) + end + if dx + dr ~= 0 then + if not testBlockAt(curBlock, curX + dx, curY + dy, dr > 0 and curRot % #curBlock + dr or curRot) then + eraseBlockAt(curBlock, curX, curY, curRot) + curX = curX + dx + curY = curY + dy + curRot = dr == 0 and curRot or curRot % #curBlock + dr + drawBlockAt(curBlock, curX, curY, curRot) + end + end + elseif e[1] == "term_resize" then + local _, h = term.getSize() + if h == 20 then + heightAdjust = 0 + else + heightAdjust = 1 + end + redrawPit() + drawBlockAt(curBlock, curX, curY, curRot) + end + end + + msgBox("Game Over!") + while true do + local _, k = os.pullEvent("key") + if k == keys.space or k == keys.enter or k == keys.numPadEnter then + break + end + end + + level = math.min(level, 9) +end + + +local selected = 1 +local playersDetected = false + +local function drawMenu() + term.setBackgroundColor(colors.black) + term.setTextColor(colorass(colors.red, colors.white)) + term.clear() + + local cx, cy = math.floor(width / 2), math.floor(height / 2) + + term.setCursorPos(cx - 6, cy - 2) + term.write("F A L L I N G") + + if playersDetected then + if selected == 0 then + term.setTextColor(colorass(colors.blue, colors.black)) + term.setBackgroundColor(colorass(colors.gray, colors.white)) + else + term.setTextColor(colorass(colors.lightBlue, colors.white)) + term.setBackgroundColor(colors.black) + end + term.setCursorPos(cx - 12, cy) + term.write(" Play head-to-head game! ") + end + + term.setCursorPos(cx - 10, cy + 1) + if selected == 1 then + term.setTextColor(colorass(colors.blue, colors.black)) + term.setBackgroundColor(colorass(colors.lightGray, colors.white)) + else + term.setTextColor(colorass(colors.lightBlue, colors.white)) + term.setBackgroundColor(colors.black) + end + term.write(" Play from level: <" .. level .. "> ") + + term.setCursorPos(cx - 3, cy + 3) + if selected == 2 then + term.setTextColor(colorass(colors.blue, colors.black)) + term.setBackgroundColor(colorass(colors.lightGray, colors.white)) + else + term.setTextColor(colorass(colors.lightBlue, colors.white)) + term.setBackgroundColor(colors.black) + end + term.write(" Quit ") +end + + +local function runMenu() + drawMenu() + + while true do + local event = { os.pullEvent() } + if event[1] == "key" then + local key = event[2] + if key == keys.right or key == keys.d and selected == 1 then + level = math.min(level + 1, 9) + drawMenu() + elseif key == keys.left or key == keys.a and selected == 1 then + level = math.max(level - 1, 1) + drawMenu() + elseif key >= keys.one and key <= keys.nine and selected == 1 then + level = key - keys.one + 1 + drawMenu() + elseif key == keys.up or key == keys.w then + selected = selected - 1 + if selected == 0 then + selected = 2 + end + drawMenu() + elseif key == keys.down or key == keys.s then + selected = selected % 2 + 1 + drawMenu() + elseif key == keys.enter or key == keys.numPadEnter or key == keys.space then + break --begin play! + end + end + end +end + +while true do + runMenu() + if selected == 2 then + break + end + + playGame() +end + + +term.setTextColor(colors.white) +term.setBackgroundColor(colors.black) +term.clear() +term.setCursorPos(1, 1) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/unequip.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/unequip.lua new file mode 100644 index 000000000..a857426d4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/pocket/unequip.lua @@ -0,0 +1,15 @@ +-- SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +if not pocket then + printError("Requires a Pocket Computer") + return +end + +local ok, err = pocket.unequipBack() +if not ok then + printError(err) +else + print("Item unequipped") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/programs.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/programs.lua new file mode 100644 index 000000000..9554e33a4 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/programs.lua @@ -0,0 +1,12 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local bAll = false +local tArgs = { ... } +if #tArgs > 0 and tArgs[1] == "all" then + bAll = true +end + +local tPrograms = shell.programs(bAll) +textutils.pagedTabulate(tPrograms) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/reboot.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/reboot.lua new file mode 100644 index 000000000..5c2362940 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/reboot.lua @@ -0,0 +1,12 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if term.isColour() then + term.setTextColour(colours.yellow) +end +print("Goodbye") +term.setTextColour(colours.white) + +sleep(1) +os.reboot() diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/rednet/chat.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/rednet/chat.lua new file mode 100644 index 000000000..c708c34cb --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/rednet/chat.lua @@ -0,0 +1,438 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName .. " host ") + print(programName .. " join ") +end + +local sOpenedModem = nil +local function openModem() + for _, sModem in ipairs(peripheral.getNames()) do + if peripheral.getType(sModem) == "modem" then + if not rednet.isOpen(sModem) then + rednet.open(sModem) + sOpenedModem = sModem + end + return true + end + end + print("No modems found.") + return false +end + +local function closeModem() + if sOpenedModem ~= nil then + rednet.close(sOpenedModem) + sOpenedModem = nil + end +end + +-- Colours +local highlightColour, textColour +if term.isColour() then + textColour = colours.white + highlightColour = colours.yellow +else + textColour = colours.white + highlightColour = colours.white +end + +local sCommand = tArgs[1] +if sCommand == "host" then + -- "chat host" + -- Get hostname + local sHostname = tArgs[2] + if sHostname == nil then + printUsage() + return + end + + -- Host server + if not openModem() then + return + end + rednet.host("chat", sHostname) + print("0 users connected.") + + local tUsers = {} + local nUsers = 0 + local function send(sText, nUserID) + if nUserID then + local tUser = tUsers[nUserID] + if tUser then + rednet.send(tUser.nID, { + sType = "text", + nUserID = nUserID, + sText = sText, + }, "chat") + end + else + for nUserID, tUser in pairs(tUsers) do + rednet.send(tUser.nID, { + sType = "text", + nUserID = nUserID, + sText = sText, + }, "chat") + end + end + end + + -- Setup ping pong + local tPingPongTimer = {} + local function ping(nUserID) + local tUser = tUsers[nUserID] + rednet.send(tUser.nID, { + sType = "ping to client", + nUserID = nUserID, + }, "chat") + + local timer = os.startTimer(15) + tUser.bPingPonged = false + tPingPongTimer[timer] = nUserID + end + + local function printUsers() + local _, y = term.getCursorPos() + term.setCursorPos(1, y - 1) + term.clearLine() + if nUsers == 1 then + print(nUsers .. " user connected.") + else + print(nUsers .. " users connected.") + end + end + + -- Handle messages + local ok, error = pcall(parallel.waitForAny, + function() + while true do + local _, timer = os.pullEvent("timer") + local nUserID = tPingPongTimer[timer] + if nUserID and tUsers[nUserID] then + local tUser = tUsers[nUserID] + if tUser then + if not tUser.bPingPonged then + send("* " .. tUser.sUsername .. " has timed out") + tUsers[nUserID] = nil + nUsers = nUsers - 1 + printUsers() + else + ping(nUserID) + end + end + end + end + end, + function() + while true do + local tCommands + tCommands = { + ["me"] = function(tUser, sContent) + if #sContent > 0 then + send("* " .. tUser.sUsername .. " " .. sContent) + else + send("* Usage: /me [words]", tUser.nUserID) + end + end, + ["nick"] = function(tUser, sContent) + if #sContent > 0 then + local sOldName = tUser.sUsername + tUser.sUsername = sContent + send("* " .. sOldName .. " is now known as " .. tUser.sUsername) + else + send("* Usage: /nick [nickname]", tUser.nUserID) + end + end, + ["users"] = function(tUser, sContent) + send("* Connected Users:", tUser.nUserID) + local sUsers = "*" + for _, tUser in pairs(tUsers) do + sUsers = sUsers .. " " .. tUser.sUsername + end + send(sUsers, tUser.nUserID) + end, + ["help"] = function(tUser, sContent) + send("* Available commands:", tUser.nUserID) + local sCommands = "*" + for sCommand in pairs(tCommands) do + sCommands = sCommands .. " /" .. sCommand + end + send(sCommands .. " /logout", tUser.nUserID) + end, + } + + local nSenderID, tMessage = rednet.receive("chat") + if type(tMessage) == "table" then + if tMessage.sType == "login" then + -- Login from new client + local nUserID = tMessage.nUserID + local sUsername = tMessage.sUsername + if nUserID and sUsername then + tUsers[nUserID] = { + nID = nSenderID, + nUserID = nUserID, + sUsername = sUsername, + } + nUsers = nUsers + 1 + printUsers() + send("* " .. sUsername .. " has joined the chat") + ping(nUserID) + end + + else + -- Something else from existing client + local nUserID = tMessage.nUserID + local tUser = tUsers[nUserID] + if tUser and tUser.nID == nSenderID then + if tMessage.sType == "logout" then + send("* " .. tUser.sUsername .. " has left the chat") + tUsers[nUserID] = nil + nUsers = nUsers - 1 + printUsers() + + elseif tMessage.sType == "chat" then + local sMessage = tMessage.sText + if sMessage then + local sCommand = string.match(sMessage, "^/([a-z]+)") + if sCommand then + local fnCommand = tCommands[sCommand] + if fnCommand then + local sContent = string.sub(sMessage, #sCommand + 3) + fnCommand(tUser, sContent) + else + send("* Unrecognised command: /" .. sCommand, tUser.nUserID) + end + else + send("<" .. tUser.sUsername .. "> " .. tMessage.sText) + end + end + + elseif tMessage.sType == "ping to server" then + rednet.send(tUser.nID, { + sType = "pong to client", + nUserID = nUserID, + }, "chat") + + elseif tMessage.sType == "pong to server" then + tUser.bPingPonged = true + + end + end + end + end + end + end + ) + if not ok then + printError(error) + end + + -- Unhost server + for nUserID, tUser in pairs(tUsers) do + rednet.send(tUser.nID, { + sType = "kick", + nUserID = nUserID, + }, "chat") + end + rednet.unhost("chat") + closeModem() + +elseif sCommand == "join" then + -- "chat join" + -- Get hostname and username + local sHostname = tArgs[2] + local sUsername = tArgs[3] + if sHostname == nil or sUsername == nil then + printUsage() + return + end + + -- Connect + if not openModem() then + return + end + write("Looking up " .. sHostname .. "... ") + local nHostID = rednet.lookup("chat", sHostname) + if nHostID == nil then + print("Failed.") + return + else + print("Success.") + end + + -- Login + local nUserID = math.random(1, 2147483647) + rednet.send(nHostID, { + sType = "login", + nUserID = nUserID, + sUsername = sUsername, + }, "chat") + + -- Setup ping pong + local bPingPonged = true + local pingPongTimer = os.startTimer(0) + + local function ping() + rednet.send(nHostID, { + sType = "ping to server", + nUserID = nUserID, + }, "chat") + bPingPonged = false + pingPongTimer = os.startTimer(15) + end + + -- Handle messages + local w, h = term.getSize() + local parentTerm = term.current() + local titleWindow = window.create(parentTerm, 1, 1, w, 1, true) + local historyWindow = window.create(parentTerm, 1, 2, w, h - 2, true) + local promptWindow = window.create(parentTerm, 1, h, w, 1, true) + historyWindow.setCursorPos(1, h - 2) + + term.clear() + term.setTextColour(textColour) + term.redirect(promptWindow) + promptWindow.restoreCursor() + + local function drawTitle() + local w = titleWindow.getSize() + local sTitle = sUsername .. " on " .. sHostname + titleWindow.setTextColour(highlightColour) + titleWindow.setCursorPos(math.floor(w / 2 - #sTitle / 2), 1) + titleWindow.clearLine() + titleWindow.write(sTitle) + promptWindow.restoreCursor() + end + + local function printMessage(sMessage) + term.redirect(historyWindow) + print() + if string.match(sMessage, "^%*") then + -- Information + term.setTextColour(highlightColour) + write(sMessage) + term.setTextColour(textColour) + else + -- Chat + local sUsernameBit = string.match(sMessage, "^<[^>]*>") + if sUsernameBit then + term.setTextColour(highlightColour) + write(sUsernameBit) + term.setTextColour(textColour) + write(string.sub(sMessage, #sUsernameBit + 1)) + else + write(sMessage) + end + end + term.redirect(promptWindow) + promptWindow.restoreCursor() + end + + drawTitle() + + local ok, error = pcall(parallel.waitForAny, + function() + while true do + local sEvent, timer = os.pullEvent() + if sEvent == "timer" then + if timer == pingPongTimer then + if not bPingPonged then + printMessage("Server timeout.") + return + else + ping() + end + end + + elseif sEvent == "term_resize" then + local w, h = parentTerm.getSize() + titleWindow.reposition(1, 1, w, 1) + historyWindow.reposition(1, 2, w, h - 2) + promptWindow.reposition(1, h, w, 1) + + end + end + end, + function() + while true do + local nSenderID, tMessage = rednet.receive("chat") + if nSenderID == nHostID and type(tMessage) == "table" and tMessage.nUserID == nUserID then + if tMessage.sType == "text" then + local sText = tMessage.sText + if sText then + printMessage(sText) + end + + elseif tMessage.sType == "ping to client" then + rednet.send(nSenderID, { + sType = "pong to server", + nUserID = nUserID, + }, "chat") + + elseif tMessage.sType == "pong to client" then + bPingPonged = true + + elseif tMessage.sType == "kick" then + return + + end + end + end + end, + function() + local tSendHistory = {} + while true do + promptWindow.setCursorPos(1, 1) + promptWindow.clearLine() + promptWindow.setTextColor(highlightColour) + promptWindow.write(": ") + promptWindow.setTextColor(textColour) + + local sChat = read(nil, tSendHistory) + if string.match(sChat, "^/logout") then + break + else + rednet.send(nHostID, { + sType = "chat", + nUserID = nUserID, + sText = sChat, + }, "chat") + table.insert(tSendHistory, sChat) + end + end + end + ) + + -- Close the windows + term.redirect(parentTerm) + + -- Print error notice + local _, h = term.getSize() + term.setCursorPos(1, h) + term.clearLine() + term.setCursorBlink(false) + if not ok then + printError(error) + end + + -- Logout + rednet.send(nHostID, { + sType = "logout", + nUserID = nUserID, + }, "chat") + closeModem() + + -- Print disconnection notice + print("Disconnected.") + +else + -- "chat somethingelse" + printUsage() + +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/rednet/repeat.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/rednet/repeat.lua new file mode 100644 index 000000000..b1acbcc9b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/rednet/repeat.lua @@ -0,0 +1,99 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +-- Find modems +local tModems = {} +for _, sModem in ipairs(peripheral.getNames()) do + if peripheral.getType(sModem) == "modem" then + table.insert(tModems, sModem) + end +end +if #tModems == 0 then + print("No modems found.") + return +elseif #tModems == 1 then + print("1 modem found.") +else + print(#tModems .. " modems found.") +end + +local function open(nChannel) + for n = 1, #tModems do + local sModem = tModems[n] + peripheral.call(sModem, "open", nChannel) + end +end + +local function close(nChannel) + for n = 1, #tModems do + local sModem = tModems[n] + peripheral.call(sModem, "close", nChannel) + end +end + +-- Open channels +print("0 messages repeated.") +open(rednet.CHANNEL_REPEAT) + +-- Main loop (terminate to break) +local ok, error = pcall(function() + local tReceivedMessages = {} + local tReceivedMessageTimeouts = {} + local nTransmittedMessages = 0 + + while true do + local sEvent, sModem, nChannel, nReplyChannel, tMessage = os.pullEvent() + if sEvent == "modem_message" then + -- Got a modem message, rebroadcast it if it's a rednet thing + if nChannel == rednet.CHANNEL_REPEAT then + if type(tMessage) == "table" and tMessage.nMessageID and tMessage.nRecipient and type(tMessage.nRecipient) == "number" then + if not tReceivedMessages[tMessage.nMessageID] then + -- Ensure we only repeat a message once + tReceivedMessages[tMessage.nMessageID] = true + tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID + + local recipient_channel = tMessage.nRecipient + if tMessage.nRecipient ~= rednet.CHANNEL_BROADCAST then + recipient_channel = recipient_channel % rednet.MAX_ID_CHANNELS + end + + -- Send on all other open modems, to the target and to other repeaters + for n = 1, #tModems do + local sOtherModem = tModems[n] + peripheral.call(sOtherModem, "transmit", rednet.CHANNEL_REPEAT, nReplyChannel, tMessage) + peripheral.call(sOtherModem, "transmit", recipient_channel, nReplyChannel, tMessage) + end + + -- Log the event + nTransmittedMessages = nTransmittedMessages + 1 + local _, y = term.getCursorPos() + term.setCursorPos(1, y - 1) + term.clearLine() + if nTransmittedMessages == 1 then + print(nTransmittedMessages .. " message repeated.") + else + print(nTransmittedMessages .. " messages repeated.") + end + end + end + end + + elseif sEvent == "timer" then + -- Got a timer event, use it to clear the message history + local nTimer = sModem + local nMessageID = tReceivedMessageTimeouts[nTimer] + if nMessageID then + tReceivedMessageTimeouts[nTimer] = nil + tReceivedMessages[nMessageID] = nil + end + + end + end +end) +if not ok then + printError(error) +end + +-- Close channels +close(rednet.CHANNEL_REPEAT) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/redstone.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/redstone.lua new file mode 100644 index 000000000..9280ad54d --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/redstone.lua @@ -0,0 +1,122 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } + +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usages:") + print(programName .. " probe") + print(programName .. " set ") + print(programName .. " set ") + print(programName .. " pulse ") +end + +local sCommand = tArgs[1] +if sCommand == "probe" then + -- "redstone probe" + -- Regular input + print("Redstone inputs: ") + + local count = 0 + local bundledCount = 0 + for _, sSide in ipairs(redstone.getSides()) do + if redstone.getBundledInput(sSide) > 0 then + bundledCount = bundledCount + 1 + end + if redstone.getInput(sSide) then + if count > 0 then + io.write(", ") + end + io.write(sSide) + count = count + 1 + end + end + if count > 0 then + print(".") + else + print("None.") + end + + -- Bundled input + if bundledCount > 0 then + print() + print("Bundled inputs:") + for _, sSide in ipairs(redstone.getSides()) do + local nInput = redstone.getBundledInput(sSide) + if nInput ~= 0 then + write(sSide .. ": ") + local count = 0 + for sColour, nColour in pairs(colors) do + if type(nColour) == "number" and colors.test(nInput, nColour) then + if count > 0 then + write(", ") + end + if term.isColour() then + term.setTextColour(nColour) + end + write(sColour) + if term.isColour() then + term.setTextColour(colours.white) + end + count = count + 1 + end + end + print(".") + end + end + end + +elseif sCommand == "pulse" then + -- "redstone pulse" + local sSide = tArgs[2] + local nCount = tonumber(tArgs[3]) or 1 + local nPeriod = tonumber(tArgs[4]) or 0.5 + for _ = 1, nCount do + redstone.setOutput(sSide, true) + sleep(nPeriod / 2) + redstone.setOutput(sSide, false) + sleep(nPeriod / 2) + end + +elseif sCommand == "set" then + -- "redstone set" + local sSide = tArgs[2] + if #tArgs > 3 then + -- Bundled cable output + local sColour = tArgs[3] + local nColour = colors[sColour] or colours[sColour] + if type(nColour) ~= "number" then + printError("No such color") + return + end + + local sValue = tArgs[4] + if sValue == "true" then + rs.setBundledOutput(sSide, colors.combine(rs.getBundledOutput(sSide), nColour)) + elseif sValue == "false" then + rs.setBundledOutput(sSide, colors.subtract(rs.getBundledOutput(sSide), nColour)) + else + print("Value must be boolean") + end + else + -- Regular output + local sValue = tArgs[3] + local nValue = tonumber(sValue) + if sValue == "true" then + rs.setOutput(sSide, true) + elseif sValue == "false" then + rs.setOutput(sSide, false) + elseif nValue and nValue >= 0 and nValue <= 15 then + rs.setAnalogOutput(sSide, nValue) + else + print("Value must be boolean or 0-15") + end + end + +else + -- Something else + printUsage() + +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/rename.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/rename.lua new file mode 100644 index 000000000..19e402c17 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/rename.lua @@ -0,0 +1,32 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs < 2 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sSource = shell.resolve(tArgs[1]) +local sDest = shell.resolve(tArgs[2]) + +if not fs.exists(sSource) then + printError("No matching files") + return +elseif fs.isDriveRoot(sSource) then + printError("Can't rename mounts") + return +elseif fs.isReadOnly(sSource) then + printError("Source is read-only") + return +elseif fs.exists(sDest) then + printError("Destination exists") + return +elseif fs.isReadOnly(sDest) then + printError("Destination is read-only") + return +end + +fs.move(sSource, sDest) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/set.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/set.lua new file mode 100644 index 000000000..8e2072bdd --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/set.lua @@ -0,0 +1,59 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local pp = require "cc.pretty" + +local tArgs = { ... } +if #tArgs == 0 then + -- "set" + local _, y = term.getCursorPos() + local tSettings = {} + for n, sName in ipairs(settings.getNames()) do + tSettings[n] = textutils.serialize(sName) .. " is " .. textutils.serialize(settings.get(sName)) + end + textutils.pagedPrint(table.concat(tSettings, "\n"), y - 3) + +elseif #tArgs == 1 then + -- "set foo" + local sName = tArgs[1] + local deets = settings.getDetails(sName) + local msg = pp.text(sName, colors.cyan) .. " is " .. pp.pretty(deets.value) + if deets.default ~= nil and deets.value ~= deets.default then + msg = msg .. " (default is " .. pp.pretty(deets.default) .. ")" + end + pp.print(msg) + if deets.description then print(deets.description) end + +else + -- "set foo bar" + local sName = tArgs[1] + local sValue = tArgs[2] + 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 + + local option = settings.getDetails(sName) + if value == nil then + settings.unset(sName) + print(textutils.serialize(sName) .. " unset") + elseif option.type and option.type ~= type(value) then + printError(("%s is not a valid %s."):format(textutils.serialize(sValue), option.type)) + else + settings.set(sName, value) + print(textutils.serialize(sName) .. " set to " .. textutils.serialize(value)) + end + + if value ~= option.value then + settings.save() + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/shell.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/shell.lua new file mode 100644 index 000000000..dcc05dd56 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/shell.lua @@ -0,0 +1,795 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--[[- The shell API provides access to CraftOS's command line interface. + +It allows you to @{run|start programs}, @{setCompletionFunction|add completion +for a program}, and much more. + +@{shell} is not a "true" API. Instead, it is a standard program, which injects +its API into the programs that it launches. This allows for multiple shells to +run at the same time, but means that the API is not available in the global +environment, and so is unavailable to other @{os.loadAPI|APIs}. + +## Programs and the program path +When you run a command with the shell, either from the prompt or +@{shell.run|from Lua code}, the shell API performs several steps to work out +which program to run: + + 1. Firstly, the shell attempts to resolve @{shell.aliases|aliases}. This allows + us to use multiple names for a single command. For example, the `list` + program has two aliases: `ls` and `dir`. When you write `ls /rom`, that's + expanded to `list /rom`. + + 2. Next, the shell attempts to find where the program actually is. For this, it + uses the @{shell.path|program path}. This is a colon separated list of + directories, each of which is checked to see if it contains the program. + + `list` or `list.lua` doesn't exist in `.` (the current directory), so she + shell now looks in `/rom/programs`, where `list.lua` can be found! + + 3. Finally, the shell reads the file and checks if the file starts with a + `#!`. This is a [hashbang][], which says that this file shouldn't be treated + as Lua, but instead passed to _another_ program, the name of which should + follow the `#!`. + +[hashbang]: https://en.wikipedia.org/wiki/Shebang_(Unix) + +@module[module] shell +@changed 1.103.0 Added support for hashbangs. +]] + +local make_package = dofile("rom/modules/main/cc/require.lua").make + +local multishell = multishell +local parentShell = shell +local parentTerm = term.current() + +if multishell then + multishell.setTitle(multishell.getCurrent(), "shell") +end + +local bExit = false +local sDir = parentShell and parentShell.dir() or "" +local sPath = parentShell and parentShell.path() or ".:/rom/programs" +local tAliases = parentShell and parentShell.aliases() or {} +local tCompletionInfo = parentShell and parentShell.getCompletionInfo() or {} +local tProgramStack = {} + +local shell = {} --- @export +local function createShellEnv(dir) + local env = { shell = shell, multishell = multishell } + env.require, env.package = make_package(env, dir) + return env +end + +-- Set up a dummy require based on the current shell, for loading some of our internal dependencies. +local require +do + local env = setmetatable(createShellEnv("/rom/programs"), { __index = _ENV }) + require = env.require +end +local expect = require("cc.expect").expect +local exception = require "cc.internal.exception" + +-- Colours +local promptColour, textColour, bgColour +if term.isColour() then + promptColour = colours.yellow + textColour = colours.white + bgColour = colours.black +else + promptColour = colours.white + textColour = colours.white + bgColour = colours.black +end + +local function tokenise(...) + local sLine = table.concat({ ... }, " ") + local tWords = {} + local bQuoted = false + for match in string.gmatch(sLine .. "\"", "(.-)\"") do + if bQuoted then + table.insert(tWords, match) + else + for m in string.gmatch(match, "[^ \t]+") do + table.insert(tWords, m) + end + end + bQuoted = not bQuoted + end + return tWords +end + +-- Execute a program using os.run, unless a shebang is present. +-- In that case, execute the program using the interpreter specified in the hashbang. +-- This may occur recursively, up to the maximum number of times specified by remainingRecursion +-- Returns the same type as os.run, which is a boolean indicating whether the program exited successfully. +local function executeProgram(remainingRecursion, path, args) + local file, err = fs.open(path, "r") + if not file then + printError(err) + return false + end + + -- First check if the file begins with a #! + local contents = file.readLine() or "" + + if contents:sub(1, 2) == "#!" then + file.close() + + remainingRecursion = remainingRecursion - 1 + if remainingRecursion == 0 then + printError("Hashbang recursion depth limit reached when loading file: " .. path) + return false + end + + -- Load the specified hashbang program instead + local hashbangArgs = tokenise(contents:sub(3)) + local originalHashbangPath = table.remove(hashbangArgs, 1) + local resolvedHashbangProgram = shell.resolveProgram(originalHashbangPath) + if not resolvedHashbangProgram then + printError("Hashbang program not found: " .. originalHashbangPath) + return false + elseif resolvedHashbangProgram == "rom/programs/shell.lua" and #hashbangArgs == 0 then + -- If we try to launch the shell then our shebang expands to "shell ", which just does a + -- shell.run("") again, resulting in an infinite loop. This may still happen (if the user + -- has a custom shell), but this reduces the risk. + -- It's a little ugly special-casing this, but it's probably worth warning about. + printError("Cannot use the shell as a hashbang program") + return false + end + + -- Add the path and any arguments to the interpreter's arguments + table.insert(hashbangArgs, path) + for _, v in ipairs(args) do + table.insert(hashbangArgs, v) + end + + hashbangArgs[0] = originalHashbangPath + return executeProgram(remainingRecursion, resolvedHashbangProgram, hashbangArgs) + end + + contents = contents .. "\n" .. (file.readAll() or "") + file.close() + + local dir = fs.getDir(path) + local env = setmetatable(createShellEnv(dir), { __index = _G }) + env.arg = args + + local func, err = load(contents, "@/" .. path, nil, env) + if not func then + -- We had a syntax error. Attempt to run it through our own parser if + -- the file is "small enough", otherwise report the original error. + if #contents < 1024 * 128 then + local parser = require "cc.internal.syntax" + if parser.parse_program(contents) then printError(err) end + else + printError(err) + end + + return false + end + + if settings.get("bios.strict_globals", false) then + getmetatable(env).__newindex = function(_, name) + error("Attempt to create global " .. tostring(name), 2) + end + end + + local ok, err, co = exception.try(func, table.unpack(args, 1, args.n)) + + if ok then return true end + + if err and err ~= "" then + printError(err) + exception.report(err, co) + end + + return false +end + +--- Run a program with the supplied arguments. +-- +-- Unlike @{shell.run}, each argument is passed to the program verbatim. While +-- `shell.run("echo", "b c")` runs `echo` with `b` and `c`, +-- `shell.execute("echo", "b c")` runs `echo` with a single argument `b c`. +-- +-- @tparam string command The program to execute. +-- @tparam string ... Arguments to this program. +-- @treturn boolean Whether the program exited successfully. +-- @since 1.88.0 +-- @usage Run `paint my-image` from within your program: +-- +-- shell.execute("paint", "my-image") +function shell.execute(command, ...) + expect(1, command, "string") + for i = 1, select('#', ...) do + expect(i + 1, select(i, ...), "string") + end + + local sPath = shell.resolveProgram(command) + if sPath ~= nil then + tProgramStack[#tProgramStack + 1] = sPath + if multishell then + local sTitle = fs.getName(sPath) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1, -5) + end + multishell.setTitle(multishell.getCurrent(), sTitle) + end + + local result = executeProgram(100, sPath, { [0] = command, ... }) + + tProgramStack[#tProgramStack] = nil + if multishell then + if #tProgramStack > 0 then + local sTitle = fs.getName(tProgramStack[#tProgramStack]) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1, -5) + end + multishell.setTitle(multishell.getCurrent(), sTitle) + else + multishell.setTitle(multishell.getCurrent(), "shell") + end + end + return result + else + printError("No such program") + return false + end +end + +-- Install shell API + +--- Run a program with the supplied arguments. +-- +-- All arguments are concatenated together and then parsed as a command line. As +-- a result, `shell.run("program a b")` is the same as `shell.run("program", +-- "a", "b")`. +-- +-- @tparam string ... The program to run and its arguments. +-- @treturn boolean Whether the program exited successfully. +-- @usage Run `paint my-image` from within your program: +-- +-- shell.run("paint", "my-image") +-- @see shell.execute Run a program directly without parsing the arguments. +-- @changed 1.80pr1 Programs now get their own environment instead of sharing the same one. +-- @changed 1.83.0 `arg` is now added to the environment. +function shell.run(...) + local tWords = tokenise(...) + local sCommand = tWords[1] + if sCommand then + return shell.execute(sCommand, table.unpack(tWords, 2)) + end + return false +end + +--- Exit the current shell. +-- +-- This does _not_ terminate your program, it simply makes the shell terminate +-- after your program has finished. If this is the toplevel shell, then the +-- computer will be shutdown. +function shell.exit() + bExit = true +end + +--- Return the current working directory. This is what is displayed before the +-- `> ` of the shell prompt, and is used by @{shell.resolve} to handle relative +-- paths. +-- +-- @treturn string The current working directory. +-- @see setDir To change the working directory. +function shell.dir() + return sDir +end + +--- Set the current working directory. +-- +-- @tparam string dir The new working directory. +-- @throws If the path does not exist or is not a directory. +-- @usage Set the working directory to "rom" +-- +-- shell.setDir("rom") +function shell.setDir(dir) + expect(1, dir, "string") + if not fs.isDir(dir) then + error("Not a directory", 2) + end + sDir = fs.combine(dir, "") +end + +--- Set the path where programs are located. +-- +-- The path is composed of a list of directory names in a string, each separated +-- by a colon (`:`). On normal turtles will look in the current directory (`.`), +-- `/rom/programs` and `/rom/programs/turtle` folder, making the path +-- `.:/rom/programs:/rom/programs/turtle`. +-- +-- @treturn string The current shell's path. +-- @see setPath To change the current path. +function shell.path() + return sPath +end + +--- Set the @{path|current program path}. +-- +-- Be careful to prefix directories with a `/`. Otherwise they will be searched +-- for from the @{shell.dir|current directory}, rather than the computer's root. +-- +-- @tparam string path The new program path. +-- @since 1.2 +function shell.setPath(path) + expect(1, path, "string") + sPath = path +end + +--- Resolve a relative path to an absolute path. +-- +-- The @{fs} and @{io} APIs work using absolute paths, and so we must convert +-- any paths relative to the @{dir|current directory} to absolute ones. This +-- does nothing when the path starts with `/`. +-- +-- @tparam string path The path to resolve. +-- @usage Resolve `startup.lua` when in the `rom` folder. +-- +-- shell.setDir("rom") +-- print(shell.resolve("startup.lua")) +-- -- => rom/startup.lua +function shell.resolve(path) + expect(1, path, "string") + local sStartChar = string.sub(path, 1, 1) + if sStartChar == "/" or sStartChar == "\\" then + return fs.combine("", path) + else + return fs.combine(sDir, path) + end +end + +local function pathWithExtension(_sPath, _sExt) + local nLen = #sPath + local sEndChar = string.sub(_sPath, nLen, nLen) + -- Remove any trailing slashes so we can add an extension to the path safely + if sEndChar == "/" or sEndChar == "\\" then + _sPath = string.sub(_sPath, 1, nLen - 1) + end + return _sPath .. "." .. _sExt +end + +--- Resolve a program, using the @{path|program path} and list of @{aliases|aliases}. +-- +-- @tparam string command The name of the program +-- @treturn string|nil The absolute path to the program, or @{nil} if it could +-- not be found. +-- @since 1.2 +-- @changed 1.80pr1 Now searches for files with and without the `.lua` extension. +-- @usage Locate the `hello` program. +-- +-- shell.resolveProgram("hello") +-- -- => rom/programs/fun/hello.lua +function shell.resolveProgram(command) + expect(1, command, "string") + -- Substitute aliases firsts + if tAliases[command] ~= nil then + command = tAliases[command] + end + + -- If the path is a global path, use it directly + if command:find("/") or command:find("\\") then + local sPath = shell.resolve(command) + if fs.exists(sPath) and not fs.isDir(sPath) then + return sPath + else + local sPathLua = pathWithExtension(sPath, "lua") + if fs.exists(sPathLua) and not fs.isDir(sPathLua) then + return sPathLua + end + end + return nil + end + + -- Otherwise, look on the path variable + for sPath in string.gmatch(sPath, "[^:]+") do + sPath = fs.combine(shell.resolve(sPath), command) + if fs.exists(sPath) and not fs.isDir(sPath) then + return sPath + else + local sPathLua = pathWithExtension(sPath, "lua") + if fs.exists(sPathLua) and not fs.isDir(sPathLua) then + return sPathLua + end + end + end + + -- Not found + return nil +end + +--- Return a list of all programs on the @{shell.path|path}. +-- +-- @tparam[opt] boolean include_hidden Include hidden files. Namely, any which +-- start with `.`. +-- @treturn { string } A list of available programs. +-- @usage textutils.tabulate(shell.programs()) +-- @since 1.2 +function shell.programs(include_hidden) + expect(1, include_hidden, "boolean", "nil") + + local tItems = {} + + -- Add programs from the path + for sPath in string.gmatch(sPath, "[^:]+") do + sPath = shell.resolve(sPath) + if fs.isDir(sPath) then + local tList = fs.list(sPath) + for n = 1, #tList do + local sFile = tList[n] + if not fs.isDir(fs.combine(sPath, sFile)) and + (include_hidden or string.sub(sFile, 1, 1) ~= ".") then + if #sFile > 4 and sFile:sub(-4) == ".lua" then + sFile = sFile:sub(1, -5) + end + tItems[sFile] = true + 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 + +local function completeProgram(sLine) + local bIncludeHidden = settings.get("shell.autocomplete_hidden") + if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then + -- Add programs from the root + return fs.complete(sLine, sDir, { + include_files = true, + include_dirs = false, + include_hidden = bIncludeHidden, + }) + + else + local tResults = {} + local tSeen = {} + + -- Add aliases + for sAlias in pairs(tAliases) do + if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then + local sResult = string.sub(sAlias, #sLine + 1) + if not tSeen[sResult] then + table.insert(tResults, sResult) + tSeen[sResult] = true + end + end + end + + -- Add all subdirectories. We don't include files as they will be added in the block below + local tDirs = fs.complete(sLine, sDir, { + include_files = false, + include_dirs = false, + include_hidden = bIncludeHidden, + }) + for i = 1, #tDirs do + local sResult = tDirs[i] + if not tSeen[sResult] then + table.insert (tResults, sResult) + tSeen [sResult] = true + end + end + + -- Add programs from the path + local tPrograms = shell.programs() + for n = 1, #tPrograms do + local sProgram = tPrograms[n] + if #sProgram > #sLine and string.sub(sProgram, 1, #sLine) == sLine then + local sResult = string.sub(sProgram, #sLine + 1) + if not tSeen[sResult] then + table.insert(tResults, sResult) + tSeen[sResult] = true + end + end + end + + -- Sort and return + table.sort(tResults) + return tResults + end +end + +local function completeProgramArgument(sProgram, nArgument, sPart, tPreviousParts) + local tInfo = tCompletionInfo[sProgram] + if tInfo then + return tInfo.fnComplete(shell, nArgument, sPart, tPreviousParts) + end + return nil +end + +--- Complete a shell command line. +-- +-- This accepts an incomplete command, and completes the program name or +-- arguments. For instance, `l` will be completed to `ls`, and `ls ro` will be +-- completed to `ls rom/`. +-- +-- Completion handlers for your program may be registered with +-- @{shell.setCompletionFunction}. +-- +-- @tparam string sLine The input to complete. +-- @treturn { string }|nil The list of possible completions. +-- @see _G.read For more information about completion. +-- @see shell.completeProgram +-- @see shell.setCompletionFunction +-- @see shell.getCompletionInfo +-- @since 1.74 +function shell.complete(sLine) + expect(1, sLine, "string") + if #sLine > 0 then + local tWords = tokenise(sLine) + local nIndex = #tWords + if string.sub(sLine, #sLine, #sLine) == " " then + nIndex = nIndex + 1 + end + if nIndex == 1 then + local sBit = tWords[1] or "" + local sPath = shell.resolveProgram(sBit) + if tCompletionInfo[sPath] then + return { " " } + else + local tResults = completeProgram(sBit) + for n = 1, #tResults do + local sResult = tResults[n] + local sPath = shell.resolveProgram(sBit .. sResult) + if tCompletionInfo[sPath] then + tResults[n] = sResult .. " " + end + end + return tResults + end + + elseif nIndex > 1 then + local sPath = shell.resolveProgram(tWords[1]) + local sPart = tWords[nIndex] or "" + local tPreviousParts = tWords + tPreviousParts[nIndex] = nil + return completeProgramArgument(sPath , nIndex - 1, sPart, tPreviousParts) + + end + end + return nil +end + +--- Complete the name of a program. +-- +-- @tparam string program The name of a program to complete. +-- @treturn { string } A list of possible completions. +-- @see cc.shell.completion.program +function shell.completeProgram(program) + expect(1, program, "string") + return completeProgram(program) +end + +--- Set the completion function for a program. When the program is entered on +-- the command line, this program will be called to provide auto-complete +-- information. +-- +-- The completion function accepts four arguments: +-- +-- 1. The current shell. As completion functions are inherited, this is not +-- guaranteed to be the shell you registered this function in. +-- 2. The index of the argument currently being completed. +-- 3. The current argument. This may be the empty string. +-- 4. A list of the previous arguments. +-- +-- For instance, when completing `pastebin put rom/st` our pastebin completion +-- function will receive the shell API, an index of 2, `rom/st` as the current +-- argument, and a "previous" table of `{ "put" }`. This function may then wish +-- to return a table containing `artup.lua`, indicating the entire command +-- should be completed to `pastebin put rom/startup.lua`. +-- +-- You completion entries may also be followed by a space, if you wish to +-- indicate another argument is expected. +-- +-- @tparam string program The path to the program. This should be an absolute path +-- _without_ the leading `/`. +-- @tparam function(shell: table, index: number, argument: string, previous: { string }):({ string }|nil) complete +-- The completion function. +-- @see cc.shell.completion Various utilities to help with writing completion functions. +-- @see shell.complete +-- @see _G.read For more information about completion. +-- @since 1.74 +function shell.setCompletionFunction(program, complete) + expect(1, program, "string") + expect(2, complete, "function") + tCompletionInfo[program] = { + fnComplete = complete, + } +end + +--- Get a table containing all completion functions. +-- +-- This should only be needed when building custom shells. Use +-- @{setCompletionFunction} to add a completion function. +-- +-- @treturn { [string] = { fnComplete = function } } A table mapping the +-- absolute path of programs, to their completion functions. +function shell.getCompletionInfo() + return tCompletionInfo +end + +--- Returns the path to the currently running program. +-- +-- @treturn string The absolute path to the running program. +-- @since 1.3 +function shell.getRunningProgram() + if #tProgramStack > 0 then + return tProgramStack[#tProgramStack] + end + return nil +end + +--- Add an alias for a program. +-- +-- @tparam string command The name of the alias to add. +-- @tparam string program The name or path to the program. +-- @since 1.2 +-- @usage Alias `vim` to the `edit` program +-- +-- shell.setAlias("vim", "edit") +function shell.setAlias(command, program) + expect(1, command, "string") + expect(2, program, "string") + tAliases[command] = program +end + +--- Remove an alias. +-- +-- @tparam string command The alias name to remove. +function shell.clearAlias(command) + expect(1, command, "string") + tAliases[command] = nil +end + +--- Get the current aliases for this shell. +-- +-- Aliases are used to allow multiple commands to refer to a single program. For +-- instance, the `list` program is aliased to `dir` or `ls`. Running `ls`, `dir` +-- or `list` in the shell will all run the `list` program. +-- +-- @treturn { [string] = string } A table, where the keys are the names of +-- aliases, and the values are the path to the program. +-- @see shell.setAlias +-- @see shell.resolveProgram This uses aliases when resolving a program name to +-- an absolute path. +function shell.aliases() + -- Copy aliases + local tCopy = {} + for sAlias, sCommand in pairs(tAliases) do + tCopy[sAlias] = sCommand + end + return tCopy +end + +if multishell then + --- Open a new @{multishell} tab running a command. + -- + -- This behaves similarly to @{shell.run}, but instead returns the process + -- index. + -- + -- This function is only available if the @{multishell} API is. + -- + -- @tparam string ... The command line to run. + -- @see shell.run + -- @see multishell.launch + -- @since 1.6 + -- @usage Launch the Lua interpreter and switch to it. + -- + -- local id = shell.openTab("lua") + -- shell.switchTab(id) + function shell.openTab(...) + local tWords = tokenise(...) + local sCommand = tWords[1] + if sCommand then + local sPath = shell.resolveProgram(sCommand) + if sPath == "rom/programs/shell.lua" then + return multishell.launch(createShellEnv("rom/programs"), sPath, table.unpack(tWords, 2)) + elseif sPath ~= nil then + return multishell.launch(createShellEnv("rom/programs"), "rom/programs/shell.lua", sCommand, table.unpack(tWords, 2)) + else + printError("No such program") + end + end + end + + --- Switch to the @{multishell} tab with the given index. + -- + -- @tparam number id The tab to switch to. + -- @see multishell.setFocus + -- @since 1.6 + function shell.switchTab(id) + expect(1, id, "number") + multishell.setFocus(id) + end +end + +local tArgs = { ... } +if #tArgs > 0 then + -- "shell x y z" + -- Run the program specified on the commandline + shell.run(...) + +else + local function show_prompt() + term.setBackgroundColor(bgColour) + term.setTextColour(promptColour) + write(shell.dir() .. "> ") + term.setTextColour(textColour) + end + + -- "shell" + -- Print the header + term.setBackgroundColor(bgColour) + term.setTextColour(promptColour) + print(os.version()) + term.setTextColour(textColour) + + -- Run the startup program + if parentShell == nil then + shell.run("/rom/startup.lua") + end + + -- Read commands and execute them + local tCommandHistory = {} + while not bExit do + term.redirect(parentTerm) + show_prompt() + + + local complete + if settings.get("shell.autocomplete") then complete = shell.complete end + + local ok, result + local co = coroutine.create(read) + assert(coroutine.resume(co, nil, tCommandHistory, complete)) + + while coroutine.status(co) ~= "dead" do + local event = table.pack(os.pullEvent()) + if event[1] == "file_transfer" then + -- Abandon the current prompt + local _, h = term.getSize() + local _, y = term.getCursorPos() + if y == h then + term.scroll(1) + term.setCursorPos(1, y) + else + term.setCursorPos(1, y + 1) + end + term.setCursorBlink(false) + + -- Run the import script with the provided files + local ok, err = require("cc.internal.import")(event[2].getFiles()) + if not ok and err then printError(err) end + + -- And attempt to restore the prompt. + show_prompt() + term.setCursorBlink(true) + event = { "term_resize", n = 1 } -- Nasty hack to force read() to redraw. + end + + if result == nil or event[1] == result or event[1] == "terminate" then + ok, result = coroutine.resume(co, table.unpack(event, 1, event.n)) + if not ok then error(result, 0) end + end + end + + if result:match("%S") and tCommandHistory[#tCommandHistory] ~= result then + table.insert(tCommandHistory, result) + end + shell.run(result) + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/shutdown.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/shutdown.lua new file mode 100644 index 000000000..f8b80bc9b --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/shutdown.lua @@ -0,0 +1,12 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if term.isColour() then + term.setTextColour(colours.yellow) +end +print("Goodbye") +term.setTextColour(colours.white) + +sleep(1) +os.shutdown() diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/time.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/time.lua new file mode 100644 index 000000000..24ff5a3c1 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/time.lua @@ -0,0 +1,7 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local nTime = os.time() +local nDay = os.day() +print("The time is " .. textutils.formatTime(nTime, false) .. " on Day " .. nDay) diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/craft.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/craft.lua new file mode 100644 index 000000000..9dce7ab41 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/craft.lua @@ -0,0 +1,41 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +if not turtle.craft then + print("Requires a Crafty Turtle") + return +end + +local tArgs = { ... } +local nLimit = tonumber(tArgs[1]) + +if not nLimit and tArgs[1] ~= "all" then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " all|") + return +end + +local nCrafted = 0 +local nOldCount = turtle.getItemCount(turtle.getSelectedSlot()) +if turtle.craft(nLimit) then + local nNewCount = turtle.getItemCount(turtle.getSelectedSlot()) + if not nLimit or nOldCount <= nLimit then + nCrafted = nNewCount + else + nCrafted = nOldCount - nNewCount + end +end + +if nCrafted > 1 then + print(nCrafted .. " items crafted") +elseif nCrafted == 1 then + print("1 item crafted") +else + print("No items crafted") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/dance.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/dance.lua new file mode 100644 index 000000000..21dad33f3 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/dance.lua @@ -0,0 +1,110 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") +end + +local tMoves = { + function() + turtle.up() + turtle.down() + end, + function() + turtle.up() + turtle.turnLeft() + turtle.turnLeft() + turtle.turnLeft() + turtle.turnLeft() + turtle.down() + end, + function() + turtle.up() + turtle.turnRight() + turtle.turnRight() + turtle.turnRight() + turtle.turnRight() + turtle.down() + end, + function() + turtle.turnLeft() + turtle.turnLeft() + turtle.turnLeft() + turtle.turnLeft() + end, + function() + turtle.turnRight() + turtle.turnRight() + turtle.turnRight() + turtle.turnRight() + end, + function() + turtle.turnLeft() + turtle.back() + turtle.back() + turtle.turnRight() + turtle.turnRight() + turtle.back() + turtle.back() + turtle.turnLeft() + end, + function() + turtle.turnRight() + turtle.back() + turtle.back() + turtle.turnLeft() + turtle.turnLeft() + turtle.back() + turtle.back() + turtle.turnRight() + end, + function() + turtle.back() + turtle.turnLeft() + turtle.back() + turtle.turnLeft() + turtle.back() + turtle.turnLeft() + turtle.back() + turtle.turnLeft() + end, + function() + turtle.back() + turtle.turnRight() + turtle.back() + turtle.turnRight() + turtle.back() + turtle.turnRight() + turtle.back() + turtle.turnRight() + end, +} + +textutils.slowWrite("Preparing to get down.") +textutils.slowPrint("..", 0.75) + +local sAudio = nil +for _, sName in pairs(peripheral.getNames()) do + if disk.hasAudio(sName) then + disk.playAudio(sName) + print("Jamming to " .. disk.getAudioTitle(sName)) + sAudio = sName + break + end +end + +print("Press any key to stop the groove") + +parallel.waitForAny( + function() os.pullEvent("key") end, + function() + while true do + tMoves[math.random(1, #tMoves)]() + end + end +) + +if sAudio then + disk.stopAudio(sAudio) +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/equip.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/equip.lua new file mode 100644 index 000000000..2c7cc6972 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/equip.lua @@ -0,0 +1,47 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") +end + +if #tArgs ~= 2 then + printUsage() + return +end + +local function equip(nSlot, fnEquipFunction) + turtle.select(nSlot) + local nOldCount = turtle.getItemCount(nSlot) + if nOldCount == 0 then + print("Nothing to equip") + elseif fnEquipFunction() then + local nNewCount = turtle.getItemCount(nSlot) + if nNewCount > 0 then + print("Items swapped") + else + print("Item equipped") + end + else + print("Item not equippable") + end +end + +local nSlot = tonumber(tArgs[1]) +local sSide = tArgs[2] +if sSide == "left" then + equip(nSlot, turtle.equipLeft) +elseif sSide == "right" then + equip(nSlot, turtle.equipRight) +else + printUsage() + return +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/excavate.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/excavate.lua new file mode 100644 index 000000000..5e9162649 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/excavate.lua @@ -0,0 +1,361 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +if #tArgs ~= 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +-- Mine in a quarry pattern until we hit something we can't dig +local size = tonumber(tArgs[1]) +if size < 1 then + print("Excavate diameter must be positive") + return +end + +local depth = 0 +local unloaded = 0 +local collected = 0 + +local xPos, zPos = 0, 0 +local xDir, zDir = 0, 1 + +local goTo -- Filled in further down +local refuel -- Filled in further down + +local function unload(_bKeepOneFuelStack) + print("Unloading items...") + for n = 1, 16 do + local nCount = turtle.getItemCount(n) + if nCount > 0 then + turtle.select(n) + local bDrop = true + if _bKeepOneFuelStack and turtle.refuel(0) then + bDrop = false + _bKeepOneFuelStack = false + end + if bDrop then + turtle.drop() + unloaded = unloaded + nCount + end + end + end + collected = 0 + turtle.select(1) +end + +local function returnSupplies() + local x, y, z, xd, zd = xPos, depth, zPos, xDir, zDir + print("Returning to surface...") + goTo(0, 0, 0, 0, -1) + + local fuelNeeded = 2 * (x + y + z) + 1 + if not refuel(fuelNeeded) then + unload(true) + print("Waiting for fuel") + while not refuel(fuelNeeded) do + os.pullEvent("turtle_inventory") + end + else + unload(true) + end + + print("Resuming mining...") + goTo(x, y, z, xd, zd) +end + +local function collect() + local bFull = true + local nTotalItems = 0 + for n = 1, 16 do + local nCount = turtle.getItemCount(n) + if nCount == 0 then + bFull = false + end + nTotalItems = nTotalItems + nCount + end + + if nTotalItems > collected then + collected = nTotalItems + if math.fmod(collected + unloaded, 50) == 0 then + print("Mined " .. collected + unloaded .. " items.") + end + end + + if bFull then + print("No empty slots left.") + return false + end + return true +end + +function refuel(amount) + local fuelLevel = turtle.getFuelLevel() + if fuelLevel == "unlimited" then + return true + end + + local needed = amount or xPos + zPos + depth + 2 + if turtle.getFuelLevel() < needed then + for n = 1, 16 do + if turtle.getItemCount(n) > 0 then + turtle.select(n) + if turtle.refuel(1) then + while turtle.getItemCount(n) > 0 and turtle.getFuelLevel() < needed do + turtle.refuel(1) + end + if turtle.getFuelLevel() >= needed then + turtle.select(1) + return true + end + end + end + end + turtle.select(1) + return false + end + + return true +end + +local function tryForwards() + if not refuel() then + print("Not enough Fuel") + returnSupplies() + end + + while not turtle.forward() do + if turtle.detect() then + if turtle.dig() then + if not collect() then + returnSupplies() + end + else + return false + end + elseif turtle.attack() then + if not collect() then + returnSupplies() + end + else + sleep(0.5) + end + end + + xPos = xPos + xDir + zPos = zPos + zDir + return true +end + +local function tryDown() + if not refuel() then + print("Not enough Fuel") + returnSupplies() + end + + while not turtle.down() do + if turtle.detectDown() then + if turtle.digDown() then + if not collect() then + returnSupplies() + end + else + return false + end + elseif turtle.attackDown() then + if not collect() then + returnSupplies() + end + else + sleep(0.5) + end + end + + depth = depth + 1 + if math.fmod(depth, 10) == 0 then + print("Descended " .. depth .. " metres.") + end + + return true +end + +local function turnLeft() + turtle.turnLeft() + xDir, zDir = -zDir, xDir +end + +local function turnRight() + turtle.turnRight() + xDir, zDir = zDir, -xDir +end + +function goTo(x, y, z, xd, zd) + while depth > y do + if turtle.up() then + depth = depth - 1 + elseif turtle.digUp() or turtle.attackUp() then + collect() + else + sleep(0.5) + end + end + + if xPos > x then + while xDir ~= -1 do + turnLeft() + end + while xPos > x do + if turtle.forward() then + xPos = xPos - 1 + elseif turtle.dig() or turtle.attack() then + collect() + else + sleep(0.5) + end + end + elseif xPos < x then + while xDir ~= 1 do + turnLeft() + end + while xPos < x do + if turtle.forward() then + xPos = xPos + 1 + elseif turtle.dig() or turtle.attack() then + collect() + else + sleep(0.5) + end + end + end + + if zPos > z then + while zDir ~= -1 do + turnLeft() + end + while zPos > z do + if turtle.forward() then + zPos = zPos - 1 + elseif turtle.dig() or turtle.attack() then + collect() + else + sleep(0.5) + end + end + elseif zPos < z then + while zDir ~= 1 do + turnLeft() + end + while zPos < z do + if turtle.forward() then + zPos = zPos + 1 + elseif turtle.dig() or turtle.attack() then + collect() + else + sleep(0.5) + end + end + end + + while depth < y do + if turtle.down() then + depth = depth + 1 + elseif turtle.digDown() or turtle.attackDown() then + collect() + else + sleep(0.5) + end + end + + while zDir ~= zd or xDir ~= xd do + turnLeft() + end +end + +if not refuel() then + print("Out of Fuel") + return +end + +print("Excavating...") + +local reseal = false +turtle.select(1) +if turtle.digDown() then + reseal = true +end + +local alternate = 0 +local done = false +while not done do + for n = 1, size do + for _ = 1, size - 1 do + if not tryForwards() then + done = true + break + end + end + if done then + break + end + if n < size then + if math.fmod(n + alternate, 2) == 0 then + turnLeft() + if not tryForwards() then + done = true + break + end + turnLeft() + else + turnRight() + if not tryForwards() then + done = true + break + end + turnRight() + end + end + end + if done then + break + end + + if size > 1 then + if math.fmod(size, 2) == 0 then + turnRight() + else + if alternate == 0 then + turnLeft() + else + turnRight() + end + alternate = 1 - alternate + end + end + + if not tryDown() then + done = true + break + end +end + +print("Returning to surface...") + +-- Return to where we started +goTo(0, 0, 0, 0, -1) +unload(false) +goTo(0, 0, 0, 0, 1) + +-- Seal the hole +if reseal then + turtle.placeDown() +end + +print("Mined " .. collected + unloaded .. " items total.") diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/go.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/go.lua new file mode 100644 index 000000000..4d76e3ff7 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/go.lua @@ -0,0 +1,63 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +if #tArgs < 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local tHandlers = { + ["fd"] = turtle.forward, + ["forward"] = turtle.forward, + ["forwards"] = turtle.forward, + ["bk"] = turtle.back, + ["back"] = turtle.back, + ["up"] = turtle.up, + ["dn"] = turtle.down, + ["down"] = turtle.down, + ["lt"] = turtle.turnLeft, + ["left"] = turtle.turnLeft, + ["rt"] = turtle.turnRight, + ["right"] = turtle.turnRight, +} + +local nArg = 1 +while nArg <= #tArgs do + local sDirection = tArgs[nArg] + local nDistance = 1 + if nArg < #tArgs then + local num = tonumber(tArgs[nArg + 1]) + if num then + nDistance = num + nArg = nArg + 1 + end + end + nArg = nArg + 1 + + local fnHandler = tHandlers[string.lower(sDirection)] + if fnHandler then + while nDistance > 0 do + if fnHandler() then + nDistance = nDistance - 1 + elseif turtle.getFuelLevel() == 0 then + print("Out of fuel") + return + else + sleep(0.5) + end + end + else + print("No such direction: " .. sDirection) + print("Try: forward, back, up, down") + return + end + +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/refuel.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/refuel.lua new file mode 100644 index 000000000..eb65aa63c --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/refuel.lua @@ -0,0 +1,50 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +local nLimit = 1 +if #tArgs > 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " [number]") + return +elseif #tArgs > 0 then + if tArgs[1] == "all" then + nLimit = nil + else + nLimit = tonumber(tArgs[1]) + if not nLimit then + print("Invalid limit, expected a number or \"all\"") + return + end + end +end + +if turtle.getFuelLevel() ~= "unlimited" then + for n = 1, 16 do + -- Stop if we've reached the limit, or are fully refuelled. + if nLimit and nLimit <= 0 or turtle.getFuelLevel() >= turtle.getFuelLimit() then + break + end + + local nCount = turtle.getItemCount(n) + if nCount > 0 then + turtle.select(n) + if turtle.refuel(nLimit) and nLimit then + local nNewCount = turtle.getItemCount(n) + nLimit = nLimit - (nCount - nNewCount) + end + end + end + print("Fuel level is " .. turtle.getFuelLevel()) + if turtle.getFuelLevel() == turtle.getFuelLimit() then + print("Fuel limit reached") + end +else + print("Fuel level is unlimited") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/tunnel.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/tunnel.lua new file mode 100644 index 000000000..d54c48395 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/tunnel.lua @@ -0,0 +1,191 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +if #tArgs ~= 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +-- Mine in a quarry pattern until we hit something we can't dig +local length = tonumber(tArgs[1]) +if length < 1 then + print("Tunnel length must be positive") + return +end +local collected = 0 + +local function collect() + collected = collected + 1 + if math.fmod(collected, 25) == 0 then + print("Mined " .. collected .. " items.") + end +end + +local function tryDig() + while turtle.detect() do + if turtle.dig() then + collect() + sleep(0.5) + else + return false + end + end + return true +end + +local function tryDigUp() + while turtle.detectUp() do + if turtle.digUp() then + collect() + sleep(0.5) + else + return false + end + end + return true +end + +local function tryDigDown() + while turtle.detectDown() do + if turtle.digDown() then + collect() + sleep(0.5) + else + return false + end + end + return true +end + +local function refuel() + local fuelLevel = turtle.getFuelLevel() + if fuelLevel == "unlimited" or fuelLevel > 0 then + return + end + + local function tryRefuel() + for n = 1, 16 do + if turtle.getItemCount(n) > 0 then + turtle.select(n) + if turtle.refuel(1) then + turtle.select(1) + return true + end + end + end + turtle.select(1) + return false + end + + if not tryRefuel() then + print("Add more fuel to continue.") + while not tryRefuel() do + os.pullEvent("turtle_inventory") + end + print("Resuming Tunnel.") + end +end + +local function tryUp() + refuel() + while not turtle.up() do + if turtle.detectUp() then + if not tryDigUp() then + return false + end + elseif turtle.attackUp() then + collect() + else + sleep(0.5) + end + end + return true +end + +local function tryDown() + refuel() + while not turtle.down() do + if turtle.detectDown() then + if not tryDigDown() then + return false + end + elseif turtle.attackDown() then + collect() + else + sleep(0.5) + end + end + return true +end + +local function tryForward() + refuel() + while not turtle.forward() do + if turtle.detect() then + if not tryDig() then + return false + end + elseif turtle.attack() then + collect() + else + sleep(0.5) + end + end + return true +end + +print("Tunnelling...") + +for n = 1, length do + turtle.placeDown() + tryDigUp() + turtle.turnLeft() + tryDig() + tryUp() + tryDig() + turtle.turnRight() + turtle.turnRight() + tryDig() + tryDown() + tryDig() + turtle.turnLeft() + + if n < length then + tryDig() + if not tryForward() then + print("Aborting Tunnel.") + break + end + else + print("Tunnel complete.") + end + +end + +--[[ +print( "Returning to start..." ) + +-- Return to where we started +turtle.turnLeft() +turtle.turnLeft() +while depth > 0 do + if turtle.forward() then + depth = depth - 1 + else + turtle.dig() + end +end +turtle.turnRight() +turtle.turnRight() +]] + +print("Tunnel complete.") +print("Mined " .. collected .. " items total.") diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/turn.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/turn.lua new file mode 100644 index 000000000..e1e829a12 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/turn.lua @@ -0,0 +1,47 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +if #tArgs < 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local tHandlers = { + ["lt"] = turtle.turnLeft, + ["left"] = turtle.turnLeft, + ["rt"] = turtle.turnRight, + ["right"] = turtle.turnRight, +} + +local nArg = 1 +while nArg <= #tArgs do + local sDirection = tArgs[nArg] + local nDistance = 1 + if nArg < #tArgs then + local num = tonumber(tArgs[nArg + 1]) + if num then + nDistance = num + nArg = nArg + 1 + end + end + nArg = nArg + 1 + + local fnHandler = tHandlers[string.lower(sDirection)] + if fnHandler then + for _ = 1, nDistance do + fnHandler(nArg) + end + else + print("No such direction: " .. sDirection) + print("Try: left, right") + return + end +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/unequip.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/unequip.lua new file mode 100644 index 000000000..9cc17fff3 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/turtle/unequip.lua @@ -0,0 +1,49 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +if not turtle then + printError("Requires a Turtle") + return +end + +local tArgs = { ... } +local function printUsage() + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") +end + +if #tArgs ~= 1 then + printUsage() + return +end + +local function unequip(fnEquipFunction) + for nSlot = 1, 16 do + local nOldCount = turtle.getItemCount(nSlot) + if nOldCount == 0 then + turtle.select(nSlot) + if fnEquipFunction() then + local nNewCount = turtle.getItemCount(nSlot) + if nNewCount > 0 then + print("Item unequipped") + return + else + print("Nothing to unequip") + return + end + end + end + end + print("No space to unequip item") +end + +local sSide = tArgs[1] +if sSide == "left" then + unequip(turtle.equipLeft) +elseif sSide == "right" then + unequip(turtle.equipRight) +else + printUsage() + return +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/programs/type.lua b/src/main/resources/assets/cctweaked/lua/rom/programs/type.lua new file mode 100644 index 000000000..25421d700 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/programs/type.lua @@ -0,0 +1,21 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local tArgs = { ... } +if #tArgs < 1 then + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + print("Usage: " .. programName .. " ") + return +end + +local sPath = shell.resolve(tArgs[1]) +if fs.exists(sPath) then + if fs.isDir(sPath) then + print("directory") + else + print("file") + end +else + print("No such path") +end diff --git a/src/main/resources/assets/cctweaked/lua/rom/startup.lua b/src/main/resources/assets/cctweaked/lua/rom/startup.lua new file mode 100644 index 000000000..2c1f147a3 --- /dev/null +++ b/src/main/resources/assets/cctweaked/lua/rom/startup.lua @@ -0,0 +1,217 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +local completion = require "cc.shell.completion" + +-- Setup paths +local sPath = ".:/rom/programs:/rom/programs/http" +if term.isColor() then + sPath = sPath .. ":/rom/programs/advanced" +end +if turtle then + sPath = sPath .. ":/rom/programs/turtle" +else + sPath = sPath .. ":/rom/programs/rednet:/rom/programs/fun" + if term.isColor() then + sPath = sPath .. ":/rom/programs/fun/advanced" + end +end +if pocket then + sPath = sPath .. ":/rom/programs/pocket" +end +if commands then + sPath = sPath .. ":/rom/programs/command" +end +shell.setPath(sPath) +help.setPath("/rom/help") + +-- Setup aliases +shell.setAlias("ls", "list") +shell.setAlias("dir", "list") +shell.setAlias("cp", "copy") +shell.setAlias("mv", "move") +shell.setAlias("rm", "delete") +shell.setAlias("clr", "clear") +shell.setAlias("rs", "redstone") +shell.setAlias("sh", "shell") +if term.isColor() then + shell.setAlias("background", "bg") + shell.setAlias("foreground", "fg") +end + +-- Setup completion functions + +local function completePastebinPut(shell, text, previous) + if previous[2] == "put" then + return fs.complete(text, shell.dir(), true, false) + end +end + +shell.setCompletionFunction("rom/programs/alias.lua", completion.build(nil, completion.program)) +shell.setCompletionFunction("rom/programs/cd.lua", completion.build(completion.dir)) +shell.setCompletionFunction("rom/programs/clear.lua", completion.build({ completion.choice, { "screen", "palette", "all" } })) +shell.setCompletionFunction("rom/programs/copy.lua", completion.build( + { completion.dirOrFile, true }, + completion.dirOrFile +)) +shell.setCompletionFunction("rom/programs/delete.lua", completion.build({ completion.dirOrFile, many = true })) +shell.setCompletionFunction("rom/programs/drive.lua", completion.build(completion.dir)) +shell.setCompletionFunction("rom/programs/edit.lua", completion.build(completion.file)) +shell.setCompletionFunction("rom/programs/eject.lua", completion.build(completion.peripheral)) +shell.setCompletionFunction("rom/programs/gps.lua", completion.build({ completion.choice, { "host", "host ", "locate" } })) +shell.setCompletionFunction("rom/programs/help.lua", completion.build(completion.help)) +shell.setCompletionFunction("rom/programs/id.lua", completion.build(completion.peripheral)) +shell.setCompletionFunction("rom/programs/label.lua", completion.build( + { completion.choice, { "get", "get ", "set ", "clear", "clear " } }, + completion.peripheral +)) +shell.setCompletionFunction("rom/programs/list.lua", completion.build(completion.dir)) +shell.setCompletionFunction("rom/programs/mkdir.lua", completion.build({ completion.dir, many = true })) + +local complete_monitor_extra = { "scale" } +shell.setCompletionFunction("rom/programs/monitor.lua", completion.build( + function(shell, text, previous) + local choices = completion.peripheral(shell, text, previous, true) + for _, option in pairs(completion.choice(shell, text, previous, complete_monitor_extra, true)) do + choices[#choices + 1] = option + end + return choices + end, + function(shell, text, previous) + if previous[2] == "scale" then + return completion.peripheral(shell, text, previous, true) + else + return completion.programWithArgs(shell, text, previous, 3) + end + end, + { + function(shell, text, previous) + if previous[2] ~= "scale" then + return completion.programWithArgs(shell, text, previous, 3) + end + end, + many = true, + } +)) + +shell.setCompletionFunction("rom/programs/move.lua", completion.build( + { completion.dirOrFile, true }, + completion.dirOrFile +)) +shell.setCompletionFunction("rom/programs/redstone.lua", completion.build( + { completion.choice, { "probe", "set ", "pulse " } }, + completion.side +)) +shell.setCompletionFunction("rom/programs/rename.lua", completion.build( + { completion.dirOrFile, true }, + completion.dirOrFile +)) +shell.setCompletionFunction("rom/programs/shell.lua", completion.build({ completion.programWithArgs, 2, many = true })) +shell.setCompletionFunction("rom/programs/type.lua", completion.build(completion.dirOrFile)) +shell.setCompletionFunction("rom/programs/set.lua", completion.build({ completion.setting, true })) +shell.setCompletionFunction("rom/programs/advanced/bg.lua", completion.build({ completion.programWithArgs, 2, many = true })) +shell.setCompletionFunction("rom/programs/advanced/fg.lua", completion.build({ completion.programWithArgs, 2, many = true })) +shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build( + { completion.choice, { "play", "play ", "stop " } }, + completion.peripheral +)) +shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build( + { completion.choice, { "play ", "stop " } }, + function(shell, text, previous) + if previous[2] == "play" then return completion.file(shell, text, previous, true) + elseif previous[2] == "stop" then return completion.peripheral(shell, text, previous, false) + end + end, + function(shell, text, previous) + if previous[2] == "play" then return completion.peripheral(shell, text, previous, false) + end + end +)) +shell.setCompletionFunction("rom/programs/fun/advanced/paint.lua", completion.build(completion.file)) +shell.setCompletionFunction("rom/programs/http/pastebin.lua", completion.build( + { completion.choice, { "put ", "get ", "run " } }, + completePastebinPut +)) +shell.setCompletionFunction("rom/programs/rednet/chat.lua", completion.build({ completion.choice, { "host ", "join " } })) +shell.setCompletionFunction("rom/programs/command/exec.lua", completion.build(completion.command)) +shell.setCompletionFunction("rom/programs/http/wget.lua", completion.build({ completion.choice, { "run " } })) + +if turtle then + shell.setCompletionFunction("rom/programs/turtle/go.lua", completion.build( + { completion.choice, { "left", "right", "forward", "back", "down", "up" }, true, many = true } + )) + shell.setCompletionFunction("rom/programs/turtle/turn.lua", completion.build( + { completion.choice, { "left", "right" }, true, many = true } + )) + shell.setCompletionFunction("rom/programs/turtle/equip.lua", completion.build( + nil, + { completion.choice, { "left", "right" } } + )) + shell.setCompletionFunction("rom/programs/turtle/unequip.lua", completion.build( + { completion.choice, { "left", "right" } } + )) +end + +-- Run autorun files +if fs.exists("/rom/autorun") and fs.isDir("/rom/autorun") then + local tFiles = fs.list("/rom/autorun") + for _, sFile in ipairs(tFiles) do + if string.sub(sFile, 1, 1) ~= "." then + local sPath = "/rom/autorun/" .. sFile + if not fs.isDir(sPath) then + shell.run(sPath) + end + end + end +end + +local function findStartups(sBaseDir) + local tStartups = nil + local sBasePath = "/" .. fs.combine(sBaseDir, "startup") + local sStartupNode = shell.resolveProgram(sBasePath) + if sStartupNode then + tStartups = { sStartupNode } + end + -- It's possible that there is a startup directory and a startup.lua file, so this has to be + -- executed even if a file has already been found. + if fs.isDir(sBasePath) then + if tStartups == nil then + tStartups = {} + end + for _, v in pairs(fs.list(sBasePath)) do + local sPath = "/" .. fs.combine(sBasePath, v) + if not fs.isDir(sPath) then + tStartups[#tStartups + 1] = sPath + end + end + end + return tStartups +end + +-- Show MOTD +if settings.get("motd.enable") then + shell.run("motd") +end + +-- Run the user created startup, either from disk drives or the root +local tUserStartups = nil +if settings.get("shell.allow_startup") then + tUserStartups = findStartups("/") +end +if settings.get("shell.allow_disk_startup") then + for _, sName in pairs(peripheral.getNames()) do + if disk.isPresent(sName) and disk.hasData(sName) then + local startups = findStartups(disk.getMountPath(sName)) + if startups then + tUserStartups = startups + break + end + end + end +end +if tUserStartups then + for _, v in pairs(tUserStartups) do + shell.run(v) + end +end diff --git a/src/main/resources/assets/cctweaked/textures/gui/term_font.png b/src/main/resources/assets/cctweaked/textures/gui/term_font.png new file mode 100644 index 000000000..975fcc05b Binary files /dev/null and b/src/main/resources/assets/cctweaked/textures/gui/term_font.png differ diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 000000000..0901c01cf --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,17 @@ +[ + { + "modid": "cctweaked", + "name": "CC: Tweaked", + "description": "CC: Tweaked is a core mod for ComputerCraft, doing terrible things in older versions of Minecraft.", + "version": "${version}", + "mcversion": "${mcVersion}", + "url": "https://github.com/cc-tweaked/cc-tweaked", + "credits": "Dan200 for ComputerCraft", + "authorList": [ + "SquidDev" + ], + "dependencies": [ + "ComputerCraft" + ] + } +] diff --git a/vendor/Cobalt b/vendor/Cobalt new file mode 160000 index 000000000..a63040385 --- /dev/null +++ b/vendor/Cobalt @@ -0,0 +1 @@ +Subproject commit a630403859ff6321d10292fbf94dcfb30b507d9d