├── .DS_Store
├── .checkstyle
├── checkstyle.xml
└── suppressions.xml
├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ ├── config.yml
│ └── feature-request.md
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── LICENSE_HEADER
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── renovate.json
├── settings.gradle.kts
└── src
└── main
├── java
└── xyz
│ └── jpenilla
│ └── modscommand
│ ├── ModsCommandClientModInitializer.java
│ ├── ModsCommandModInitializer.java
│ ├── command
│ ├── Commander.java
│ ├── Commands.java
│ ├── RegistrableCommand.java
│ ├── argument
│ │ └── parser
│ │ │ └── ModDescriptionParser.java
│ └── commands
│ │ ├── DumpModsCommand.java
│ │ └── ModsCommand.java
│ ├── configuration
│ ├── Config.java
│ └── ConfigHolder.java
│ ├── model
│ ├── AbstractModDescription.java
│ ├── Environment.java
│ ├── FabricModMetadataModDescription.java
│ ├── ModDescription.java
│ ├── ModDescriptionImpl.java
│ └── Mods.java
│ └── util
│ ├── BiIntFunction.java
│ ├── Colors.java
│ └── Pagination.java
└── resources
└── assets
└── mods-command
└── icon.png
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jpenilla/mods-command/afc3aa251bac7faeabbda69c7e89be8aabd73fe7/.DS_Store
--------------------------------------------------------------------------------
/.checkstyle/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
266 |
267 |
268 |
269 |
278 |
279 |
280 |
285 |
286 |
287 |
288 |
--------------------------------------------------------------------------------
/.checkstyle/suppressions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 2
6 | indent_style = space
7 | insert_final_newline = true
8 | max_line_length = off
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # These are explicitly windows files and should use crlf
5 | *.bat text eol=crlf
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug
4 | title: "[Bug Report] This happens when another thing should have"
5 | labels: bug, unconfirmed
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Dig straight down
16 | 2. Right click 5 times
17 | 3. Craft a Netherite Hoe
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Environment information:**
27 |
28 | Either manually fill out the information below, *or* attach a [paste](https://paste.gg/) of the output from running `/dumpmods` or `dumpclientmods`.
29 | - Operating System: [e.g. Ubuntu 20.04, Windows 10, macOS 11.0]
30 | - Java Version: [e.g. 1.8, 11, 16]
31 | - Java Vendor: [e.g. AdoptOpenJDK, GraalVM]
32 | - Minecraft Version: [e.g. 1.16.5, 21w14a]
33 | - Mods Command Version: [e.g. 1.0.0]
34 | - Other Installed Mods: [e.g. Starlight, Sodium]
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest a feature
4 | title: "[Feature Request] My Feature"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: "build"
2 |
3 | on:
4 | push:
5 | branches: [ "**" ]
6 | tags-ignore: [ "**" ]
7 | pull_request:
8 | release:
9 | types: [ released ]
10 |
11 | jobs:
12 | call-build:
13 | uses: "jpenilla/actions/.github/workflows/shared-ci.yml@master"
14 | secrets: inherit
15 | with:
16 | modrinth-publish: true
17 | artifacts-path: 'build/libs/mods-command-mc*.jar'
18 | loom: true
19 | jdk-version: 21
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # gradle
2 |
3 | .gradle/
4 | build/
5 | out/
6 | classes/
7 |
8 | # eclipse
9 |
10 | *.launch
11 |
12 | # idea
13 |
14 | .idea/
15 | *.iml
16 | *.ipr
17 | *.iws
18 |
19 | # vscode
20 |
21 | .settings/
22 | .vscode/
23 | bin/
24 | .classpath
25 | .project
26 |
27 | # fabric
28 |
29 | run/
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/LICENSE_HEADER:
--------------------------------------------------------------------------------
1 | Mods Command
2 | Copyright (c) 2022 Jason Penilla
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mods Command
2 |
3 | [](https://github.com/jpenilla/mods-command/actions) [](LICENSE) [](https://github.com/jpenilla/mods-command/releases)
4 |
5 | A Fabric mod adding commands to list, search, and get information about installed mods.
6 | Requires [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api).
7 |
8 | ### Commands
9 |
10 | *(Minecraft Command Syntax Reference: [Minecraft Wiki](https://minecraft.fandom.com/wiki/Commands#Syntax))*
11 |
12 | Command | Description | Permission
13 | ------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------
14 | `/mods [page] []` | Displays a paginated view of installed mods. | `modscommand.mods`
15 | `/mods info ` | Displays detailed information about the specified mod. | `modscommand.mods`
16 | `/mods info children []` | Displays a paginated view of child mods for the specified mod. | `modscommand.mods`
17 | `/mods search []` | Displays a paginated view of mods matching the search query. | `modscommand.mods`
18 | `/mods config ` | Opens the Mod Menu config screen for the specified mod. This command is only registered when installed on the client and Mod Menu is installed. | `modscommand.mods`
19 | `/dumpmods` | Dumps the list of installed mods and some information about the current environment to `installed-mods.yml` in the game directory. When used in game, the contents of the file can be copied to the clipboard by clicking a chat message. This is a diagnostics command, meant to aid in creating more useful bug reports. | `modscommand.dumpmods`
20 |
21 | ### Client Commands
22 |
23 | Client commands are commands which are processed on the client and never sent to the server.
24 |
25 | All of Mods Command's commands are usable as client commands.
26 |
27 | - `/mods` and all subcommands are registered under `/clientmods` and `/modscommand:clientmods`.
28 | - `/dumpmods` is registered as `/dumpclientmods` and `/modscommand:dumpclientmods`.
29 | - The client variants of commands do not require any permissions.
30 |
31 | ### Configuration
32 |
33 | Mods Command can be configured though the `mods-command.conf` generated in the `config` directory after the first run.
34 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import me.modmuss50.mpp.ReleaseType
2 | import xyz.jpenilla.resourcefactory.fabric.Environment
3 |
4 | plugins {
5 | val indraVersion = "3.1.3"
6 | id("net.kyori.indra") version indraVersion
7 | id("net.kyori.indra.git") version indraVersion
8 | id("net.kyori.indra.checkstyle") version indraVersion
9 | id("net.kyori.indra.licenser.spotless") version indraVersion
10 | id("quiet-fabric-loom") version "1.10-SNAPSHOT"
11 | id("me.modmuss50.mod-publish-plugin") version "0.8.4"
12 | id("xyz.jpenilla.resource-factory-fabric-convention") version "1.3.0"
13 | }
14 |
15 | decorateVersion()
16 |
17 | repositories {
18 | mavenCentral()
19 | maven("https://repo.jpenilla.xyz/snapshots/") {
20 | mavenContent {
21 | snapshotsOnly()
22 | includeGroup("xyz.jpenilla")
23 | }
24 | }
25 | sonatype.s01Snapshots()
26 | sonatype.ossSnapshots()
27 | maven("https://maven.fabricmc.net/")
28 | maven("https://maven.terraformersmc.com/releases/")
29 | }
30 |
31 | val bom: Configuration by configurations.creating
32 | listOf(configurations.implementation, configurations.include, configurations.modImplementation)
33 | .forEach { it { extendsFrom(bom) } }
34 |
35 | dependencies {
36 | minecraft(libs.minecraft)
37 | mappings(loom.officialMojangMappings())
38 | modImplementation(libs.fabricLoader)
39 | modImplementation(libs.fabricApi)
40 |
41 | bom(platform(libs.cloudBom))
42 | bom(platform(libs.cloudMinecraftBom))
43 | modImplementation(libs.cloudFabric)
44 | include(libs.cloudFabric)
45 | implementation(libs.cloudMinecraftExtras)
46 | include(libs.cloudMinecraftExtras)
47 |
48 | modImplementation(libs.adventureFabric)
49 | include(libs.adventureFabric)
50 |
51 | bom(platform(libs.configurateBom))
52 | implementation(libs.configurateCore)
53 | include(libs.configurateCore)
54 | implementation(libs.configurateHocon)
55 | include(libs.configurateHocon)
56 | implementation(libs.configurateYaml)
57 | include(libs.configurateYaml)
58 |
59 | modImplementation(libs.modmenu)
60 | }
61 |
62 | fabricModJson {
63 | name = "Mods Command"
64 | author("jmp")
65 | contact {
66 | val githubUrl = "https://github.com/jpenilla/mods-command"
67 | homepage = githubUrl
68 | sources = githubUrl
69 | issues = "$githubUrl/issues"
70 | }
71 | icon("assets/mods-command/icon.png")
72 | environment = Environment.ANY
73 | mainEntrypoint("xyz.jpenilla.modscommand.ModsCommandModInitializer")
74 | clientEntrypoint("xyz.jpenilla.modscommand.ModsCommandClientModInitializer")
75 | apache2License()
76 | depends("fabric", "*")
77 | depends("fabricloader", ">=${libs.versions.fabricLoader.get()}")
78 | depends("minecraft", "1.21.5")
79 | depends("cloud", "*")
80 | depends("adventure-platform-fabric", "*")
81 | }
82 |
83 | tasks {
84 | jar {
85 | val projectName = project.name
86 | from("LICENSE") {
87 | rename { "LICENSE_${projectName}" }
88 | }
89 | }
90 | remapJar {
91 | archiveFileName.set("${project.name}-mc${libs.versions.minecraft.get()}-${project.version}.jar")
92 | }
93 | withType().configureEach {
94 | options.compilerArgs.add("-Xlint:-processing")
95 | }
96 | }
97 |
98 | indra {
99 | javaVersions {
100 | target(21)
101 | }
102 | github("jpenilla", "ModsCommand")
103 | apache2License()
104 | }
105 |
106 | indraSpotlessLicenser {
107 | licenseHeaderFile(rootProject.file("LICENSE_HEADER"))
108 | }
109 |
110 | publishMods.modrinth {
111 | projectId = "PExmWQV8"
112 | type = ReleaseType.STABLE
113 | file = tasks.remapJar.flatMap { it.archiveFile }
114 | changelog = providers.environmentVariable("RELEASE_NOTES")
115 | accessToken = providers.environmentVariable("MODRINTH_TOKEN")
116 | modLoaders.add("fabric")
117 | minecraftVersions.add(libs.versions.minecraft)
118 | }
119 |
120 | fun decorateVersion() {
121 | val versionString = version as String
122 | val decorated = if (versionString.endsWith("-SNAPSHOT")) {
123 | "$versionString+${indraGit.commit()?.name?.substring(0, 7) ?: error("Could not determine git hash")}"
124 | } else {
125 | versionString
126 | }
127 | version = decorated
128 | }
129 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | group=xyz.jpenilla
2 | version=1.1.11-SNAPSHOT
3 | description=Adds commands to list, search, and get information about installed mods.
4 |
5 | org.gradle.jvmargs=-Xmx2G
6 | org.gradle.caching=true
7 | org.gradle.parallel=true
8 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | minecraft = "1.21.5"
3 | fabricLoader = "0.16.14"
4 | fabricApi = "0.124.2+1.21.5"
5 |
6 | [libraries]
7 | minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
8 |
9 | fabricLoader = { module = "net.fabricmc:fabric-loader", version.ref = "fabricLoader" }
10 | fabricApi = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabricApi" }
11 |
12 | cloudBom = { module = "org.incendo:cloud-bom", version = "2.0.0" }
13 | cloudMinecraftBom = { module = "org.incendo:cloud-minecraft-bom", version = "2.0.0-beta.10" }
14 | cloudFabric = "org.incendo:cloud-fabric:2.0.0-beta.10"
15 | cloudMinecraftExtras = { module = "org.incendo:cloud-minecraft-extras" }
16 |
17 | adventureFabric = "net.kyori:adventure-platform-fabric:6.4.0"
18 |
19 | configurateBom = "org.spongepowered:configurate-bom:4.2.0"
20 | configurateCore = { module = "org.spongepowered:configurate-core" }
21 | configurateHocon = { module = "org.spongepowered:configurate-hocon" }
22 | configurateYaml = { module = "org.spongepowered:configurate-yaml" }
23 |
24 | modmenu = "com.terraformersmc:modmenu:13.0.3"
25 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jpenilla/mods-command/afc3aa251bac7faeabbda69c7e89be8aabd73fe7/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ],
6 | "ignoreDeps": [
7 | "quiet-fabric-loom",
8 | "com.mojang:minecraft"
9 | ],
10 | "labels": [
11 | "dependencies"
12 | ],
13 | "packageRules": [
14 | {
15 | "description": "Correct Fabric API version handling",
16 | "matchPackageNames": ["net.fabricmc.fabric-api:fabric-api", "net.fabricmc.fabric-api:fabric-api-deprecated"],
17 | "versioning": "regex:^(?\\d+)(\\.(?\\d+))?(\\.(?\\d+))?(?:\\+(?.*))?$"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | maven("https://maven.fabricmc.net/")
5 | maven("https://repo.jpenilla.xyz/snapshots/")
6 | }
7 | }
8 |
9 | plugins {
10 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
11 | }
12 |
13 | rootProject.name = "mods-command"
14 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/ModsCommandClientModInitializer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand;
18 |
19 | import java.util.function.Function;
20 | import net.fabricmc.api.ClientModInitializer;
21 | import org.checkerframework.checker.nullness.qual.NonNull;
22 | import org.checkerframework.framework.qual.DefaultQualifier;
23 | import org.incendo.cloud.CommandManager;
24 | import org.incendo.cloud.SenderMapper;
25 | import org.incendo.cloud.execution.ExecutionCoordinator;
26 | import org.incendo.cloud.fabric.FabricClientCommandManager;
27 | import xyz.jpenilla.modscommand.command.Commander;
28 | import xyz.jpenilla.modscommand.command.Commands;
29 | import xyz.jpenilla.modscommand.command.RegistrableCommand;
30 | import xyz.jpenilla.modscommand.command.commands.DumpModsCommand;
31 | import xyz.jpenilla.modscommand.command.commands.ModsCommand;
32 |
33 | @DefaultQualifier(NonNull.class)
34 | public final class ModsCommandClientModInitializer implements ClientModInitializer {
35 | @Override
36 | public void onInitializeClient() {
37 | final FabricClientCommandManager manager = new FabricClientCommandManager<>(
38 | ExecutionCoordinator.simpleCoordinator(),
39 | SenderMapper.create(
40 | Commander.ClientCommander::new,
41 | commander -> ((Commander.ClientCommander) commander).source()
42 | )
43 | );
44 | Commands.configureCommandManager(manager);
45 |
46 | this.registerCommand(manager, "clientmods", label -> new ModsCommand(label, null));
47 | this.registerCommand(manager, "dumpclientmods", label -> new DumpModsCommand(label, null));
48 | }
49 |
50 | private void registerCommand(final CommandManager commandManager, final String label, final Function factory) {
51 | factory.apply(label).register(commandManager);
52 | factory.apply("modscommand:%s".formatted(label)).register(commandManager);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/ModsCommandModInitializer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand;
18 |
19 | import java.io.IOException;
20 | import net.fabricmc.api.ModInitializer;
21 | import net.fabricmc.loader.api.FabricLoader;
22 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
23 | import org.checkerframework.checker.nullness.qual.NonNull;
24 | import org.checkerframework.framework.qual.DefaultQualifier;
25 | import org.incendo.cloud.SenderMapper;
26 | import org.incendo.cloud.execution.ExecutionCoordinator;
27 | import org.incendo.cloud.fabric.FabricServerCommandManager;
28 | import org.incendo.cloud.permission.Permission;
29 | import org.slf4j.Logger;
30 | import org.slf4j.LoggerFactory;
31 | import xyz.jpenilla.modscommand.command.Commander;
32 | import xyz.jpenilla.modscommand.command.Commands;
33 | import xyz.jpenilla.modscommand.command.commands.DumpModsCommand;
34 | import xyz.jpenilla.modscommand.command.commands.ModsCommand;
35 | import xyz.jpenilla.modscommand.configuration.Config;
36 | import xyz.jpenilla.modscommand.configuration.ConfigHolder;
37 | import xyz.jpenilla.modscommand.model.Mods;
38 |
39 | import static xyz.jpenilla.modscommand.model.Mods.mods;
40 |
41 | @DefaultQualifier(NonNull.class)
42 | public final class ModsCommandModInitializer implements ModInitializer {
43 | private static @MonotonicNonNull ModsCommandModInitializer instance;
44 | public static final Logger LOGGER = LoggerFactory.getLogger("Mods Command");
45 |
46 | private final ConfigHolder configHolder = ConfigHolder.create(
47 | FabricLoader.getInstance().getModContainer("mods-command").orElseThrow(),
48 | Config.class
49 | );
50 |
51 | @Override
52 | public void onInitialize() {
53 | instance = this;
54 |
55 | this.loadConfig();
56 |
57 | final Mods mods = mods(); // Initialize so it can't fail later
58 | LOGGER.info("Mods Command detected {} loaded mods ({} top-level).", mods.totalModCount(), mods.topLevelModCount()); // We identify ourselves in log messages due to Vanilla MC's terrible Log4j config.
59 |
60 | final FabricServerCommandManager manager = new FabricServerCommandManager<>(
61 | ExecutionCoordinator.simpleCoordinator(),
62 | SenderMapper.create(
63 | Commander.ServerCommander::new,
64 | commander -> ((Commander.ServerCommander) commander).source()
65 | )
66 | );
67 | Commands.configureCommandManager(manager);
68 |
69 | final ModsCommand modsCommand = new ModsCommand("mods", Permission.of("modscommand.mods"));
70 | modsCommand.register(manager);
71 |
72 | final DumpModsCommand dumpModsCommand = new DumpModsCommand("dumpmods", Permission.of("modscommand.dumpmods"));
73 | dumpModsCommand.register(manager);
74 | }
75 |
76 | private void loadConfig() {
77 | try {
78 | this.configHolder.load();
79 | } catch (final IOException ex) {
80 | throw new RuntimeException("Failed to load Mods Command config", ex);
81 | }
82 | }
83 |
84 | public Config config() {
85 | return this.configHolder.config();
86 | }
87 |
88 | public static ModsCommandModInitializer instance() {
89 | if (instance == null) {
90 | throw new IllegalStateException("Mods Command has not yet been initialized!");
91 | }
92 | return instance;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/command/Commander.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.command;
18 |
19 | import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
20 | import net.kyori.adventure.audience.Audience;
21 | import net.kyori.adventure.audience.ForwardingAudience;
22 | import net.kyori.adventure.platform.modcommon.MinecraftClientAudiences;
23 | import net.minecraft.commands.CommandSourceStack;
24 | import org.checkerframework.checker.nullness.qual.NonNull;
25 | import org.checkerframework.framework.qual.DefaultQualifier;
26 |
27 | @DefaultQualifier(NonNull.class)
28 | public interface Commander extends ForwardingAudience.Single {
29 | record ClientCommander(FabricClientCommandSource source) implements Commander {
30 | @Override
31 | public Audience audience() {
32 | return MinecraftClientAudiences.of().audience();
33 | }
34 | }
35 |
36 | record ServerCommander(CommandSourceStack source) implements Commander {
37 | @Override
38 | public Audience audience() {
39 | return this.source;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/command/Commands.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.command;
18 |
19 | import org.checkerframework.checker.nullness.qual.NonNull;
20 | import org.checkerframework.framework.qual.DefaultQualifier;
21 | import org.incendo.cloud.fabric.FabricCommandManager;
22 | import org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler;
23 | import xyz.jpenilla.modscommand.command.argument.parser.ModDescriptionParser;
24 |
25 | @DefaultQualifier(NonNull.class)
26 | public final class Commands {
27 | private Commands() {
28 | }
29 |
30 | public static void configureCommandManager(final @NonNull FabricCommandManager manager) {
31 | MinecraftExceptionHandler.createNative()
32 | .defaultHandlers()
33 | .registerTo(manager);
34 |
35 | ModDescriptionParser.registerParser(manager);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/command/RegistrableCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.command;
18 |
19 | import org.checkerframework.checker.nullness.qual.NonNull;
20 | import org.checkerframework.framework.qual.DefaultQualifier;
21 | import org.incendo.cloud.CommandManager;
22 |
23 | @DefaultQualifier(NonNull.class)
24 | public interface RegistrableCommand {
25 | void register(final CommandManager commandManager);
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/command/argument/parser/ModDescriptionParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.command.argument.parser;
18 |
19 | import net.minecraft.network.chat.Component;
20 | import org.checkerframework.checker.nullness.qual.NonNull;
21 | import org.checkerframework.checker.nullness.qual.Nullable;
22 | import org.checkerframework.framework.qual.DefaultQualifier;
23 | import org.incendo.cloud.CommandManager;
24 | import org.incendo.cloud.context.CommandContext;
25 | import org.incendo.cloud.context.CommandInput;
26 | import org.incendo.cloud.parser.ArgumentParseResult;
27 | import org.incendo.cloud.parser.ArgumentParser;
28 | import org.incendo.cloud.parser.ParserDescriptor;
29 | import org.incendo.cloud.suggestion.BlockingSuggestionProvider;
30 | import org.incendo.cloud.suggestion.Suggestion;
31 | import xyz.jpenilla.modscommand.command.Commander;
32 | import xyz.jpenilla.modscommand.model.ModDescription;
33 | import xyz.jpenilla.modscommand.util.Colors;
34 |
35 | import static org.incendo.cloud.brigadier.suggestion.TooltipSuggestion.suggestion;
36 | import static org.incendo.cloud.parser.ArgumentParseResult.failure;
37 | import static org.incendo.cloud.parser.ArgumentParseResult.success;
38 | import static xyz.jpenilla.modscommand.model.Mods.mods;
39 |
40 | @DefaultQualifier(NonNull.class)
41 | public final class ModDescriptionParser implements ArgumentParser, BlockingSuggestionProvider {
42 | public static void registerParser(final CommandManager manager) {
43 | manager.parserRegistry().registerParser(modDescriptionParser());
44 | }
45 |
46 | public static ParserDescriptor modDescriptionParser() {
47 | return ParserDescriptor.of(new ModDescriptionParser(), ModDescription.class);
48 | }
49 |
50 | @Override
51 | public ArgumentParseResult parse(final CommandContext commandContext, final CommandInput input) {
52 | final String read = input.readString();
53 | final @Nullable ModDescription meta = mods().findMod(read);
54 | if (meta != null) {
55 | return success(meta);
56 | }
57 | return failure(new IllegalArgumentException(
58 | String.format("No mod with id '%s'.", read)
59 | ));
60 | }
61 |
62 | @Override
63 | public Iterable extends Suggestion> suggestions(final CommandContext commandContext, final CommandInput input) {
64 | return mods().allMods()
65 | .map(modDescription -> suggestion(
66 | modDescription.modId(),
67 | Component.literal(modDescription.name()).withColor(Colors.BRIGHT_BLUE.value())
68 | ))
69 | .toList();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/command/commands/DumpModsCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.command.commands;
18 |
19 | import java.io.BufferedWriter;
20 | import java.io.IOException;
21 | import java.io.StringWriter;
22 | import java.nio.file.Files;
23 | import java.nio.file.Path;
24 | import net.fabricmc.loader.api.FabricLoader;
25 | import net.kyori.adventure.text.TextComponent;
26 | import org.checkerframework.checker.nullness.qual.NonNull;
27 | import org.checkerframework.checker.nullness.qual.Nullable;
28 | import org.checkerframework.framework.qual.DefaultQualifier;
29 | import org.incendo.cloud.Command;
30 | import org.incendo.cloud.CommandManager;
31 | import org.incendo.cloud.context.CommandContext;
32 | import org.incendo.cloud.permission.Permission;
33 | import org.spongepowered.configurate.ConfigurateException;
34 | import org.spongepowered.configurate.ConfigurationNode;
35 | import org.spongepowered.configurate.serialize.SerializationException;
36 | import org.spongepowered.configurate.yaml.NodeStyle;
37 | import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
38 | import xyz.jpenilla.modscommand.command.Commander;
39 | import xyz.jpenilla.modscommand.command.RegistrableCommand;
40 | import xyz.jpenilla.modscommand.model.ModDescription;
41 |
42 | import static net.kyori.adventure.text.Component.text;
43 | import static net.kyori.adventure.text.event.ClickEvent.copyToClipboard;
44 | import static net.kyori.adventure.text.event.ClickEvent.openFile;
45 | import static xyz.jpenilla.modscommand.model.Mods.mods;
46 | import static xyz.jpenilla.modscommand.util.Colors.EMERALD;
47 | import static xyz.jpenilla.modscommand.util.Colors.PINK;
48 |
49 | @DefaultQualifier(NonNull.class)
50 | public final class DumpModsCommand implements RegistrableCommand {
51 | private final String label;
52 | private final @Nullable Permission permission;
53 | private final Path dumpFile;
54 |
55 | public DumpModsCommand(
56 | final String primaryAlias,
57 | final @Nullable Permission permission
58 | ) {
59 | this.label = primaryAlias;
60 | this.permission = permission;
61 | this.dumpFile = FabricLoader.getInstance().getGameDir().resolve("installed-mods.yml");
62 | }
63 |
64 | @Override
65 | public void register(final CommandManager manager) {
66 | final Command.Builder builder = manager.commandBuilder(this.label);
67 | if (this.permission == null) {
68 | manager.command(builder.handler(this::executeDumpModList));
69 | } else {
70 | manager.command(builder.permission(this.permission).handler(this::executeDumpModList));
71 | }
72 | }
73 |
74 | private void executeDumpModList(final CommandContext ctx) {
75 | final String dump;
76 | try {
77 | dump = createDump();
78 | Files.writeString(this.dumpFile, dump);
79 | } catch (final IOException ex) {
80 | throw new RuntimeException("Failed to create mod list dump.", ex);
81 | }
82 | final TextComponent.Builder message = text()
83 | .content("Saved list of installed mods to ")
84 | .append(text(builder -> {
85 | builder.content(this.dumpFile.getFileName().toString()).color(PINK);
86 | if (ctx.sender() instanceof Commander.ClientCommander) {
87 | builder.clickEvent(openFile(this.dumpFile.toAbsolutePath().toString()))
88 | .hoverEvent(text("Click to open file!", EMERALD));
89 | }
90 | }))
91 | .append(text(" in the game directory."));
92 | ctx.sender().sendMessage(message);
93 | final TextComponent.Builder copyMessage = text()
94 | .content("Click here to copy it's contents to the clipboard.")
95 | .color(PINK)
96 | .clickEvent(copyToClipboard(dump))
97 | .hoverEvent(text("Click to copy to clipboard!", EMERALD));
98 | ctx.sender().sendMessage(copyMessage);
99 | }
100 |
101 | private static String createDump() throws ConfigurateException {
102 | final StringWriter stringWriter = new StringWriter();
103 | final BufferedWriter bufferedWriter = new BufferedWriter(stringWriter);
104 | final YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
105 | .sink(() -> bufferedWriter)
106 | .nodeStyle(NodeStyle.BLOCK)
107 | .build();
108 | final ConfigurationNode root = loader.createNode();
109 |
110 | final FabricLoader fabricLoader = FabricLoader.getInstance();
111 | root.node("environment-type").set(fabricLoader.getEnvironmentType());
112 | root.node("development-environment").set(fabricLoader.isDevelopmentEnvironment());
113 | root.node("launch-arguments").set(fabricLoader.getLaunchArguments(true));
114 |
115 | final ConfigurationNode os = root.node("operating-system");
116 | os.node("arch").set(System.getProperty("os.arch"));
117 | os.node("name").set(System.getProperty("os.name"));
118 | os.node("version").set(System.getProperty("os.version"));
119 |
120 | final ConfigurationNode java = root.node("java");
121 | java.node("vendor").set(System.getProperty("java.vendor"));
122 | java.node("vendor-url").set(System.getProperty("java.vendor.url"));
123 | java.node("version").set(System.getProperty("java.version"));
124 |
125 | final ConfigurationNode modsNode = root.node("mods");
126 | for (final ModDescription mod : mods().topLevelMods()) {
127 | serializeModDescriptionToNode(modsNode, mod);
128 | }
129 |
130 | loader.save(root);
131 | return stringWriter.toString();
132 | }
133 |
134 | private static void serializeModDescriptionToNode(final ConfigurationNode node, final ModDescription mod) throws SerializationException {
135 | final ConfigurationNode modNode = node.appendListNode();
136 | modNode.node("mod-id").set(mod.modId());
137 | modNode.node("name").set(mod.name());
138 | modNode.node("version").set(mod.version());
139 | if (!mod.authors().isEmpty()) {
140 | modNode.node("authors").set(String.join(", ", mod.authors()));
141 | }
142 | for (final ModDescription child : mod.children()) {
143 | serializeModDescriptionToNode(modNode.node("children"), child);
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/command/commands/ModsCommand.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.command.commands;
18 |
19 | import com.terraformersmc.modmenu.ModMenu;
20 | import java.util.List;
21 | import java.util.Locale;
22 | import java.util.function.Consumer;
23 | import java.util.function.IntFunction;
24 | import java.util.function.Predicate;
25 | import java.util.regex.Matcher;
26 | import java.util.regex.Pattern;
27 | import java.util.stream.Stream;
28 | import net.fabricmc.api.EnvType;
29 | import net.fabricmc.loader.api.FabricLoader;
30 | import net.kyori.adventure.text.Component;
31 | import net.kyori.adventure.text.ComponentBuilder;
32 | import net.kyori.adventure.text.ComponentLike;
33 | import net.kyori.adventure.text.TextComponent;
34 | import net.kyori.adventure.text.event.ClickEvent;
35 | import net.minecraft.client.Minecraft;
36 | import net.minecraft.client.gui.screens.Screen;
37 | import org.checkerframework.checker.nullness.qual.NonNull;
38 | import org.checkerframework.checker.nullness.qual.Nullable;
39 | import org.checkerframework.framework.qual.DefaultQualifier;
40 | import org.incendo.cloud.Command;
41 | import org.incendo.cloud.CommandManager;
42 | import org.incendo.cloud.component.DefaultValue;
43 | import org.incendo.cloud.component.TypedCommandComponent;
44 | import org.incendo.cloud.context.CommandContext;
45 | import org.incendo.cloud.key.CloudKey;
46 | import org.incendo.cloud.permission.Permission;
47 | import xyz.jpenilla.modscommand.command.Commander;
48 | import xyz.jpenilla.modscommand.command.RegistrableCommand;
49 | import xyz.jpenilla.modscommand.model.Environment;
50 | import xyz.jpenilla.modscommand.model.ModDescription;
51 | import xyz.jpenilla.modscommand.util.BiIntFunction;
52 | import xyz.jpenilla.modscommand.util.Pagination;
53 |
54 | import static java.util.Comparator.comparing;
55 | import static net.kyori.adventure.text.Component.empty;
56 | import static net.kyori.adventure.text.Component.newline;
57 | import static net.kyori.adventure.text.Component.space;
58 | import static net.kyori.adventure.text.Component.text;
59 | import static net.kyori.adventure.text.Component.textOfChildren;
60 | import static net.kyori.adventure.text.Component.toComponent;
61 | import static net.kyori.adventure.text.event.ClickEvent.copyToClipboard;
62 | import static net.kyori.adventure.text.event.ClickEvent.openUrl;
63 | import static net.kyori.adventure.text.event.ClickEvent.runCommand;
64 | import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
65 | import static net.kyori.adventure.text.format.TextDecoration.BOLD;
66 | import static net.kyori.adventure.text.format.TextDecoration.ITALIC;
67 | import static net.kyori.adventure.text.format.TextDecoration.UNDERLINED;
68 | import static org.incendo.cloud.key.CloudKey.cloudKey;
69 | import static org.incendo.cloud.parser.standard.IntegerParser.integerParser;
70 | import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;
71 | import static xyz.jpenilla.modscommand.command.argument.parser.ModDescriptionParser.modDescriptionParser;
72 | import static xyz.jpenilla.modscommand.model.Mods.mods;
73 | import static xyz.jpenilla.modscommand.util.Colors.BLUE;
74 | import static xyz.jpenilla.modscommand.util.Colors.BRIGHT_BLUE;
75 | import static xyz.jpenilla.modscommand.util.Colors.EMERALD;
76 | import static xyz.jpenilla.modscommand.util.Colors.MIDNIGHT_BLUE;
77 | import static xyz.jpenilla.modscommand.util.Colors.MUSTARD;
78 | import static xyz.jpenilla.modscommand.util.Colors.PINK;
79 | import static xyz.jpenilla.modscommand.util.Colors.PURPLE;
80 |
81 | @DefaultQualifier(NonNull.class)
82 | public final class ModsCommand implements RegistrableCommand {
83 | private static final CloudKey MOD_ARGUMENT_KEY = cloudKey("mod_id", ModDescription.class);
84 | private static final CloudKey PAGE_ARGUMENT_KEY = cloudKey("page_number", Integer.class);
85 | private static final CloudKey QUERY_ARGUMENT_KEY = cloudKey("query", String.class);
86 | private static final Pattern URL_PATTERN = Pattern.compile("(?:(https?)://)?([-\\w_.]+\\.\\w{2,})(/\\S*)?"); // copied from adventure-text-serializer-legacy
87 | private static final Component GRAY_SEPARATOR = text(':', GRAY);
88 | private static final Component DASH = text(" - ", MIDNIGHT_BLUE);
89 |
90 | private final String label;
91 | private final @Nullable Permission permission;
92 |
93 | public ModsCommand(final String primaryAlias, final @Nullable Permission permission) {
94 | this.label = primaryAlias;
95 | this.permission = permission;
96 | }
97 |
98 | @Override
99 | public void register(final CommandManager manager) {
100 | final Command.Builder base = manager.commandBuilder(this.label);
101 | final Command.Builder mods;
102 | if (this.permission != null) {
103 | mods = base.permission(this.permission);
104 | } else {
105 | mods = base;
106 | }
107 | manager.command(
108 | mods.handler(this::executeListMods)
109 | );
110 | manager.command(
111 | mods.literal("page")
112 | .argument(pageArgument())
113 | .handler(this::executeListMods)
114 | );
115 | final Command.Builder info = mods.literal("info")
116 | .required(MOD_ARGUMENT_KEY, modDescriptionParser());
117 | manager.command(info.handler(this::executeModInfo));
118 | manager.command(
119 | info.literal("children")
120 | .argument(pageArgument())
121 | .handler(this::executeListChildren)
122 | );
123 | manager.command(
124 | mods.literal("search")
125 | .required(QUERY_ARGUMENT_KEY, greedyStringParser())
126 | .handler(this::executeSearch)
127 | );
128 |
129 | if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT && FabricLoader.getInstance().isModLoaded("modmenu")) {
130 | manager.command(
131 | mods.literal("config")
132 | .required(MOD_ARGUMENT_KEY, modDescriptionParser())
133 | .handler(ctx -> {
134 | final ModDescription mod = ctx.get(MOD_ARGUMENT_KEY);
135 | final Minecraft client = Minecraft.getInstance();
136 | final @Nullable Screen configScreen = ModMenu.getConfigScreen(mod.modId(), client.screen);
137 | if (configScreen == null) {
138 | ctx.sender().sendMessage(textOfChildren(coloredBoldModName(mod), text(" does not have a config screen!", MUSTARD)));
139 | return;
140 | }
141 | client.execute(() -> client.setScreen(configScreen));
142 | })
143 | );
144 | }
145 | }
146 |
147 | private static TypedCommandComponent pageArgument() {
148 | return TypedCommandComponent.builder()
149 | .parser(integerParser(1))
150 | .name(PAGE_ARGUMENT_KEY.name())
151 | .optional()
152 | .defaultValue(DefaultValue.constant(1))
153 | .build();
154 | }
155 |
156 | private void executeListMods(final CommandContext ctx) {
157 | final int page = ctx.optional(PAGE_ARGUMENT_KEY).orElse(1);
158 | final Pagination pagination = Pagination.builder()
159 | .header((currentPage, pages) -> Component.textOfChildren(
160 | text("Loaded Mods", PURPLE, BOLD),
161 | text(String.format(" (%s total, %s top-level)", mods().totalModCount(), mods().topLevelModCount()), GRAY, ITALIC)
162 | ))
163 | .footer(this.footerRenderer(p -> String.format("/%s page %d", this.label, p)))
164 | .pageOutOfRange(ModsCommand::pageOutOfRange)
165 | .item((item, lastOfPage) -> Component.textOfChildren(DASH, this.shortModDescription(item)))
166 | .build();
167 | pagination.render(mods().topLevelMods(), page, 8).forEach(ctx.sender()::sendMessage);
168 | }
169 |
170 | private void executeListChildren(final CommandContext ctx) {
171 | final ModDescription mod = ctx.get(MOD_ARGUMENT_KEY);
172 | final int page = ctx.get(PAGE_ARGUMENT_KEY);
173 | if (mod.children().isEmpty()) {
174 | final TextComponent.Builder message = text()
175 | .color(MUSTARD)
176 | .content("Mod ")
177 | .append(text()
178 | .append(coloredBoldModName(mod))
179 | .apply(this.modClickAndHover(mod)))
180 | .append(text(" does not have any child mods!"));
181 | ctx.sender().sendMessage(message);
182 | return;
183 | }
184 | final Pagination pagination = Pagination.builder()
185 | .header((currentPage, pages) -> text()
186 | .color(MUSTARD)
187 | .append(coloredBoldModName(mod))
188 | .append(text(" child mods")))
189 | .footer(this.footerRenderer(p -> String.format("/%s info %s children %s", this.label, mod.modId(), p)))
190 | .pageOutOfRange(ModsCommand::pageOutOfRange)
191 | .item((item, lastOfPage) -> Component.textOfChildren(DASH, this.shortModDescription(item)))
192 | .build();
193 | pagination.render(mod.children(), page, 8).forEach(ctx.sender()::sendMessage);
194 | }
195 |
196 | private void executeSearch(final CommandContext ctx) {
197 | final String rawQuery = ctx.optional(QUERY_ARGUMENT_KEY).orElse("").toLowerCase(Locale.ENGLISH).trim();
198 | final String[] split = rawQuery.split(" ");
199 | int page = 1;
200 | String tempQuery = rawQuery;
201 | if (split.length > 1) {
202 | try {
203 | final String pageText = split[split.length - 1];
204 | page = Integer.parseInt(pageText);
205 | tempQuery = rawQuery.substring(0, Math.max(rawQuery.lastIndexOf(pageText) - 1, 0));
206 | } catch (final NumberFormatException ex) {
207 | page = 1;
208 | tempQuery = rawQuery;
209 | }
210 | }
211 | final String query = tempQuery;
212 |
213 | final List results = mods().allMods()
214 | .filter(matchesQuery(query))
215 | .flatMap(match -> Stream.concat(match.parentStream(), match.selfAndChildren()))
216 | .distinct()
217 | .sorted(comparing(ModDescription::modId))
218 | .toList();
219 | if (results.isEmpty()) {
220 | ctx.sender().sendMessage(
221 | text()
222 | .color(MUSTARD)
223 | .content("No results for query '")
224 | .append(text(query, PURPLE))
225 | .append(text("'."))
226 | );
227 | return;
228 | }
229 | final Pagination pagination = Pagination.builder()
230 | .header((currentPage, pages) -> Component.textOfChildren(
231 | text()
232 | .decorate(BOLD)
233 | .append(text(results.size(), PINK))
234 | .append(text(" results for query", PURPLE)),
235 | GRAY_SEPARATOR,
236 | space(),
237 | text(query, MUSTARD)
238 | ))
239 | .footer(this.footerRenderer(p -> String.format("/%s search %s %d", this.label, query, p)))
240 | .pageOutOfRange(ModsCommand::pageOutOfRange)
241 | .item((item, lastOfPage) -> Component.textOfChildren(DASH, this.shortModDescription(item)))
242 | .build();
243 | pagination.render(results, page, 8).forEach(ctx.sender()::sendMessage);
244 | }
245 |
246 | private static Predicate matchesQuery(final String query) {
247 | final String queryLower = query.toLowerCase(Locale.ENGLISH);
248 | return mod -> mod.modId().toLowerCase(Locale.ENGLISH).contains(queryLower)
249 | || mod.name().toLowerCase(Locale.ENGLISH).contains(queryLower)
250 | || "clientsided client-sided client sided".contains(queryLower) && mod.environment() == Environment.CLIENT
251 | || "serversided server-sided server sided".contains(queryLower) && mod.environment() == Environment.SERVER
252 | || mod.authors().stream().anyMatch(author -> author.toLowerCase(Locale.ENGLISH).contains(queryLower));
253 | }
254 |
255 | private BiIntFunction footerRenderer(final IntFunction commandFunction) {
256 | return (currentPage, pages) -> {
257 | if (pages == 1) {
258 | return empty(); // we don't need to see 'Page 1/1'
259 | }
260 | final TextComponent.Builder builder = text()
261 | .color(MUSTARD)
262 | .content("Page ")
263 | .append(text(currentPage, PURPLE))
264 | .append(text('/'))
265 | .append(text(pages, PURPLE));
266 | if (currentPage > 1) {
267 | builder.append(space())
268 | .append(previousPageButton(currentPage, commandFunction));
269 | }
270 | if (currentPage < pages) {
271 | builder.append(space())
272 | .append(nextPageButton(currentPage, commandFunction));
273 | }
274 | return builder;
275 | };
276 | }
277 |
278 | private static Component previousPageButton(final int currentPage, final IntFunction commandFunction) {
279 | return text()
280 | .content("←")
281 | .color(BRIGHT_BLUE)
282 | .clickEvent(runCommand(commandFunction.apply(currentPage - 1)))
283 | .hoverEvent(text("Click for previous page.", EMERALD))
284 | .build();
285 | }
286 |
287 | private static Component nextPageButton(final int currentPage, final IntFunction commandFunction) {
288 | return text()
289 | .content("→")
290 | .color(BRIGHT_BLUE)
291 | .clickEvent(runCommand(commandFunction.apply(currentPage + 1)))
292 | .hoverEvent(text("Click for next page.", EMERALD))
293 | .build();
294 | }
295 |
296 | private static Component pageOutOfRange(final int currentPage, final int pages) {
297 | return text()
298 | .color(MUSTARD)
299 | .content("Page ")
300 | .append(text(currentPage, PURPLE))
301 | .append(text(" is out of range"))
302 | .append(text('!'))
303 | .append(text(" There are only "))
304 | .append(text(pages, PURPLE))
305 | .append(text(" pages of results."))
306 | .build();
307 | }
308 |
309 | private void executeModInfo(final CommandContext ctx) {
310 | final ModDescription mod = ctx.get(MOD_ARGUMENT_KEY);
311 | final TextComponent.Builder builder = text()
312 | .append(coloredBoldModName(mod))
313 | .color(MUSTARD)
314 | .append(newline())
315 | .append(space())
316 | .append(labelled("mod id", text(mod.modId())));
317 |
318 | if (!mod.version().isEmpty()) {
319 | builder.append(newline())
320 | .append(space())
321 | .append(labelled("version", text(mod.version())));
322 | }
323 | if (!mod.description().isEmpty()) {
324 | builder.append(newline())
325 | .append(space())
326 | .append(labelled("description", text(mod.description())));
327 | }
328 | if (!mod.authors().isEmpty()) {
329 | builder.append(newline())
330 | .append(space())
331 | .append(labelled(
332 | "authors",
333 | mod.authors().stream()
334 | .map(Component::text)
335 | .collect(toComponent(text(", ", GRAY)))
336 | ));
337 | }
338 | if (!mod.contributors().isEmpty()) {
339 | builder.append(newline())
340 | .append(space())
341 | .append(labelled(
342 | "contributors",
343 | mod.contributors().stream()
344 | .map(Component::text)
345 | .collect(toComponent(text(", ", GRAY)))
346 | ));
347 | }
348 | if (!mod.licenses().isEmpty()) {
349 | builder.append(newline())
350 | .append(space())
351 | .append(labelled(
352 | "license",
353 | mod.licenses().stream()
354 | .map(Component::text)
355 | .collect(toComponent(text(", ", GRAY)))
356 | ));
357 | }
358 | builder.append(newline())
359 | .append(space())
360 | .append(labelled("type", text(mod.type())));
361 | if (mod.environment() != Environment.UNIVERSAL) { // should be fine
362 | builder.append(newline())
363 | .append(space())
364 | .append(labelled("environment", mod.environment().display()));
365 | }
366 | final @Nullable ModDescription parent = mod.parent();
367 | if (parent != null) {
368 | builder.append(newline())
369 | .append(space())
370 | .append(labelled(
371 | "parent mod",
372 | text()
373 | .content(parent.modId())
374 | .apply(this.modClickAndHover(parent))
375 | ));
376 | }
377 | if (!mod.children().isEmpty()) {
378 | builder.append(newline())
379 | .append(space())
380 | .append(labelled(
381 | "child mods",
382 | mod.children().stream()
383 | .limit(5)
384 | .map(this::modIdWithClickAndHover)
385 | .collect(toComponent(text(", ", GRAY)))
386 | ));
387 | if (mod.children().size() > 5) {
388 | builder.append(
389 | text()
390 | .color(GRAY)
391 | .decorate(ITALIC)
392 | .content(", and " + (mod.children().size() - 5) + " more...")
393 | .hoverEvent(text()
394 | .color(EMERALD)
395 | .content("Click to see all of ")
396 | .append(coloredBoldModName(mod))
397 | .append(text("'s child mods."))
398 | .build())
399 | .clickEvent(runCommand(String.format("/%s info %s children", this.label, mod.modId())))
400 | );
401 | }
402 | }
403 | if (!mod.contact().isEmpty()) {
404 | builder.append(newline())
405 | .append(space())
406 | .append(labelled("contact", empty()));
407 | mod.contact().forEach((key, value) -> {
408 | builder.append(newline());
409 | final TextComponent.Builder info = text()
410 | .append(space())
411 | .append(DASH)
412 | .append(labelled(key, openUrlOrCopyToClipboard(value)));
413 | builder.append(info);
414 | });
415 | }
416 | ctx.sender().sendMessage(builder);
417 | }
418 |
419 | private static Component labelled(final String label, final ComponentLike value) {
420 | final TextComponent.Builder builder = text()
421 | .append(text(label, BLUE))
422 | .append(GRAY_SEPARATOR);
423 | if (value != empty()) {
424 | builder.append(space())
425 | .append(value);
426 | }
427 | return builder.build();
428 | }
429 |
430 | private static Component openUrlOrCopyToClipboard(final String value) {
431 | final TextComponent.Builder builder = text()
432 | .content(value)
433 | .color(BRIGHT_BLUE);
434 | final Matcher matcher = URL_PATTERN.matcher(value);
435 | if (matcher.find() && matcher.group().equals(value)) {
436 | builder.hoverEvent(text("Click to open url!", EMERALD));
437 | builder.clickEvent(openUrl(value));
438 | builder.decorate(UNDERLINED);
439 | } else {
440 | builder.hoverEvent(text("Click to copy to clipboard!", EMERALD));
441 | builder.clickEvent(copyToClipboard(value));
442 | }
443 | return builder.build();
444 | }
445 |
446 | private Component shortModDescription(final ModDescription mod) {
447 | final TextComponent.Builder builder = text()
448 | .apply(this.modClickAndHover(mod))
449 | .append(text(mod.name(), BLUE))
450 | .append(text(String.format(" (%s)", mod.modId()), GRAY, ITALIC));
451 | if (!mod.version().isEmpty()) {
452 | builder.append(text(String.format(" v%s", mod.version()), EMERALD));
453 | }
454 | if (!mod.children().isEmpty()) {
455 | final String mods = mod.children().size() == 1 ? "mod" : "mods";
456 | builder.append(text(String.format(" (%d child %s)", mod.childrenStream().count(), mods), GRAY, ITALIC));
457 | }
458 | return builder.build();
459 | }
460 |
461 | private Component modIdWithClickAndHover(final ModDescription mod) {
462 | return text()
463 | .content(mod.modId())
464 | .apply(this.modClickAndHover(mod))
465 | .build();
466 | }
467 |
468 | private Consumer super ComponentBuilder, ?>> modClickAndHover(final ModDescription mod) {
469 | return builder ->
470 | builder.clickEvent(this.modInfo(mod))
471 | .hoverEvent(text()
472 | .color(EMERALD)
473 | .content("Click to see more about ")
474 | .append(coloredBoldModName(mod))
475 | .append(text('!'))
476 | .build());
477 | }
478 |
479 | private static TextComponent coloredBoldModName(final ModDescription mod) {
480 | return text(mod.name(), PURPLE, BOLD);
481 | }
482 |
483 | private ClickEvent modInfo(final ModDescription description) {
484 | return runCommand(String.format("/%s info %s", this.label, description.modId()));
485 | }
486 | }
487 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/configuration/Config.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.configuration;
18 |
19 | import java.util.HashSet;
20 | import java.util.Set;
21 | import org.checkerframework.checker.nullness.qual.NonNull;
22 | import org.checkerframework.framework.qual.DefaultQualifier;
23 | import org.spongepowered.configurate.objectmapping.ConfigSerializable;
24 | import org.spongepowered.configurate.objectmapping.meta.Comment;
25 |
26 | @ConfigSerializable
27 | @DefaultQualifier(NonNull.class)
28 | public final class Config {
29 | private HiddenMods hiddenMods = new HiddenMods();
30 |
31 | @ConfigSerializable
32 | public static final class HiddenMods {
33 | @Comment("Set the list of mod ids to hide/ignore.")
34 | private Set hiddenModIds = new HashSet<>();
35 | }
36 |
37 | public Set hiddenModIds() {
38 | return this.hiddenMods.hiddenModIds;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/configuration/ConfigHolder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.configuration;
18 |
19 | import io.leangen.geantyref.TypeToken;
20 | import java.io.IOException;
21 | import java.nio.file.Files;
22 | import java.nio.file.Path;
23 | import net.fabricmc.loader.api.FabricLoader;
24 | import net.fabricmc.loader.api.ModContainer;
25 | import org.checkerframework.checker.nullness.qual.NonNull;
26 | import org.checkerframework.checker.nullness.qual.Nullable;
27 | import org.checkerframework.framework.qual.DefaultQualifier;
28 | import org.spongepowered.configurate.ConfigurationNode;
29 | import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
30 | import org.spongepowered.configurate.loader.ConfigurationLoader;
31 |
32 | @DefaultQualifier(NonNull.class)
33 | public final class ConfigHolder {
34 | private final TypeToken configType;
35 | private final Path configFile;
36 | private final ConfigurationLoader> configLoader;
37 | private volatile @Nullable C config;
38 |
39 | private ConfigHolder(
40 | final Path file,
41 | final TypeToken configType
42 | ) {
43 | this.configType = configType;
44 | this.configFile = file;
45 | this.configLoader = createLoader(this.configFile);
46 | }
47 |
48 | public C config() {
49 | final @Nullable C config = this.config;
50 | if (config == null) {
51 | throw new IllegalStateException("Config is not loaded (null)");
52 | }
53 | return config;
54 | }
55 |
56 | public @Nullable C configIfLoaded() {
57 | return this.config;
58 | }
59 |
60 | public synchronized void load() throws IOException {
61 | @Nullable C loaded = null;
62 | @Nullable IOException fail = null;
63 | try {
64 | if (!Files.exists(this.configFile.getParent())) {
65 | Files.createDirectories(this.configFile.getParent());
66 | }
67 | final ConfigurationNode load = this.configLoader.load();
68 | loaded = load.get(this.configType);
69 | } catch (final IOException ex) {
70 | fail = ex;
71 | }
72 | if (loaded == null) {
73 | if (fail == null) {
74 | fail = new IOException("Failed to coerce loaded config node to correct type %s".formatted(this.configType.getType().getTypeName()));
75 | }
76 | throw new IOException("Failed to load config file %s".formatted(this.configFile), fail);
77 | }
78 | this.config = loaded;
79 |
80 | try {
81 | this.configLoader.save(this.configLoader.createNode(node -> node.set(this.config)));
82 | } catch (final IOException ex) {
83 | throw new IOException("Failed to write back loaded config file %s".formatted(this.configFile), ex);
84 | }
85 | }
86 |
87 | private static HoconConfigurationLoader createLoader(final Path file) {
88 | return HoconConfigurationLoader.builder()
89 | .path(file)
90 | .build();
91 | }
92 |
93 | public static ConfigHolder create(final Path file, final TypeToken configType) {
94 | return new ConfigHolder<>(file, configType);
95 | }
96 |
97 | public static ConfigHolder create(final Path file, final Class configType) {
98 | return new ConfigHolder<>(file, TypeToken.get(configType));
99 | }
100 |
101 | public static ConfigHolder create(final ModContainer modContainer, final TypeToken configType) {
102 | final Path file = FabricLoader.getInstance().getConfigDir()
103 | .resolve(modContainer.getMetadata().getId() + ".conf");
104 | return create(file, configType);
105 | }
106 |
107 | public static ConfigHolder create(final ModContainer modContainer, final Class configType) {
108 | return create(modContainer, TypeToken.get(configType));
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/model/AbstractModDescription.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.model;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Collections;
21 | import java.util.List;
22 | import net.kyori.examination.string.StringExaminer;
23 | import org.checkerframework.checker.nullness.qual.NonNull;
24 | import org.checkerframework.checker.nullness.qual.Nullable;
25 | import org.checkerframework.framework.qual.DefaultQualifier;
26 |
27 | import static java.util.Comparator.comparing;
28 |
29 | @DefaultQualifier(NonNull.class)
30 | public abstract class AbstractModDescription implements ModDescription {
31 | private final List children = new ArrayList<>();
32 | private @Nullable ModDescription parent = null;
33 |
34 | protected AbstractModDescription(final List children) {
35 | for (final ModDescription child : children) {
36 | this.addChild(child);
37 | }
38 | }
39 |
40 | public void addChild(final ModDescription newChild) {
41 | if (!(newChild instanceof AbstractModDescription newChildAbs)) {
42 | throw new IllegalArgumentException(String.format("Cannot add non-AbstractModDescription as a child. Attempted to add %s '%s'.", newChild.getClass().getName(), newChild));
43 | }
44 | newChildAbs.parent = this;
45 | this.children.add(newChild);
46 | this.children.sort(comparing(ModDescription::modId));
47 | }
48 |
49 | @Override
50 | public @Nullable ModDescription parent() {
51 | return this.parent;
52 | }
53 |
54 | @Override
55 | public List children() {
56 | return Collections.unmodifiableList(this.children);
57 | }
58 |
59 | @Override
60 | public String toString() {
61 | return StringExaminer.simpleEscaping().examine(this);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/model/Environment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.model;
18 |
19 | import net.kyori.adventure.text.Component;
20 | import net.kyori.adventure.text.ComponentLike;
21 | import org.checkerframework.checker.nullness.qual.NonNull;
22 | import org.checkerframework.framework.qual.DefaultQualifier;
23 |
24 | import static net.kyori.adventure.text.Component.text;
25 | import static xyz.jpenilla.modscommand.util.Colors.EMERALD;
26 |
27 | @DefaultQualifier(NonNull.class)
28 | public enum Environment {
29 | CLIENT(text()
30 | .content("client")
31 | .hoverEvent(text("Only runs on the client.", EMERALD))),
32 | SERVER(text()
33 | .content("server")
34 | .hoverEvent(text("Only runs on dedicated servers.", EMERALD))),
35 | UNIVERSAL(text()
36 | .content("universal")
37 | .hoverEvent(text("Can run on the client or on dedicated servers.", EMERALD)));
38 |
39 | private final Component display;
40 |
41 | Environment(final ComponentLike display) {
42 | this.display = display.asComponent();
43 | }
44 |
45 | public Component display() {
46 | return this.display;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/model/FabricModMetadataModDescription.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.model;
18 |
19 | import io.leangen.geantyref.GenericTypeReflector;
20 | import io.leangen.geantyref.TypeToken;
21 | import java.util.Arrays;
22 | import java.util.Collection;
23 | import java.util.Map;
24 | import java.util.stream.Stream;
25 | import net.fabricmc.loader.api.metadata.ModEnvironment;
26 | import net.fabricmc.loader.api.metadata.ModMetadata;
27 | import net.fabricmc.loader.api.metadata.Person;
28 | import net.kyori.examination.ExaminableProperty;
29 | import org.checkerframework.checker.nullness.qual.NonNull;
30 | import org.checkerframework.framework.qual.DefaultQualifier;
31 |
32 | @DefaultQualifier(NonNull.class)
33 | final class FabricModMetadataModDescription extends AbstractModDescription {
34 | private final ModMetadata metadata;
35 |
36 | FabricModMetadataModDescription(
37 | final ModMetadata metadata,
38 | final ModDescription... children
39 | ) {
40 | super(Arrays.asList(children));
41 | this.metadata = metadata;
42 | }
43 |
44 | @Override
45 | public String modId() {
46 | return this.metadata.getId();
47 | }
48 |
49 | @Override
50 | public String name() {
51 | return this.metadata.getName();
52 | }
53 |
54 | @Override
55 | public String version() {
56 | return this.metadata.getVersion().getFriendlyString();
57 | }
58 |
59 | @Override
60 | public String type() {
61 | return this.metadata.getType();
62 | }
63 |
64 | @Override
65 | public String description() {
66 | return this.metadata.getDescription();
67 | }
68 |
69 | @Override
70 | public Collection authors() {
71 | return this.metadata.getAuthors().stream()
72 | .map(Person::getName)
73 | .toList();
74 | }
75 |
76 | @Override
77 | public Collection contributors() {
78 | return this.metadata.getContributors().stream()
79 | .map(Person::getName)
80 | .toList();
81 | }
82 |
83 | @Override
84 | public Collection licenses() {
85 | return this.metadata.getLicense();
86 | }
87 |
88 | @Override
89 | public Map contact() {
90 | return this.metadata.getContact().asMap();
91 | }
92 |
93 | @Override
94 | public Environment environment() {
95 | return fromFabric(this.metadata.getEnvironment());
96 | }
97 |
98 | @Override
99 | public boolean hasAttribute(final TypeToken> type) {
100 | if (GenericTypeReflector.erase(type.getType()).equals(ModMetadata.class)) {
101 | return true;
102 | }
103 | return super.hasAttribute(type);
104 | }
105 |
106 | @SuppressWarnings("unchecked")
107 | @Override
108 | public A attribute(final TypeToken type) {
109 | if (GenericTypeReflector.erase(type.getType()).equals(ModMetadata.class)) {
110 | return (A) this.metadata;
111 | }
112 | return super.attribute(type);
113 | }
114 |
115 | @Override
116 | public Stream examinableProperties() {
117 | return Stream.concat(super.examinableProperties(), Stream.of(ExaminableProperty.of("metadata", this.metadata)));
118 | }
119 |
120 | private static Environment fromFabric(final ModEnvironment modEnvironment) {
121 | return switch (modEnvironment) {
122 | case CLIENT -> Environment.CLIENT;
123 | case SERVER -> Environment.SERVER;
124 | case UNIVERSAL -> Environment.UNIVERSAL;
125 | };
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/model/ModDescription.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.model;
18 |
19 | import io.leangen.geantyref.TypeToken;
20 | import java.util.Collection;
21 | import java.util.List;
22 | import java.util.Map;
23 | import java.util.stream.Stream;
24 | import net.fabricmc.loader.api.metadata.ModMetadata;
25 | import net.kyori.examination.Examinable;
26 | import net.kyori.examination.ExaminableProperty;
27 | import org.checkerframework.checker.nullness.qual.NonNull;
28 | import org.checkerframework.checker.nullness.qual.Nullable;
29 | import org.checkerframework.framework.qual.DefaultQualifier;
30 |
31 | @DefaultQualifier(NonNull.class)
32 | public interface ModDescription extends Examinable {
33 | @Nullable ModDescription parent();
34 |
35 | List children();
36 |
37 | String modId();
38 |
39 | String name();
40 |
41 | String version();
42 |
43 | String type();
44 |
45 | String description();
46 |
47 | Collection authors();
48 |
49 | Collection contributors();
50 |
51 | Collection licenses();
52 |
53 | Map contact();
54 |
55 | Environment environment();
56 |
57 | default boolean hasAttribute(final TypeToken> type) {
58 | return false;
59 | }
60 |
61 | default boolean hasAttribute(final Class> type) {
62 | return this.hasAttribute(TypeToken.get(type));
63 | }
64 |
65 | default A attribute(final TypeToken type) {
66 | throw new IllegalArgumentException();
67 | }
68 |
69 | default A attribute(final Class type) {
70 | return this.attribute(TypeToken.get(type));
71 | }
72 |
73 | default Stream parentStream() {
74 | final @Nullable ModDescription parent = this.parent();
75 | if (parent == null) {
76 | return Stream.empty();
77 | }
78 | return parent.selfAndParents();
79 | }
80 |
81 | default Stream selfAndParents() {
82 | return Stream.concat(Stream.of(this), this.parentStream());
83 | }
84 |
85 | default Stream childrenStream() {
86 | return this.children().stream().flatMap(ModDescription::selfAndChildren);
87 | }
88 |
89 | default Stream selfAndChildren() {
90 | return Stream.concat(Stream.of(this), this.childrenStream());
91 | }
92 |
93 | @Override
94 | default Stream examinableProperties() {
95 | final @Nullable ModDescription parent = this.parent();
96 | return Stream.of(
97 | ExaminableProperty.of("modId", this.modId()),
98 | ExaminableProperty.of("name", this.name()),
99 | ExaminableProperty.of("version", this.version()),
100 | ExaminableProperty.of("type", this.type()),
101 | ExaminableProperty.of("description", this.description()),
102 | ExaminableProperty.of("authors", this.authors()),
103 | ExaminableProperty.of("contributors", this.contributors()),
104 | ExaminableProperty.of("licenses", this.licenses()),
105 | ExaminableProperty.of("contact", this.contact()),
106 | ExaminableProperty.of("environment", this.environment()),
107 | ExaminableProperty.of("parent", parent == null ? null : parent.modId()),
108 | ExaminableProperty.of("children", this.children())
109 | );
110 | }
111 |
112 | static ModDescription fromFabric(final ModMetadata fabric) {
113 | return new FabricModMetadataModDescription(fabric);
114 | }
115 |
116 | static ModDescription create(
117 | final List children,
118 | final String modId,
119 | final String name,
120 | final String version,
121 | final String type,
122 | final String description,
123 | final Collection authors,
124 | final Collection contributors,
125 | final Collection licenses,
126 | final Map contact,
127 | final Environment environment
128 | ) {
129 | return new ModDescriptionImpl(children, modId, name, version, type, description, authors, contributors, licenses, contact, environment);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/model/ModDescriptionImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.model;
18 |
19 | import java.util.Collection;
20 | import java.util.List;
21 | import java.util.Map;
22 | import org.checkerframework.checker.nullness.qual.NonNull;
23 | import org.checkerframework.framework.qual.DefaultQualifier;
24 |
25 | @DefaultQualifier(NonNull.class)
26 | final class ModDescriptionImpl extends AbstractModDescription {
27 | private final String modId;
28 | private final String name;
29 | private final String version;
30 | private final String type;
31 | private final String description;
32 | private final Collection authors;
33 | private final Collection contributors;
34 | private final Collection licenses;
35 | private final Map contact;
36 | private final Environment environment;
37 |
38 | ModDescriptionImpl(
39 | final List children,
40 | final String modId,
41 | final String name,
42 | final String version,
43 | final String type,
44 | final String description,
45 | final Collection authors,
46 | final Collection contributors,
47 | final Collection licenses,
48 | final Map contact,
49 | final Environment environment
50 | ) {
51 | super(children);
52 | this.modId = modId;
53 | this.name = name;
54 | this.version = version;
55 | this.type = type;
56 | this.description = description;
57 | this.authors = authors;
58 | this.contributors = contributors;
59 | this.licenses = licenses;
60 | this.contact = contact;
61 | this.environment = environment;
62 | }
63 |
64 | @Override
65 | public String modId() {
66 | return this.modId;
67 | }
68 |
69 | @Override
70 | public String name() {
71 | return this.name;
72 | }
73 |
74 | @Override
75 | public String version() {
76 | return this.version;
77 | }
78 |
79 | @Override
80 | public String type() {
81 | return this.type;
82 | }
83 |
84 | @Override
85 | public String description() {
86 | return this.description;
87 | }
88 |
89 | @Override
90 | public Collection authors() {
91 | return this.authors;
92 | }
93 |
94 | @Override
95 | public Collection contributors() {
96 | return this.contributors;
97 | }
98 |
99 | @Override
100 | public Collection licenses() {
101 | return this.licenses;
102 | }
103 |
104 | @Override
105 | public Map contact() {
106 | return this.contact;
107 | }
108 |
109 | @Override
110 | public Environment environment() {
111 | return this.environment;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/model/Mods.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.model;
18 |
19 | import com.google.common.collect.ImmutableMap;
20 | import java.util.ArrayList;
21 | import java.util.Collection;
22 | import java.util.HashMap;
23 | import java.util.List;
24 | import java.util.Map;
25 | import java.util.Set;
26 | import java.util.stream.Stream;
27 | import net.fabricmc.loader.api.FabricLoader;
28 | import net.fabricmc.loader.api.ModContainer;
29 | import net.fabricmc.loader.api.metadata.CustomValue;
30 | import net.fabricmc.loader.api.metadata.ModMetadata;
31 | import org.checkerframework.checker.nullness.qual.NonNull;
32 | import org.checkerframework.checker.nullness.qual.Nullable;
33 | import org.checkerframework.framework.qual.DefaultQualifier;
34 | import xyz.jpenilla.modscommand.ModsCommandModInitializer;
35 |
36 | import static java.util.Collections.emptyList;
37 | import static java.util.Collections.emptyMap;
38 | import static java.util.Comparator.comparing;
39 | import static java.util.function.UnaryOperator.identity;
40 | import static java.util.stream.Collectors.toMap;
41 | import static java.util.stream.Collectors.toUnmodifiableMap;
42 | import static java.util.stream.Collectors.toUnmodifiableSet;
43 |
44 | @DefaultQualifier(NonNull.class)
45 | public final class Mods {
46 | private static final String QSL_MOD_ID = "qsl";
47 | private static final String LEGACY_FABRIC_API_MOD_ID = "fabric";
48 | private static final String FABRIC_API_MOD_ID = "fabric-api";
49 | private static final String QUILTED_FABRIC_API_MOD_ID = "quilted_fabric_api";
50 | private static final String FABRIC_API_MODULE_MARKER = "fabric-api:module-lifecycle";
51 | private static final String LOOM_GENERATED_MARKER = "fabric-loom:generated";
52 |
53 | private final Set mods;
54 | private final Map modsById;
55 | private final Map rootMods;
56 |
57 | private Mods() {
58 | this.rootMods = loadModDescriptions();
59 | this.mods = this.rootMods.values().stream().flatMap(ModDescription::selfAndChildren).collect(toUnmodifiableSet());
60 | this.modsById = this.mods.stream().collect(toUnmodifiableMap(ModDescription::modId, identity()));
61 | }
62 |
63 | public @Nullable ModDescription findMod(final String modId) {
64 | return this.modsById.get(modId);
65 | }
66 |
67 | public int totalModCount() {
68 | return this.mods.size();
69 | }
70 |
71 | public int topLevelModCount() {
72 | return this.rootMods.size();
73 | }
74 |
75 | public Stream allMods() {
76 | return this.mods.stream();
77 | }
78 |
79 | public Collection topLevelMods() {
80 | return this.rootMods.values();
81 | }
82 |
83 | public static Mods mods() {
84 | return Holder.INSTANCE;
85 | }
86 |
87 | private static Map loadModDescriptions() {
88 | final FabricLoader loader = FabricLoader.getInstance();
89 |
90 | final Set hiddenModIds = ModsCommandModInitializer.instance().config().hiddenModIds();
91 |
92 | final Map descriptions = loader.getAllMods().stream()
93 | .map(ModContainer::getMetadata)
94 | .map(ModDescription::fromFabric)
95 | .filter(mod -> !hiddenModIds.contains(mod.modId()))
96 | .collect(toMap(ModDescription::modId, identity()));
97 |
98 | arrangeQSLChildren(descriptions);
99 | arrangeQFapiChildren(descriptions);
100 | arrangeFapiChildren(descriptions);
101 | arrangeLoomGenerated(descriptions);
102 | arrangeChildModsUsingModMenuMetadata(descriptions);
103 |
104 | return ImmutableMap.builder()
105 | .orderEntriesByValue(comparing(ModDescription::modId))
106 | .putAll(descriptions)
107 | .build();
108 | }
109 |
110 | private static void arrangeChildModsUsingModMenuMetadata(final Map descriptions) {
111 | final Map> byParent = new HashMap<>();
112 | descriptions.values().forEach(modDescription -> {
113 | final @Nullable String parent = parentUsingModMenuMetadata(modDescription);
114 | if (parent == null) {
115 | return;
116 | }
117 | byParent.computeIfAbsent(parent, $ -> new ArrayList<>()).add(modDescription);
118 | });
119 | byParent.forEach((parentId, children) -> {
120 | final @Nullable ModDescription parent = descriptions.get(parentId);
121 | if (parent == null) {
122 | return;
123 | }
124 | for (final ModDescription child : children) {
125 | descriptions.remove(child.modId());
126 | ((AbstractModDescription) parent).addChild(child);
127 | }
128 | });
129 | }
130 |
131 | private static void arrangeQSLChildren(final Map descriptions) {
132 | final List qslModules = findChildrenUsingModMenuMetadata(QSL_MOD_ID, descriptions);
133 | if (qslModules.isEmpty()) {
134 | return;
135 | }
136 | // QSL mod may not exist (in case of qfapi qsl)
137 | final ModDescription qsl = descriptions.computeIfAbsent(QSL_MOD_ID, id -> ModDescription.create(
138 | qslModules,
139 | id,
140 | "Quilt Standard Libraries",
141 | "",
142 | "quilt",
143 | "A set of libraries to assist in making Quilt mods.",
144 | List.of("QuiltMC: QSL Team"),
145 | emptyList(),
146 | emptyList(),
147 | emptyMap(),
148 | Environment.UNIVERSAL
149 | ));
150 | qslModules.forEach(module -> {
151 | descriptions.remove(module.modId());
152 | if (!qsl.children().contains(module)) {
153 | ((AbstractModDescription) qsl).addChild(module);
154 | }
155 | });
156 | }
157 |
158 | private static void arrangeQFapiChildren(final Map descriptions) {
159 | final @Nullable ModDescription qfapi = descriptions.get(QUILTED_FABRIC_API_MOD_ID);
160 | if (qfapi != null) {
161 | final List qfapiModules = descriptions.values().stream()
162 | .filter(it -> {
163 | return it.hasAttribute(ModMetadata.class)
164 | && it.attribute(ModMetadata.class).containsCustomValue(FABRIC_API_MODULE_MARKER)
165 | && it.modId().startsWith("quilted_"); // not ideal, but works
166 | })
167 | .toList();
168 | qfapiModules.forEach(module -> {
169 | descriptions.remove(module.modId());
170 | ((AbstractModDescription) qfapi).addChild(module);
171 | });
172 | }
173 | }
174 |
175 | private static void arrangeFapiChildren(final Map descriptions) {
176 | final @Nullable ModDescription fapi = fabricApi(descriptions);
177 | if (fapi == null) {
178 | return;
179 | }
180 | final List fapiModules = descriptions.values().stream()
181 | .filter(it -> it.hasAttribute(ModMetadata.class) && it.attribute(ModMetadata.class).containsCustomValue(FABRIC_API_MODULE_MARKER))
182 | .toList();
183 | fapiModules.forEach(module -> {
184 | descriptions.remove(module.modId());
185 | ((AbstractModDescription) fapi).addChild(module);
186 | });
187 | }
188 |
189 | private static @Nullable ModDescription fabricApi(final Map descriptions) {
190 | @Nullable ModDescription fapi = descriptions.get(FABRIC_API_MOD_ID);
191 | if (fapi == null) {
192 | fapi = descriptions.get(LEGACY_FABRIC_API_MOD_ID);
193 | }
194 | return fapi;
195 | }
196 |
197 | private static void arrangeLoomGenerated(final Map descriptions) {
198 | final List loomGeneratedMods = descriptions.values().stream()
199 | .filter(it -> it.hasAttribute(ModMetadata.class) && it.attribute(ModMetadata.class).containsCustomValue(LOOM_GENERATED_MARKER))
200 | .toList();
201 | loomGeneratedMods.forEach(module -> descriptions.remove(module.modId()));
202 | if (!loomGeneratedMods.isEmpty()) {
203 | descriptions.put(
204 | "loom-generated",
205 | ModDescription.create(
206 | loomGeneratedMods,
207 | "loom-generated",
208 | "Loom Generated",
209 | "",
210 | "category",
211 | "Parent mod to all Loom-generated library mods.",
212 | emptyList(),
213 | emptyList(),
214 | emptyList(),
215 | emptyMap(),
216 | Environment.UNIVERSAL
217 | )
218 | );
219 | }
220 | }
221 |
222 | private static List findChildrenUsingModMenuMetadata(final String parentId, final Map descriptions) {
223 | return descriptions.values().stream()
224 | .filter(description -> parentId.equals(parentUsingModMenuMetadata(description)))
225 | .toList();
226 | }
227 |
228 | private static @Nullable String parentUsingModMenuMetadata(final ModDescription modDescription) {
229 | if (!modDescription.hasAttribute(ModMetadata.class)) {
230 | return null;
231 | }
232 | final ModMetadata meta = modDescription.attribute(ModMetadata.class);
233 | if (!meta.containsCustomValue("modmenu")
234 | || !meta.getCustomValue("modmenu").getAsObject().containsKey("parent")) {
235 | return null;
236 | }
237 | final CustomValue parent = meta.getCustomValue("modmenu")
238 | .getAsObject()
239 | .get("parent");
240 | if (parent.getType() == CustomValue.CvType.STRING) {
241 | return parent.getAsString();
242 | } else {
243 | return parent.getAsObject().get("id").getAsString();
244 | }
245 | }
246 |
247 | private static final class Holder {
248 | static final Mods INSTANCE = new Mods();
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/util/BiIntFunction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.util;
18 |
19 | @FunctionalInterface
20 | public interface BiIntFunction {
21 | T apply(int i, int i1);
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/util/Colors.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.util;
18 |
19 | import net.kyori.adventure.text.format.TextColor;
20 | import org.checkerframework.checker.nullness.qual.NonNull;
21 | import org.checkerframework.framework.qual.DefaultQualifier;
22 |
23 | import static net.kyori.adventure.text.format.TextColor.color;
24 |
25 | @DefaultQualifier(NonNull.class)
26 | public final class Colors {
27 | private Colors() {
28 | }
29 |
30 | public static final TextColor EMERALD = color(0x4BE173);
31 | public static final TextColor ORANGE = color(0xED9234);
32 | public static final TextColor BLUE = color(0x1E90FF);
33 | public static final TextColor BRIGHT_BLUE = color(0x7DCFE2);
34 | public static final TextColor MIDNIGHT_BLUE = color(0x4D4F58);
35 | public static final TextColor PINK = color(0xDF678C);
36 | public static final TextColor PURPLE = color(0x843DE8);
37 | public static final TextColor MUSTARD = color(0xFEE455);
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/xyz/jpenilla/modscommand/util/Pagination.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Mods Command
3 | * Copyright (c) 2022 Jason Penilla
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package xyz.jpenilla.modscommand.util;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Collection;
21 | import java.util.Collections;
22 | import java.util.Iterator;
23 | import java.util.List;
24 | import java.util.RandomAccess;
25 | import net.kyori.adventure.text.Component;
26 | import net.kyori.adventure.text.ComponentLike;
27 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
28 | import org.checkerframework.checker.nullness.qual.NonNull;
29 | import org.checkerframework.framework.qual.DefaultQualifier;
30 |
31 | import static java.util.Objects.requireNonNull;
32 | import static net.kyori.adventure.text.Component.empty;
33 |
34 | @DefaultQualifier(NonNull.class)
35 | public interface Pagination {
36 | ComponentLike header(int page, int pages);
37 |
38 | ComponentLike footer(int page, int pages);
39 |
40 | ComponentLike pageOutOfRange(int page, int pages);
41 |
42 | ComponentLike item(T item, boolean lastOfPage);
43 |
44 | default List render(
45 | final Collection content,
46 | final int page,
47 | final int itemsPerPage
48 | ) {
49 | if (content.isEmpty()) {
50 | throw new IllegalArgumentException("Cannot paginate an empty collection.");
51 | }
52 |
53 | final int pages = (int) Math.ceil(content.size() / (itemsPerPage * 1.00));
54 | if (page < 1 || page > pages) {
55 | return Collections.singletonList(this.pageOutOfRange(page, pages).asComponent());
56 | }
57 |
58 | final List renderedContent = new ArrayList<>();
59 |
60 | final Component header = this.header(page, pages).asComponent();
61 | if (header != empty()) {
62 | renderedContent.add(header);
63 | }
64 |
65 | final int start = itemsPerPage * (page - 1);
66 | final int maxIndex = start + itemsPerPage;
67 |
68 | if (content instanceof RandomAccess && content instanceof final List contentList) {
69 | for (int i = start; i < maxIndex; i++) {
70 | if (i > content.size() - 1) {
71 | break;
72 | }
73 | renderedContent.add(this.item(contentList.get(i), i == maxIndex - 1).asComponent());
74 | }
75 | } else {
76 | final Iterator iterator = content.iterator();
77 | for (int i = 0; i < start && iterator.hasNext(); i++) {
78 | iterator.next();
79 | }
80 | for (int i = start; i < maxIndex && iterator.hasNext(); ++i) {
81 | renderedContent.add(this.item(iterator.next(), i == maxIndex - 1).asComponent());
82 | }
83 | }
84 |
85 | final Component footer = this.footer(page, pages).asComponent();
86 | if (footer != empty()) {
87 | renderedContent.add(footer);
88 | }
89 |
90 | return Collections.unmodifiableList(renderedContent);
91 | }
92 |
93 | static Builder builder() {
94 | return new Builder<>();
95 | }
96 |
97 | final class Builder {
98 | private BiIntFunction headerRenderer = ($, $$) -> empty();
99 | private BiIntFunction footerRenderer = ($, $$) -> empty();
100 | private @MonotonicNonNull BiIntFunction pageOutOfRangeRenderer = null;
101 | private @MonotonicNonNull ItemRenderer itemRenderer = null;
102 |
103 | private Builder() {
104 | }
105 |
106 | public Builder header(final BiIntFunction headerRenderer) {
107 | this.headerRenderer = headerRenderer;
108 | return this;
109 | }
110 |
111 | public Builder footer(final BiIntFunction footerRenderer) {
112 | this.footerRenderer = footerRenderer;
113 | return this;
114 | }
115 |
116 | public Builder pageOutOfRange(final BiIntFunction pageOutOfRangeRenderer) {
117 | this.pageOutOfRangeRenderer = pageOutOfRangeRenderer;
118 | return this;
119 | }
120 |
121 | public Builder item(final ItemRenderer itemRenderer) {
122 | this.itemRenderer = itemRenderer;
123 | return this;
124 | }
125 |
126 | public Pagination build() {
127 | return new DelegatingPaginationImpl<>(
128 | requireNonNull(this.headerRenderer, "Must provide a header renderer!"),
129 | requireNonNull(this.footerRenderer, "Must provide a footer renderer!"),
130 | requireNonNull(this.pageOutOfRangeRenderer, "Must provide a page out of range renderer!"),
131 | requireNonNull(this.itemRenderer, "Must provide an item renderer!")
132 | );
133 | }
134 |
135 | @FunctionalInterface
136 | public interface ItemRenderer {
137 | ComponentLike render(T item, boolean lastOfPage);
138 | }
139 |
140 | private record DelegatingPaginationImpl(
141 | BiIntFunction headerRenderer,
142 | BiIntFunction footerRenderer,
143 | BiIntFunction pageOutOfRangeRenderer,
144 | ItemRenderer itemRenderer
145 | ) implements Pagination {
146 | @Override
147 | public ComponentLike header(final int page, final int pages) {
148 | return this.headerRenderer.apply(page, pages);
149 | }
150 |
151 | @Override
152 | public ComponentLike footer(final int page, final int pages) {
153 | return this.footerRenderer.apply(page, pages);
154 | }
155 |
156 | @Override
157 | public ComponentLike pageOutOfRange(final int page, final int pages) {
158 | return this.pageOutOfRangeRenderer.apply(page, pages);
159 | }
160 |
161 | @Override
162 | public ComponentLike item(final T item, final boolean lastOfPage) {
163 | return this.itemRenderer.render(item, lastOfPage);
164 | }
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/resources/assets/mods-command/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jpenilla/mods-command/afc3aa251bac7faeabbda69c7e89be8aabd73fe7/src/main/resources/assets/mods-command/icon.png
--------------------------------------------------------------------------------